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);