diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index cee055eaf95af..08bfdb6a418e0 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -340,6 +340,11 @@ 205ef031e0fe5152dede0bd9f99d0f6f9e7f1e45 + + https://github.com/dotnet/runtime + 4dffd80c4d77c27e772a0be26e8036af77fbb26e + + https://github.com/dotnet/runtime 205ef031e0fe5152dede0bd9f99d0f6f9e7f1e45 diff --git a/eng/Versions.props b/eng/Versions.props index 92943f75e5fce..082210e390a50 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -123,7 +123,7 @@ 5.0.0 1.2.0-beta.507 4.5.1 - 7.0.0 + 8.0.0 5.0.0 4.8.6 8.0.0 @@ -131,8 +131,8 @@ 4.5.5 9.0.0-alpha.1.24072.1 - 7.0.0 - 7.0.0 + 8.0.0 + 8.0.0 6.0.0 5.0.0 5.0.0 diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SNPrintF.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SNPrintF.cs index fada5626b317f..39695cde2e5fa 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SNPrintF.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SNPrintF.cs @@ -26,7 +26,7 @@ internal static partial class Sys /// success; if the return value is equal to the size then the result may have been truncated. /// On failure, returns a negative value. /// - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SNPrintF", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SNPrintF_1S", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] internal static unsafe partial int SNPrintF(byte* str, int size, string format, string arg1); /// @@ -47,7 +47,7 @@ internal static partial class Sys /// success; if the return value is equal to the size then the result may have been truncated. /// On failure, returns a negative value. /// - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SNPrintF", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SNPrintF_1I", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] internal static unsafe partial int SNPrintF(byte* str, int size, string format, int arg1); } } diff --git a/src/mono/mono/mini/aot-runtime-wasm.c b/src/mono/mono/mini/aot-runtime-wasm.c index 2ab4ae75301ce..cf1ab02392934 100644 --- a/src/mono/mono/mini/aot-runtime-wasm.c +++ b/src/mono/mono/mini/aot-runtime-wasm.c @@ -54,6 +54,14 @@ type_to_c (MonoType *t) goto handle_enum; } + // https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md#function-signatures + // Any struct or union that recursively (including through nested structs, unions, and arrays) + // contains just a single scalar value and is not specified to have greater than natural alignment. + // FIXME: Handle the scenario where there are fields of struct types that contain no members + MonoType *scalar_vtype; + if (mini_wasm_is_scalar_vtype (t, &scalar_vtype)) + return type_to_c (scalar_vtype); + return 'I'; case MONO_TYPE_GENERICINST: if (m_class_is_valuetype (t->data.klass)) diff --git a/src/mono/mono/mini/interp/interp.c b/src/mono/mono/mini/interp/interp.c index afeb41929ec2c..59f7c09e53066 100644 --- a/src/mono/mono/mini/interp/interp.c +++ b/src/mono/mono/mini/interp/interp.c @@ -1345,10 +1345,22 @@ typedef enum { typedef struct { int ilen, flen; - PInvokeArgType ret_type; + MonoType *ret_mono_type; + PInvokeArgType ret_pinvoke_type; PInvokeArgType *arg_types; } BuildArgsFromSigInfo; +static MonoType * +filter_type_for_args_from_sig (MonoType *type) { +#if defined(HOST_WASM) + MonoType *etype; + if (MONO_TYPE_ISSTRUCT (type) && mini_wasm_is_scalar_vtype (type, &etype)) + // FIXME: Does this need to be recursive? + return etype; +#endif + return type; +} + static BuildArgsFromSigInfo * get_build_args_from_sig_info (MonoMemoryManager *mem_manager, MonoMethodSignature *sig) { @@ -1360,7 +1372,7 @@ get_build_args_from_sig_info (MonoMemoryManager *mem_manager, MonoMethodSignatur g_assert (!sig->hasthis); for (int i = 0; i < sig->param_count; i++) { - MonoType *type = sig->params [i]; + MonoType *type = filter_type_for_args_from_sig (sig->params [i]); guint32 ptype; retry: @@ -1442,7 +1454,9 @@ get_build_args_from_sig_info (MonoMemoryManager *mem_manager, MonoMethodSignatur info->ilen = ilen; info->flen = flen; - switch (sig->ret->type) { + info->ret_mono_type = filter_type_for_args_from_sig (sig->ret); + + switch (info->ret_mono_type->type) { case MONO_TYPE_BOOLEAN: case MONO_TYPE_CHAR: case MONO_TYPE_I1: @@ -1463,17 +1477,17 @@ get_build_args_from_sig_info (MonoMemoryManager *mem_manager, MonoMethodSignatur case MONO_TYPE_U8: case MONO_TYPE_VALUETYPE: case MONO_TYPE_GENERICINST: - info->ret_type = PINVOKE_ARG_INT; + info->ret_pinvoke_type = PINVOKE_ARG_INT; break; case MONO_TYPE_R4: case MONO_TYPE_R8: - info->ret_type = PINVOKE_ARG_R8; + info->ret_pinvoke_type = PINVOKE_ARG_R8; break; case MONO_TYPE_VOID: - info->ret_type = PINVOKE_ARG_NONE; + info->ret_pinvoke_type = PINVOKE_ARG_NONE; break; default: - g_error ("build_args_from_sig: ret type not implemented yet: 0x%x\n", sig->ret->type); + g_error ("build_args_from_sig: ret type not implemented yet: 0x%x\n", info->ret_mono_type->type); } return info; @@ -1563,7 +1577,7 @@ build_args_from_sig (InterpMethodArguments *margs, MonoMethodSignature *sig, Bui } } - switch (info->ret_type) { + switch (info->ret_pinvoke_type) { case PINVOKE_ARG_INT: margs->retval = (gpointer*)frame->retval; margs->is_float_ret = 0; @@ -1781,8 +1795,8 @@ ves_pinvoke_method ( g_free (ccontext.stack); #else // Only the vt address has been returned, we need to copy the entire content on interp stack - if (!context->has_resume_state && MONO_TYPE_ISSTRUCT (sig->ret)) - stackval_from_data (sig->ret, frame.retval, (char*)frame.retval->data.p, sig->pinvoke && !sig->marshalling_disabled); + if (!context->has_resume_state && MONO_TYPE_ISSTRUCT (call_info->ret_mono_type)) + stackval_from_data (call_info->ret_mono_type, frame.retval, (char*)frame.retval->data.p, sig->pinvoke && !sig->marshalling_disabled); if (margs.iargs != margs.iargs_buf) g_free (margs.iargs); @@ -4252,7 +4266,7 @@ mono_interp_exec_method (InterpFrame *frame, ThreadContext *context, FrameClause LOCAL_VAR (call_args_offset, gpointer) = unboxed; } -jit_call: +jit_call: { InterpMethodCodeType code_type = cmethod->code_type; diff --git a/src/mono/mono/mini/mini-llvm.c b/src/mono/mono/mini/mini-llvm.c index e6087568efc4f..9a01d6248f26b 100644 --- a/src/mono/mono/mini/mini-llvm.c +++ b/src/mono/mono/mini/mini-llvm.c @@ -4192,6 +4192,7 @@ emit_entry_bb (EmitContext *ctx, LLVMBuilderRef builder) case LLVMArgVtypeAddr: case LLVMArgVtypeByRef: case LLVMArgAsFpArgs: + case LLVMArgWasmVtypeAsScalar: { MonoClass *klass = mono_class_from_mono_type_internal (ainfo->type); if (mini_class_is_simd (ctx->cfg, klass)) { @@ -4968,6 +4969,8 @@ process_call (EmitContext *ctx, MonoBasicBlock *bb, LLVMBuilderRef *builder_ref, if (!addresses [call->inst.dreg]) addresses [call->inst.dreg] = build_alloca_address (ctx, sig->ret); emit_store (builder, lcall, convert_full (ctx, addresses [call->inst.dreg]->value, pointer_type (LLVMTypeOf (lcall)), FALSE), is_volatile); + load_name = "wasm_vtype_as_scalar"; + should_promote_to_value = TRUE; break; } default: @@ -5421,6 +5424,7 @@ static LLVMValueRef concatenate_vectors (EmitContext *ctx, LLVMValueRef xs, LLVMValueRef ys) { LLVMTypeRef t = LLVMTypeOf (xs); + g_assert (LLVMGetTypeKind (t) == LLVMVectorTypeKind); unsigned int elems = LLVMGetVectorSize (t) * 2; int mask [MAX_VECTOR_ELEMS] = { 0 }; for (guint i = 0; i < elems; ++i) @@ -6175,8 +6179,13 @@ process_bb (EmitContext *ctx, MonoBasicBlock *bb) } break; case LLVMArgWasmVtypeAsScalar: - g_assert (addresses [ins->sreg1]); - retval = LLVMBuildLoad2 (builder, ret_type, build_ptr_cast (builder, addresses [ins->sreg1]->value, pointer_type (ret_type)), ""); + if (!addresses [ins->sreg1]) { + /* SIMD value */ + g_assert (lhs); + retval = LLVMBuildBitCast (builder, lhs, ret_type, ""); + } else { + retval = LLVMBuildLoad2 (builder, ret_type, build_ptr_cast (builder, addresses [ins->sreg1]->value, pointer_type (ret_type)), ""); + } break; } LLVMBuildRet (builder, retval); @@ -6225,8 +6234,8 @@ process_bb (EmitContext *ctx, MonoBasicBlock *bb) if (lhs) { // Vector3: ret_type is Vector3, lhs is Vector3 represented as a Vector4 (three elements + zero). We need to extract only the first 3 elements from lhs. - int len = mono_class_value_size (klass, NULL) == 12 ? 3 : LLVMGetVectorSize (LLVMTypeOf (lhs)); - + int len = mono_class_value_size (klass, NULL) == 12 ? 3 : LLVMGetVectorSize (LLVMTypeOf (lhs)); + for (int i = 0; i < len; i++) { elem = LLVMBuildExtractElement (builder, lhs, const_int32 (i), "extract_elem"); retval = LLVMBuildInsertValue (builder, retval, elem, i, "insert_val_struct"); @@ -6841,7 +6850,7 @@ MONO_RESTORE_WARNING // LLVM should fuse the individual Div and Rem instructions into one DIV/IDIV on x86 values [ins->dreg] = LLVMBuildTrunc (builder, LLVMBuildSDiv (builder, dividend, divisor, ""), part_type, ""); last_divrem = LLVMBuildTrunc (builder, LLVMBuildSRem (builder, dividend, divisor, ""), part_type, ""); - break; + break; } case OP_X86_IDIVREMU: case OP_X86_LDIVREMU: { @@ -6856,7 +6865,7 @@ MONO_RESTORE_WARNING LLVMValueRef divisor = LLVMBuildZExt (builder, convert (ctx, arg3, part_type), full_type, ""); values [ins->dreg] = LLVMBuildTrunc (builder, LLVMBuildUDiv (builder, dividend, divisor, ""), part_type, ""); last_divrem = LLVMBuildTrunc (builder, LLVMBuildURem (builder, dividend, divisor, ""), part_type, ""); - break; + break; } case OP_X86_IDIVREM2: case OP_X86_LDIVREM2: { @@ -10671,7 +10680,7 @@ MONO_RESTORE_WARNING // convert to 0/1 result = LLVMBuildICmp (builder, LLVMIntEQ, first_elem, LLVMConstAllOnes (LLVMInt64Type ()), ""); - + values [ins->dreg] = LLVMBuildZExt (builder, result, LLVMInt8Type (), ""); break; } @@ -12020,7 +12029,7 @@ MONO_RESTORE_WARNING gboolean scalar = ins->opcode == OP_NEGATION_SCALAR; gboolean is_float = (ins->inst_c1 == MONO_TYPE_R4 || ins->inst_c1 == MONO_TYPE_R8); - LLVMValueRef result = lhs; + LLVMValueRef result = lhs; if (scalar) result = scalar_from_vector (ctx, result); if (is_float) diff --git a/src/mono/mono/mini/mini-wasm.c b/src/mono/mono/mini/mini-wasm.c index e0849ba463623..af943e24adece 100644 --- a/src/mono/mono/mini/mini-wasm.c +++ b/src/mono/mono/mini/mini-wasm.c @@ -774,8 +774,6 @@ mini_wasm_is_scalar_vtype (MonoType *type, MonoType **etype) return FALSE; } else if (!((MONO_TYPE_IS_PRIMITIVE (t) || MONO_TYPE_IS_REFERENCE (t) || MONO_TYPE_IS_POINTER (t)))) { return FALSE; - } else if (size == 8 && t->type != MONO_TYPE_R8) { - return FALSE; } else { if (etype) *etype = t; diff --git a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs index 179d769d1e662..f553f10ddeb36 100644 --- a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -50,7 +51,9 @@ public static int Main(string[] args) buildArgs with { ProjectName = $"variadic_{buildArgs.Config}_{id}" }, id); Assert.Matches("warning.*native function.*sum.*varargs", output); - Assert.Matches("warning.*sum_(one|two|three)", output); + Assert.Contains("System.Int32 sum_one(System.Int32)", output); + Assert.Contains("System.Int32 sum_two(System.Int32, System.Int32)", output); + Assert.Contains("System.Int32 sum_three(System.Int32, System.Int32, System.Int32)", output); output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id); Assert.Contains("Main running", output); @@ -58,7 +61,7 @@ public static int Main(string[] args) [Theory] [BuildAndRun(host: RunHost.Chrome)] - public void DllImportWithFunctionPointersCompilesWithWarning(BuildArgs buildArgs, RunHost host, string id) + public void DllImportWithFunctionPointersCompilesWithoutWarning(BuildArgs buildArgs, RunHost host, string id) { string code = """ @@ -84,8 +87,8 @@ public static int Main() buildArgs with { ProjectName = $"fnptr_{buildArgs.Config}_{id}" }, id); - Assert.Matches("warning\\sWASM0001.*Could\\snot\\sget\\spinvoke.*Parsing\\sfunction\\spointer\\stypes", output); - Assert.Matches("warning\\sWASM0001.*Skipping.*using_sum_one.*because.*function\\spointer", output); + Assert.DoesNotMatch("warning\\sWASM0001.*Could\\snot\\sget\\spinvoke.*Parsing\\sfunction\\spointer\\stypes", output); + Assert.DoesNotMatch("warning\\sWASM0001.*Skipping.*using_sum_one.*because.*function\\spointer", output); output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id); Assert.Contains("Main running", output); @@ -114,8 +117,8 @@ public static int Main() buildArgs with { ProjectName = $"fnptr_variadic_{buildArgs.Config}_{id}" }, id); - Assert.Matches("warning\\sWASM0001.*Could\\snot\\sget\\spinvoke.*Parsing\\sfunction\\spointer\\stypes", output); - Assert.Matches("warning\\sWASM0001.*Skipping.*using_sum_one.*because.*function\\spointer", output); + Assert.DoesNotMatch("warning\\sWASM0001.*Could\\snot\\sget\\spinvoke.*Parsing\\sfunction\\spointer\\stypes", output); + Assert.DoesNotMatch("warning\\sWASM0001.*Skipping.*using_sum_one.*because.*function\\spointer", output); output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id); Assert.Contains("Main running", output); @@ -128,6 +131,7 @@ public void UnmanagedStructAndMethodIn_SameAssembly_WithoutDisableRuntimeMarshal { (_, string output) = SingleProjectForDisabledRuntimeMarshallingTest( withDisabledRuntimeMarshallingAttribute: false, + withAutoLayout: true, expectSuccess: false, buildArgs, id @@ -136,6 +140,22 @@ public void UnmanagedStructAndMethodIn_SameAssembly_WithoutDisableRuntimeMarshal Assert.Matches("error.*Parameter.*types.*pinvoke.*.*blittable", output); } + [Theory] + [BuildAndRun(host: RunHost.None)] + public void UnmanagedStructAndMethodIn_SameAssembly_WithoutDisableRuntimeMarshallingAttribute_WithStructLayout_ConsideredBlittable + (BuildArgs buildArgs, string id) + { + (_, string output) = SingleProjectForDisabledRuntimeMarshallingTest( + withDisabledRuntimeMarshallingAttribute: false, + withAutoLayout: false, + expectSuccess: true, + buildArgs, + id + ); + + Assert.DoesNotMatch("error.*Parameter.*types.*pinvoke.*.*blittable", output); + } + [Theory] [BuildAndRun(host: RunHost.Chrome)] public void UnmanagedStructAndMethodIn_SameAssembly_WithDisableRuntimeMarshallingAttribute_ConsideredBlittable @@ -143,6 +163,7 @@ public void UnmanagedStructAndMethodIn_SameAssembly_WithDisableRuntimeMarshallin { (buildArgs, _) = SingleProjectForDisabledRuntimeMarshallingTest( withDisabledRuntimeMarshallingAttribute: true, + withAutoLayout: true, expectSuccess: true, buildArgs, id @@ -152,8 +173,10 @@ public void UnmanagedStructAndMethodIn_SameAssembly_WithDisableRuntimeMarshallin Assert.Contains("Main running 5", output); } - private (BuildArgs buildArgs ,string output) SingleProjectForDisabledRuntimeMarshallingTest(bool withDisabledRuntimeMarshallingAttribute, bool expectSuccess, BuildArgs buildArgs, string id) - { + private (BuildArgs buildArgs ,string output) SingleProjectForDisabledRuntimeMarshallingTest( + bool withDisabledRuntimeMarshallingAttribute, bool withAutoLayout, + bool expectSuccess, BuildArgs buildArgs, string id + ) { string code = """ using System; @@ -171,8 +194,10 @@ public static int Main() Console.WriteLine("Main running " + x.Value); return 42; } - - public struct S { public int Value; } + """ + + (withAutoLayout ? "\n[StructLayout(LayoutKind.Auto)]\n" : "") + + """ + public struct S { public int Value; public float Value2; } [UnmanagedCallersOnly] public static void M(S myStruct) { } @@ -230,7 +255,7 @@ private void SeparateAssembliesForDisableRuntimeMarshallingTest { string code = (libraryHasAttribute ? "[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]" : "") - + "public struct S { public int Value; }"; + + "public struct __NonBlittableTypeForAutomatedTests__ { } public struct S { public int Value; public __NonBlittableTypeForAutomatedTests__ NonBlittable; }"; var libraryBuildArgs = ExpandBuildArgs( buildArgs with { ProjectName = $"blittable_different_library_{buildArgs.Config}_{id}" }, @@ -256,6 +281,7 @@ private void SeparateAssembliesForDisableRuntimeMarshallingTest using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; + """ + (appHasAttribute ? "[assembly: DisableRuntimeMarshalling]" : "") + """ @@ -372,7 +398,7 @@ public static int Main() id ); - Assert.Matches("warning\\sWASM0001.*Skipping.*Test::SomeFunction1.*because.*function\\spointer", output); + Assert.DoesNotMatch("warning\\sWASM0001.*Skipping.*Test::SomeFunction1.*because.*function\\spointer", output); } [Theory] @@ -406,7 +432,7 @@ file class Foo ); Assert.DoesNotMatch(".*(warning|error).*>[A-Z0-9]+__Foo", output); - + output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id); Assert.Contains("Main running", output); } @@ -692,5 +718,122 @@ void GenerateSourceFiles(string outputPath, int baseArg) return (buildArgs, output); } + + private void EnsureWasmAbiRulesAreFollowed(BuildArgs buildArgs, RunHost host, string id) + { + string programText = @" + using System; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + public struct SingleFloatStruct { + public float Value; + } + public struct SingleDoubleStruct { + public struct Nested1 { + // This field is private on purpose to ensure we treat visibility correctly + double Value; + } + public Nested1 Value; + } + public struct SingleI64Struct { + public Int64 Value; + } + + public class Test + { + public static unsafe int Main(string[] argv) + { + var i64_a = 0xFF00FF00FF00FF0L; + var i64_b = ~i64_a; + var resI = direct64(i64_a); + Console.WriteLine(""l (l)="" + resI); + + var sis = new SingleI64Struct { Value = i64_a }; + var resSI = indirect64(sis); + Console.WriteLine(""s (s)="" + resSI.Value); + + var resF = direct(3.14); + Console.WriteLine(""f (d)="" + resF); + + SingleDoubleStruct sds = default; + Unsafe.As(ref sds) = 3.14; + + resF = indirect_arg(sds); + Console.WriteLine(""f (s)="" + resF); + + var res = indirect(sds); + Console.WriteLine(""s (s)="" + res.Value); + + return (int)res.Value; + } + + [DllImport(""wasm-abi"", EntryPoint=""accept_double_struct_and_return_float_struct"")] + public static extern SingleFloatStruct indirect(SingleDoubleStruct arg); + + [DllImport(""wasm-abi"", EntryPoint=""accept_double_struct_and_return_float_struct"")] + public static extern float indirect_arg(SingleDoubleStruct arg); + + [DllImport(""wasm-abi"", EntryPoint=""accept_double_struct_and_return_float_struct"")] + public static extern float direct(double arg); + + [DllImport(""wasm-abi"", EntryPoint=""accept_and_return_i64_struct"")] + public static extern SingleI64Struct indirect64(SingleI64Struct arg); + + [DllImport(""wasm-abi"", EntryPoint=""accept_and_return_i64_struct"")] + public static extern Int64 direct64(Int64 arg); + }"; + + var extraProperties = "true<_WasmDevel>true"; + var extraItems = @""; + + buildArgs = ExpandBuildArgs(buildArgs, + extraItems: extraItems, + extraProperties: extraProperties); + + (string libraryDir, string output) = BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + InitProject: () => + { + File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText); + File.Copy(Path.Combine(BuildEnvironment.TestAssetsPath, "native-libs", "wasm-abi.c"), + Path.Combine(_projectDir!, "wasm-abi.c")); + }, + Publish: buildArgs.AOT, + // Verbosity: "diagnostic", + DotnetWasmFromRuntimePack: false)); + + string objDir = Path.Combine(_projectDir!, "obj", buildArgs.Config!, "net9.0", "browser-wasm", "wasm", buildArgs.AOT ? "for-publish" : "for-build"); + + // Verify that the right signature was added for the pinvoke. We can't determine this by examining the m2n file + // FIXME: Not possible in in-process mode for some reason, even with verbosity at "diagnostic" + // Assert.Contains("Adding pinvoke signature FD for method 'Test.", output); + + string pinvokeTable = File.ReadAllText(Path.Combine(objDir, "pinvoke-table.h")); + // Verify that the invoke is in the pinvoke table. Under various circumstances we will silently skip it, + // for example if the module isn't found + Assert.Contains("\"accept_double_struct_and_return_float_struct\", accept_double_struct_and_return_float_struct", pinvokeTable); + // Verify the signature of the C function prototype. Wasm ABI specifies that the structs should both decompose into scalars. + Assert.Contains("float accept_double_struct_and_return_float_struct (double);", pinvokeTable); + Assert.Contains("int64_t accept_and_return_i64_struct (int64_t);", pinvokeTable); + + var runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 3, host: host, id: id); + Assert.Contains("l (l)=-1148435428713435121", runOutput); + Assert.Contains("s (s)=-1148435428713435121", runOutput); + Assert.Contains("f (d)=3.14", runOutput); + Assert.Contains("f (s)=3.14", runOutput); + Assert.Contains("s (s)=3.14", runOutput); + } + + [Theory] + [BuildAndRun(host: RunHost.Chrome, aot: true)] + public void EnsureWasmAbiRulesAreFollowedInAOT(BuildArgs buildArgs, RunHost host, string id) => + EnsureWasmAbiRulesAreFollowed(buildArgs, host, id); + + [Theory] + [BuildAndRun(host: RunHost.Chrome, aot: false)] + public void EnsureWasmAbiRulesAreFollowedInInterpreter(BuildArgs buildArgs, RunHost host, string id) => + EnsureWasmAbiRulesAreFollowed(buildArgs, host, id); } } diff --git a/src/mono/wasm/testassets/native-libs/wasm-abi.c b/src/mono/wasm/testassets/native-libs/wasm-abi.c new file mode 100644 index 0000000000000..0ace2037daf2f --- /dev/null +++ b/src/mono/wasm/testassets/native-libs/wasm-abi.c @@ -0,0 +1,29 @@ +#include + +typedef struct { + float value; +} TRes; + +TRes accept_double_struct_and_return_float_struct ( + struct { struct { double value; } value; } arg +) { + printf ( + "&arg=%x (ulonglong)arg=%llx arg.value.value=%lf\n", + (unsigned int)&arg, *(unsigned long long*)&arg, (double)arg.value.value + ); + TRes result = { arg.value.value }; + return result; +} + +typedef struct { + long long value; +} TResI64; + +TResI64 accept_and_return_i64_struct (TResI64 arg) { + printf ( + "&arg=%x (ulonglong)arg=%llx\n", + (unsigned int)&arg, *(unsigned long long*)&arg + ); + TResI64 result = { ~arg.value }; + return result; +} diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 1a5b7dcae503f..ee842ee2b7364 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -228,6 +228,8 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_SetDelayedSigChildConsoleConfigurationHandler) DllImportEntry(SystemNative_SetTerminalInvalidationHandler) DllImportEntry(SystemNative_SNPrintF) + DllImportEntry(SystemNative_SNPrintF_1S) + DllImportEntry(SystemNative_SNPrintF_1I) DllImportEntry(SystemNative_Sysctl) DllImportEntry(SystemNative_MapTcpState) DllImportEntry(SystemNative_LowLevelMonitor_Create) diff --git a/src/native/libs/System.Native/pal_string.c b/src/native/libs/System.Native/pal_string.c index f1a7c65ca0e12..0692df97d07c0 100644 --- a/src/native/libs/System.Native/pal_string.c +++ b/src/native/libs/System.Native/pal_string.c @@ -23,3 +23,13 @@ int32_t SystemNative_SNPrintF(char* string, int32_t size, const char* format, .. va_end(arguments); return result; } + +int32_t SystemNative_SNPrintF_1S(char* string, int32_t size, const char* format, char* str) +{ + return SystemNative_SNPrintF(string, size, format, str); +} + +int32_t SystemNative_SNPrintF_1I(char* string, int32_t size, const char* format, int arg) +{ + return SystemNative_SNPrintF(string, size, format, arg); +} diff --git a/src/native/libs/System.Native/pal_string.h b/src/native/libs/System.Native/pal_string.h index 49160dbd94969..ff69055fec09a 100644 --- a/src/native/libs/System.Native/pal_string.h +++ b/src/native/libs/System.Native/pal_string.h @@ -15,3 +15,12 @@ * On failure, returns a negative value. */ PALEXPORT int32_t SystemNative_SNPrintF(char* string, int32_t size, const char* format, ...); + +/** + * Two specialized overloads for use from Interop.Sys, because these two signatures are not equivalent + * on some architectures (like 64-bit WebAssembly) +*/ + +PALEXPORT int32_t SystemNative_SNPrintF_1S(char* string, int32_t size, const char* format, char* str); + +PALEXPORT int32_t SystemNative_SNPrintF_1I(char* string, int32_t size, const char* format, int arg); diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs index acbe2214bce42..2d21f3820a558 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs @@ -5,6 +5,7 @@ using System.IO; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using WasmAppBuilder; namespace Microsoft.NET.Sdk.WebAssembly; @@ -69,7 +70,8 @@ public override bool Execute() if (Utils.IsNewerThan(dllFilePath, finalWebcil)) { var tmpWebcil = Path.Combine(tmpDir, webcilFileName); - var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: dllFilePath, outputPath: tmpWebcil, logger: Log); + var logAdapter = new LogAdapter(Log); + var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: dllFilePath, outputPath: tmpWebcil, logger: logAdapter); webcilWriter.ConvertToWebcil(); if (!Directory.Exists(candidatePath)) diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj index bf51f45e908d8..a41e88575de77 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj @@ -19,6 +19,7 @@ + diff --git a/src/tasks/WasmAppBuilder/IcallTableGenerator.cs b/src/tasks/WasmAppBuilder/IcallTableGenerator.cs index 8ac582371e0e6..bd90bb31199e0 100644 --- a/src/tasks/WasmAppBuilder/IcallTableGenerator.cs +++ b/src/tasks/WasmAppBuilder/IcallTableGenerator.cs @@ -10,6 +10,7 @@ using System.Reflection; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using WasmAppBuilder; internal sealed class IcallTableGenerator { @@ -19,7 +20,7 @@ internal sealed class IcallTableGenerator private readonly HashSet _signatures = new(); private Dictionary _runtimeIcalls = new Dictionary(); - private TaskLoggingHelper Log { get; set; } + private LogAdapter Log { get; set; } private readonly Func _fixupSymbolName; // @@ -28,7 +29,7 @@ internal sealed class IcallTableGenerator // The runtime icall table should be generated using // mono --print-icall-table // - public IcallTableGenerator(string? runtimeIcallTableFile, Func fixupSymbolName, TaskLoggingHelper log) + public IcallTableGenerator(string? runtimeIcallTableFile, Func fixupSymbolName, LogAdapter log) { Log = log; _fixupSymbolName = fixupSymbolName; @@ -141,7 +142,7 @@ private void ProcessType(Type type) } catch (Exception ex) when (ex is not LogAsErrorException) { - Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, $"Could not get icall, or callbacks for method '{type.FullName}::{method.Name}' because '{ex.Message}'"); + Log.Warning("WASM0001", $"Could not get icall, or callbacks for method '{type.FullName}::{method.Name}' because '{ex.Message}'"); continue; } @@ -193,7 +194,7 @@ private void ProcessType(Type type) } catch (NotImplementedException nie) { - Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, $"Failed to generate icall function for method '[{method.DeclaringType!.Assembly.GetName().Name}] {className}::{method.Name}'" + + Log.Warning("WASM0001", $"Failed to generate icall function for method '[{method.DeclaringType!.Assembly.GetName().Name}] {className}::{method.Name}'" + $" because type '{nie.Message}' is not supported for parameter named '{par.Name}'. Ignoring."); return null; } @@ -206,7 +207,7 @@ private void ProcessType(Type type) void AddSignature(Type type, MethodInfo method) { - string? signature = SignatureMapper.MethodToSignature(method); + string? signature = SignatureMapper.MethodToSignature(method, Log); if (signature == null) { throw new LogAsErrorException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); diff --git a/src/tasks/WasmAppBuilder/InterpToNativeGenerator.cs b/src/tasks/WasmAppBuilder/InterpToNativeGenerator.cs index 40f49fdc4f58e..7dfcab267a843 100644 --- a/src/tasks/WasmAppBuilder/InterpToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/InterpToNativeGenerator.cs @@ -10,6 +10,7 @@ using Microsoft.Build.Utilities; using Microsoft.Build.Framework; using System.Diagnostics.CodeAnalysis; +using WasmAppBuilder; // // This class generates the icall_trampoline_dispatch () function used by the interpreter to call native code on WASM. @@ -20,9 +21,9 @@ internal sealed class InterpToNativeGenerator { - private TaskLoggingHelper Log { get; set; } + private LogAdapter Log { get; set; } - public InterpToNativeGenerator(TaskLoggingHelper log) => Log = log; + public InterpToNativeGenerator(LogAdapter log) => Log = log; public void Generate(IEnumerable cookies, string outputPath) { diff --git a/src/tasks/WasmAppBuilder/LogAdapter.cs b/src/tasks/WasmAppBuilder/LogAdapter.cs new file mode 100644 index 0000000000000..8a068047e2f4a --- /dev/null +++ b/src/tasks/WasmAppBuilder/LogAdapter.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; + +#nullable enable + +namespace WasmAppBuilder; + +public sealed class LogAdapter +{ + public bool HasLoggedErrors + { + get => _helper?.HasLoggedErrors ?? _hasLoggedErrors; + } + + private bool _hasLoggedErrors; + private TaskLoggingHelper? _helper; + private TextWriter? _output, _errorOutput; + + public LogAdapter(TaskLoggingHelper helper) + { + _helper = helper; + _output = null; + _errorOutput = null; + } + + public LogAdapter() + { + _helper = null; + _output = Console.Out; + _errorOutput = Console.Error; + } + + private static string AutoFormat(string s, object[] o) + { + if ((o?.Length ?? 0) > 0) + return string.Format(s!, o!); + else + return s; + } + + public void LogMessage(string s, params object[] o) + { + _helper?.LogMessage(s, o); + _output?.WriteLine(AutoFormat(s, o)); + } + + public void LogMessage(MessageImportance mi, string s, params object[] o) + { + _helper?.LogMessage(mi, s, o); + _output?.WriteLine(AutoFormat(s, o)); + } + + public void InfoHigh(string code, string message, params object[] args) + { + // We use MessageImportance.High to ensure this appears in build output, since + // warnaserror makes warnings hard to use + _helper?.LogMessage(null, code, null, null, 0, 0, 0, 0, MessageImportance.High, message, args); + _output?.WriteLine($"info : {code}: {AutoFormat(message, args)}"); + } + + public void Warning(string code, string message, params object[] args) + { + _helper?.LogWarning(null, code, null, null, 0, 0, 0, 0, message, args); + _errorOutput?.WriteLine($"warning : {code}: {AutoFormat(message, args)}"); + } + + public void Error(string message) + { + _helper?.LogError(message); + _errorOutput?.WriteLine($"error : {message}"); + _hasLoggedErrors = true; + } + + public void Error(string code, string message, params object[] args) + { + _helper?.LogError(null, code, null, null, 0, 0, 0, 0, message, args); + _errorOutput?.WriteLine($"error : {code}: {AutoFormat(message, args)}"); + _hasLoggedErrors = true; + } +} diff --git a/src/tasks/WasmAppBuilder/ManagedToNativeGenerator.cs b/src/tasks/WasmAppBuilder/ManagedToNativeGenerator.cs index b5bf1f70457ab..3acdcf06ba8a6 100644 --- a/src/tasks/WasmAppBuilder/ManagedToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/ManagedToNativeGenerator.cs @@ -10,6 +10,7 @@ using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using WasmAppBuilder; namespace Microsoft.WebAssembly.Build.Tasks; @@ -37,9 +38,6 @@ public class ManagedToNativeGenerator : Task private static readonly char[] s_charsToReplace = new[] { '.', '-', '+', '<', '>' }; - // Avoid sharing this cache with all the invocations of this task throughout the build - private readonly Dictionary _symbolNameFixups = new(); - public override bool Execute() { if (Assemblies!.Length == 0) @@ -56,7 +54,8 @@ public override bool Execute() try { - ExecuteInternal(); + var logAdapter = new LogAdapter(Log); + ExecuteInternal(logAdapter); return !Log.HasLoggedErrors; } catch (LogAsErrorException e) @@ -66,19 +65,20 @@ public override bool Execute() } } - private void ExecuteInternal() + private void ExecuteInternal(LogAdapter log) { + Dictionary _symbolNameFixups = new(); List managedAssemblies = FilterOutUnmanagedBinaries(Assemblies); if (ShouldRun(managedAssemblies)) { - var pinvoke = new PInvokeTableGenerator(FixupSymbolName, Log); - var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, Log); + var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log); + var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, log); var resolver = new PathAssemblyResolver(managedAssemblies); using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib"); foreach (string asmPath in managedAssemblies) { - Log.LogMessage(MessageImportance.Low, $"Loading {asmPath} to scan for pinvokes, and icalls"); + log.LogMessage(MessageImportance.Low, $"Loading {asmPath} to scan for pinvokes, and icalls"); Assembly asm = mlc.LoadFromAssemblyPath(asmPath); pinvoke.ScanAssembly(asm); icall.ScanAssembly(asm); @@ -88,7 +88,7 @@ private void ExecuteInternal() pinvoke.Generate(PInvokeModules, PInvokeOutputPath), icall.Generate(IcallOutputPath)); - var m2n = new InterpToNativeGenerator(Log); + var m2n = new InterpToNativeGenerator(log); m2n.Generate(cookies, InterpToNativeOutputPath); if (!string.IsNullOrEmpty(CacheFilePath)) @@ -102,6 +102,40 @@ private void ExecuteInternal() fileWritesList.Add(CacheFilePath); FileWrites = fileWritesList.ToArray(); + + string FixupSymbolName(string name) + { + if (_symbolNameFixups.TryGetValue(name, out string? fixedName)) + return fixedName; + + UTF8Encoding utf8 = new(); + byte[] bytes = utf8.GetBytes(name); + StringBuilder sb = new(); + + foreach (byte b in bytes) + { + if ((b >= (byte)'0' && b <= (byte)'9') || + (b >= (byte)'a' && b <= (byte)'z') || + (b >= (byte)'A' && b <= (byte)'Z') || + (b == (byte)'_')) + { + sb.Append((char)b); + } + else if (s_charsToReplace.Contains((char)b)) + { + sb.Append('_'); + } + else + { + sb.Append($"_{b:X}_"); + } + } + + fixedName = sb.ToString(); + _symbolNameFixups[name] = fixedName; + return fixedName; + } + } private bool ShouldRun(IList managedAssemblies) @@ -158,39 +192,6 @@ bool CheckShouldRunBecauseOfOutputFile(string? path, ref DateTime oldestDt) } } - public string FixupSymbolName(string name) - { - if (_symbolNameFixups.TryGetValue(name, out string? fixedName)) - return fixedName; - - UTF8Encoding utf8 = new(); - byte[] bytes = utf8.GetBytes(name); - StringBuilder sb = new(); - - foreach (byte b in bytes) - { - if ((b >= (byte)'0' && b <= (byte)'9') || - (b >= (byte)'a' && b <= (byte)'z') || - (b >= (byte)'A' && b <= (byte)'Z') || - (b == (byte)'_')) - { - sb.Append((char)b); - } - else if (s_charsToReplace.Contains((char)b)) - { - sb.Append('_'); - } - else - { - sb.Append($"_{b:X}_"); - } - } - - fixedName = sb.ToString(); - _symbolNameFixups[name] = fixedName; - return fixedName; - } - private List FilterOutUnmanagedBinaries(string[] assemblies) { List managedAssemblies = new(assemblies.Length); diff --git a/src/tasks/WasmAppBuilder/PInvokeCollector.cs b/src/tasks/WasmAppBuilder/PInvokeCollector.cs index b760899ea0c8c..2aa95be749a4b 100644 --- a/src/tasks/WasmAppBuilder/PInvokeCollector.cs +++ b/src/tasks/WasmAppBuilder/PInvokeCollector.cs @@ -9,6 +9,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.Build.Tasks; +using WasmAppBuilder; #pragma warning disable CA1067 #pragma warning disable CS0649 @@ -58,9 +59,9 @@ public int GetHashCode(PInvoke pinvoke) internal sealed class PInvokeCollector { private readonly Dictionary _assemblyDisableRuntimeMarshallingAttributeCache = new(); - private TaskLoggingHelper Log { get; init; } + private LogAdapter Log { get; init; } - public PInvokeCollector(TaskLoggingHelper log) + public PInvokeCollector(LogAdapter log) { Log = log; } @@ -72,13 +73,12 @@ public void CollectPInvokes(List pinvokes, List callba try { CollectPInvokesForMethod(method); - if (DoesMethodHaveCallbacks(method)) + if (DoesMethodHaveCallbacks(method, Log)) callbacks.Add(new PInvokeCallback(method)); } catch (Exception ex) when (ex is not LogAsErrorException) { - Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, - $"Could not get pinvoke, or callbacks for method '{type.FullName}::{method.Name}' because '{ex.Message}'"); + Log.Warning("WASM0001", $"Could not get pinvoke, or callbacks for method '{type.FullName}::{method.Name}' because '{ex}'"); } } @@ -88,7 +88,7 @@ public void CollectPInvokes(List pinvokes, List callba if (method != null) { - string? signature = SignatureMapper.MethodToSignature(method!); + string? signature = SignatureMapper.MethodToSignature(method!, Log); if (signature == null) throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); @@ -107,7 +107,7 @@ void CollectPInvokesForMethod(MethodInfo method) var entrypoint = (string)dllimport.NamedArguments.First(arg => arg.MemberName == "EntryPoint").TypedValue.Value!; pinvokes.Add(new PInvoke(entrypoint, module, method, wasmLinkage)); - string? signature = SignatureMapper.MethodToSignature(method); + string? signature = SignatureMapper.MethodToSignature(method, Log); if (signature == null) { throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); @@ -118,15 +118,14 @@ void CollectPInvokesForMethod(MethodInfo method) } } - bool DoesMethodHaveCallbacks(MethodInfo method) + bool DoesMethodHaveCallbacks(MethodInfo method, LogAdapter log) { if (!MethodHasCallbackAttributes(method)) return false; if (TryIsMethodGetParametersUnsupported(method, out string? reason)) { - Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, - $"Skipping callback '{method.DeclaringType!.FullName}::{method.Name}' because '{reason}'."); + Log.Warning("WASM0001", $"Skipping callback '{method.DeclaringType!.FullName}::{method.Name}' because '{reason}'."); return false; } @@ -136,12 +135,12 @@ bool DoesMethodHaveCallbacks(MethodInfo method) // No DisableRuntimeMarshalling attribute, so check if the params/ret-type are // blittable bool isVoid = method.ReturnType.FullName == "System.Void"; - if (!isVoid && !IsBlittable(method.ReturnType)) + if (!isVoid && !IsBlittable(method.ReturnType, log)) Error($"The return type '{method.ReturnType.FullName}' of pinvoke callback method '{method}' needs to be blittable."); foreach (var p in method.GetParameters()) { - if (!IsBlittable(p.ParameterType)) + if (!IsBlittable(p.ParameterType, log)) Error("Parameter types of pinvoke callback method '" + method + "' needs to be blittable."); } @@ -170,38 +169,11 @@ static bool MethodHasCallbackAttributes(MethodInfo method) } } - public static bool IsBlittable(Type type) - { - if (type.IsPrimitive || type.IsByRef || type.IsPointer || type.IsEnum) - return true; - else - return false; - } + public static bool IsBlittable(Type type, LogAdapter log) => PInvokeTableGenerator.IsBlittable(type, log); private static void Error(string msg) => throw new LogAsErrorException(msg); - private static bool HasAttribute(MemberInfo element, params string[] attributeNames) - { - foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(element)) - { - try - { - for (int i = 0; i < attributeNames.Length; ++i) - { - if (cattr.AttributeType.FullName == attributeNames [i] || - cattr.AttributeType.Name == attributeNames[i]) - { - return true; - } - } - } - catch - { - // Assembly not found, ignore - } - } - return false; - } + internal static bool HasAttribute(MemberInfo element, params string[] attributeNames) => PInvokeTableGenerator.HasAttribute(element, attributeNames); private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotNullWhen(true)] out string? reason) { diff --git a/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs b/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs index 684bd10127e03..cd8535463bc34 100644 --- a/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs +++ b/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs @@ -9,21 +9,23 @@ using System.Text; using System.Text.RegularExpressions; using System.Reflection; +using System.Runtime.InteropServices; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using WasmAppBuilder; internal sealed class PInvokeTableGenerator { private readonly Dictionary _assemblyDisableRuntimeMarshallingAttributeCache = new(); - private TaskLoggingHelper Log { get; set; } + private LogAdapter Log { get; set; } private readonly Func _fixupSymbolName; private readonly HashSet signatures = new(); private readonly List pinvokes = new(); private readonly List callbacks = new(); private readonly PInvokeCollector _pinvokeCollector; - public PInvokeTableGenerator(Func fixupSymbolName, TaskLoggingHelper log) + public PInvokeTableGenerator(Func fixupSymbolName, LogAdapter log) { Log = log; _fixupSymbolName = fixupSymbolName; @@ -101,7 +103,7 @@ private void EmitPInvokeTable(StreamWriter w, Dictionary modules string imports = string.Join(Environment.NewLine, candidates.Select( p => $" {p.Method} (in [{p.Method.DeclaringType?.Assembly.GetName().Name}] {p.Method.DeclaringType})")); - Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, $"Found a native function ({first.EntryPoint}) with varargs in {first.Module}." + + Log.Warning("WASM0001", $"Found a native function ({first.EntryPoint}) with varargs in {first.Module}." + " Calling such functions is not supported, and will fail at runtime." + $" Managed DllImports: {Environment.NewLine}{imports}"); @@ -189,9 +191,41 @@ private string CEntryPoint(PInvoke pinvoke) nameof(Single) => "float", nameof(Int64) => "int64_t", nameof(UInt64) => "uint64_t", - _ => "int" + nameof(Int32) => "int32_t", + nameof(UInt32) => "uint32_t", + nameof(Int16) => "int32_t", + nameof(UInt16) => "uint32_t", + nameof(Char) => "int32_t", + nameof(Boolean) => "int32_t", + nameof(SByte) => "int32_t", + nameof(Byte) => "uint32_t", + nameof(IntPtr) => "void *", + nameof(UIntPtr) => "void *", + _ => PickCTypeNameForUnknownType(t) }; + private static string PickCTypeNameForUnknownType(Type t) + { + // Pass objects by-reference (their address by-value) + if (!t.IsValueType) + return "void *"; + // Pass pointers and function pointers by-value + else if (t.IsPointer || IsFunctionPointer(t)) + return "void *"; + else if (t.IsPrimitive) + throw new NotImplementedException("No native type mapping for type " + t); + + // https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md#function-signatures + // Any struct or union that recursively (including through nested structs, unions, and arrays) + // contains just a single scalar value and is not specified to have greater than natural alignment. + // FIXME: Handle the scenario where there are fields of struct types that contain no members + var fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (fields.Length == 1) + return MapType(fields[0].FieldType); + else + return "void *"; + } + // FIXME: System.Reflection.MetadataLoadContext can't decode function pointer types // https://github.com/dotnet/runtime/issues/43791 private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotNullWhen(true)] out string? reason) @@ -217,6 +251,7 @@ private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotN private string? GenPInvokeDecl(PInvoke pinvoke) { var method = pinvoke.Method; + if (method.Name == "EnumCalendarInfo") { // FIXME: System.Reflection.MetadataLoadContext can't decode function pointer types @@ -228,8 +263,7 @@ private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotN { // Don't use method.ToString() or any of it's parameters, or return type // because at least one of those are unsupported, and will throw - Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, - $"Skipping pinvoke '{pinvoke.Method.DeclaringType!.FullName}::{pinvoke.Method.Name}' because '{reason}'."); + Log.Warning("WASM0001", $"Skipping pinvoke '{pinvoke.Method.DeclaringType!.FullName}::{pinvoke.Method.Name}' because '{reason}'."); pinvoke.Skip = true; return null; @@ -238,15 +272,14 @@ private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotN return $$""" {{(pinvoke.WasmLinkage ? $"__attribute__((import_module(\"{EscapeLiteral(pinvoke.Module)}\"),import_name(\"{EscapeLiteral(pinvoke.EntryPoint)}\")))" : "")}} - {{(pinvoke.WasmLinkage ? "extern " : "")}}{{MapType(method.ReturnType)}} {{CEntryPoint(pinvoke)}} ({{ - string.Join(", ", method.GetParameters().Select(p => MapType(p.ParameterType))) - }}); + {{(pinvoke.WasmLinkage ? "extern " : "")}}{{MapType(method.ReturnType)}} {{CEntryPoint(pinvoke)}} ({{string.Join(", ", method.GetParameters().Select(p => MapType(p.ParameterType)))}}); """; } private string CEntryPoint(PInvokeCallback export) { - if (export.EntryPoint is not null) { + if (export.EntryPoint is not null) + { return _fixupSymbolName(export.EntryPoint); } @@ -379,12 +412,99 @@ private bool HasAssemblyDisableRuntimeMarshallingAttribute(Assembly assembly) return value; } - private static bool IsBlittable(Type type) + private static readonly Dictionary _blittableCache = new(); + + public static bool IsFunctionPointer(Type type) + { + object? bIsFunctionPointer = type.GetType().GetProperty("IsFunctionPointer")?.GetValue(type); + return (bIsFunctionPointer is bool b) && b; + } + + public static bool IsBlittable(Type type, LogAdapter log) { - if (type.IsPrimitive || type.IsByRef || type.IsPointer || type.IsEnum) + // We maintain a cache of results in order to only produce log messages the first time + // we analyze a given type. Otherwise, each (successful) use of a user-defined type + // in a callback or pinvoke would generate duplicate messages. + lock (_blittableCache) + if (_blittableCache.TryGetValue(type, out bool blittable)) + return blittable; + + bool result = IsBlittableUncached(type, log); + lock (_blittableCache) + _blittableCache[type] = result; + return result; + + static bool IsBlittableUncached(Type type, LogAdapter log) + { + if (type.IsPrimitive || type.IsByRef || type.IsPointer || type.IsEnum) + return true; + + if (IsFunctionPointer(type)) + return true; + + // HACK: SkiaSharp has pinvokes that rely on this + if (HasAttribute(type, "System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute")) + return true; + + if (type.Name == "__NonBlittableTypeForAutomatedTests__") + return false; + + if (!type.IsValueType) + { + log.InfoHigh("WASM0060", "Type {0} is not blittable: Not a ValueType", type); + return false; + } + + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (!type.IsLayoutSequential && (fields.Length > 1)) + { + log.InfoHigh("WASM0061", "Type {0} is not blittable: LayoutKind is not Sequential", type); + return false; + } + + foreach (var ft in fields) + { + if (!IsBlittable(ft.FieldType, log)) + { + log.InfoHigh("WASM0062", "Type {0} is not blittable: Field {1} is not blittable", type, ft.Name); + return false; + } + // HACK: Skip literals since they're complicated + // Ideally we would block initonly fields too since the callee could mutate them, but + // we rely on being able to pass types like System.Guid which are readonly + if (ft.IsLiteral) + { + log.InfoHigh("WASM0063", "Type {0} is not blittable: Field {1} is literal", type, ft.Name); + return false; + } + } + return true; - else - return false; + } + } + + public static bool HasAttribute(MemberInfo element, params string[] attributeNames) + { + foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(element)) + { + try + { + for (int i = 0; i < attributeNames.Length; ++i) + { + if (cattr.AttributeType.FullName == attributeNames[i] || + cattr.AttributeType.Name == attributeNames[i]) + { + return true; + } + } + } + catch + { + // Assembly not found, ignore + } + } + return false; } private static void Error(string msg) => throw new LogAsErrorException(msg); diff --git a/src/tasks/WasmAppBuilder/SignatureMapper.cs b/src/tasks/WasmAppBuilder/SignatureMapper.cs index 75b51ddeb47e8..f3b7f17ad017b 100644 --- a/src/tasks/WasmAppBuilder/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/SignatureMapper.cs @@ -7,51 +7,74 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using WasmAppBuilder; internal static class SignatureMapper { - private static char? TypeToChar(Type t) + private static char? TypeToChar(Type t, LogAdapter log) { - char? c = t.Name switch - { - nameof(String) => 'I', - nameof(Boolean) => 'I', - nameof(Char) => 'I', - nameof(Byte) => 'I', - nameof(Int16) => 'I', - nameof(UInt16) => 'I', - nameof(Int32) => 'I', - nameof(UInt32) => 'I', - nameof(IntPtr) => 'I', - nameof(UIntPtr) => 'I', - nameof(Int64) => 'L', - nameof(UInt64) => 'L', - nameof(Single) => 'F', - nameof(Double) => 'D', - "Void" => 'V', - _ => null - }; + char? c = null; + if (t.Namespace == "System") { + c = t.Name switch + { + nameof(String) => 'I', + nameof(Boolean) => 'I', + nameof(Char) => 'I', + nameof(Byte) => 'I', + nameof(Int16) => 'I', + nameof(UInt16) => 'I', + nameof(Int32) => 'I', + nameof(UInt32) => 'I', + nameof(Int64) => 'L', + nameof(UInt64) => 'L', + nameof(Single) => 'F', + nameof(Double) => 'D', + // FIXME: These will need to be L for wasm64 + nameof(IntPtr) => 'I', + nameof(UIntPtr) => 'I', + "Void" => 'V', + _ => null + }; + } if (c == null) { + // FIXME: Most of these need to be L for wasm64 if (t.IsArray) c = 'I'; + else if (t.IsByRef) + c = 'I'; + else if (typeof(Delegate).IsAssignableFrom(t)) + // FIXME: Should we narrow this to only certain types of delegates? + c = 'I'; else if (t.IsClass) c = 'I'; else if (t.IsInterface) c = 'I'; else if (t.IsEnum) - c = TypeToChar(t.GetEnumUnderlyingType()); - else if (t.IsValueType) + c = TypeToChar(t.GetEnumUnderlyingType(), log); + else if (t.IsPointer) + c = 'I'; + else if (PInvokeTableGenerator.IsFunctionPointer(t)) c = 'I'; + else if (t.IsValueType) + { + var fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (fields.Length == 1) + return TypeToChar(fields[0].FieldType, log); + else if (PInvokeTableGenerator.IsBlittable(t, log)) + c = 'I'; + } + else + log.Warning("WASM0064", $"Unsupported parameter type '{t.Name}'"); } return c; } - public static string? MethodToSignature(MethodInfo method) + public static string? MethodToSignature(MethodInfo method, LogAdapter log) { - string? result = TypeToChar(method.ReturnType)?.ToString(); + string? result = TypeToChar(method.ReturnType, log)?.ToString(); if (result == null) { return null; @@ -59,7 +82,7 @@ internal static class SignatureMapper foreach (var parameter in method.GetParameters()) { - char? parameterChar = TypeToChar(parameter.ParameterType); + char? parameterChar = TypeToChar(parameter.ParameterType, log); if (parameterChar == null) { return null; diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index e8a52724b38a4..243a7aed31a5e 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -15,6 +15,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.NET.Sdk.WebAssembly; +using WasmAppBuilder; namespace Microsoft.WebAssembly.Build.Tasks; @@ -93,6 +94,7 @@ private GlobalizationMode GetGlobalizationMode() protected override bool ExecuteInternal() { var helper = new BootJsonBuilderHelper(Log); + var logAdapter = new LogAdapter(Log); if (!ValidateArguments()) return false; @@ -132,7 +134,7 @@ protected override bool ExecuteInternal() if (UseWebcil) { using TempFileName tmpWebcil = new(); - var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: assembly, outputPath: tmpWebcil.Path, logger: Log); + var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: assembly, outputPath: tmpWebcil.Path, logger: logAdapter); webcilWriter.ConvertToWebcil(); var finalWebcil = Path.Combine(runtimeAssetsPath, Path.ChangeExtension(Path.GetFileName(assembly), Utils.WebcilInWasmExtension)); if (Utils.CopyIfDifferent(tmpWebcil.Path, finalWebcil, useHash: true)) @@ -230,7 +232,7 @@ protected override bool ExecuteInternal() if (UseWebcil) { using TempFileName tmpWebcil = new(); - var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: args.fullPath, outputPath: tmpWebcil.Path, logger: Log); + var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: args.fullPath, outputPath: tmpWebcil.Path, logger: logAdapter); webcilWriter.ConvertToWebcil(); var finalWebcil = Path.Combine(cultureDirectory, Path.ChangeExtension(name, Utils.WebcilInWasmExtension)); if (Utils.CopyIfDifferent(tmpWebcil.Path, finalWebcil, useHash: true)) diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj index f45b031653fcb..32fad42f32b95 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj @@ -23,9 +23,13 @@ - - + + + + + diff --git a/src/tasks/WasmAppBuilder/WebcilConverter.cs b/src/tasks/WasmAppBuilder/WebcilConverter.cs index 51add150a953c..526aa62460bf6 100644 --- a/src/tasks/WasmAppBuilder/WebcilConverter.cs +++ b/src/tasks/WasmAppBuilder/WebcilConverter.cs @@ -8,6 +8,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using WasmAppBuilder; namespace Microsoft.WebAssembly.Build.Tasks; @@ -21,8 +22,8 @@ public class WebcilConverter private readonly NET.WebAssembly.Webcil.WebcilConverter _converter; - private TaskLoggingHelper Log { get; } - private WebcilConverter(NET.WebAssembly.Webcil.WebcilConverter converter, string inputPath, string outputPath, TaskLoggingHelper logger) + private LogAdapter Log { get; } + private WebcilConverter(NET.WebAssembly.Webcil.WebcilConverter converter, string inputPath, string outputPath, LogAdapter logger) { _converter = converter; _inputPath = inputPath; @@ -30,7 +31,7 @@ private WebcilConverter(NET.WebAssembly.Webcil.WebcilConverter converter, string Log = logger; } - public static WebcilConverter FromPortableExecutable(string inputPath, string outputPath, TaskLoggingHelper logger) + public static WebcilConverter FromPortableExecutable(string inputPath, string outputPath, LogAdapter logger) { var converter = NET.WebAssembly.Webcil.WebcilConverter.FromPortableExecutable(inputPath, outputPath); return new WebcilConverter(converter, inputPath, outputPath, logger);