diff --git a/eng/testing/scenarios/BuildWasiAppsJobsList.txt b/eng/testing/scenarios/BuildWasiAppsJobsList.txt index b68f7fe3d30ed..bdb9ecf6e5f07 100644 --- a/eng/testing/scenarios/BuildWasiAppsJobsList.txt +++ b/eng/testing/scenarios/BuildWasiAppsJobsList.txt @@ -3,3 +3,4 @@ Wasi.Build.Tests.ILStripTests Wasi.Build.Tests.SdkMissingTests Wasi.Build.Tests.RuntimeConfigTests Wasi.Build.Tests.WasiTemplateTests +Wasi.Build.Tests.PInvokeTableGeneratorTests diff --git a/src/mono/browser/runtime/runtime.c b/src/mono/browser/runtime/runtime.c index bbd645e21e821..7dbdc02c25999 100644 --- a/src/mono/browser/runtime/runtime.c +++ b/src/mono/browser/runtime/runtime.c @@ -76,6 +76,7 @@ int monoeg_g_setenv(const char *variable, const char *value, int overwrite); int32_t mini_parse_debug_option (const char *option); char *mono_method_get_full_name (MonoMethod *method); void mono_trace_init (void); +MonoMethod *mono_marshal_get_managed_wrapper (MonoMethod *method, MonoClass *delegate_klass, MonoGCHandle target_handle, MonoError *error); /* Not part of public headers */ #define MONO_ICALL_TABLE_CALLBACKS_VERSION 3 @@ -356,3 +357,25 @@ mono_wasm_assembly_find_method (MonoClass *klass, const char *name, int argument MONO_EXIT_GC_UNSAFE; return result; } + +/* + * mono_wasm_marshal_get_managed_wrapper: + * Creates a wrapper for a function pointer to a method marked with + * UnamangedCallersOnlyAttribute. + * This wrapper ensures that the interpreter initializes the pointers. + */ +void +mono_wasm_marshal_get_managed_wrapper (const char* assemblyName, const char* typeName, const char* methodName, int num_params) +{ + MonoError error; + mono_error_init (&error); + MonoAssembly* assembly = mono_wasm_assembly_load (assemblyName); + assert (assembly); + MonoClass* class = mono_wasm_assembly_find_class (assembly, "", typeName); + assert (class); + MonoMethod* method = mono_wasm_assembly_find_method (class, methodName, num_params); + assert (method); + MonoMethod *managedWrapper = mono_marshal_get_managed_wrapper (method, NULL, 0, &error); + assert (managedWrapper); + mono_compile_method (managedWrapper); +} diff --git a/src/mono/browser/runtime/runtime.h b/src/mono/browser/runtime/runtime.h index 8718fdb596720..0ad4d2abd451a 100644 --- a/src/mono/browser/runtime/runtime.h +++ b/src/mono/browser/runtime/runtime.h @@ -18,5 +18,7 @@ extern int mono_wasm_enable_gc; MonoDomain *mono_wasm_load_runtime_common (int debug_level, MonoLogCallback log_callback, const char *interp_opts); MonoAssembly *mono_wasm_assembly_load (const char *name); MonoClass *mono_wasm_assembly_find_class (MonoAssembly *assembly, const char *namespace, const char *name); +MonoMethod *mono_wasm_assembly_find_method (MonoClass *klass, const char *name, int arguments); +void mono_wasm_marshal_get_managed_wrapper (const char* assemblyName, const char* typeName, const char* methodName, int num_params); #endif diff --git a/src/mono/sample/wasi/native/Program.cs b/src/mono/sample/wasi/native/Program.cs index cb2fd0f36caf5..d8d480869a8de 100644 --- a/src/mono/sample/wasi/native/Program.cs +++ b/src/mono/sample/wasi/native/Program.cs @@ -20,11 +20,6 @@ public static int MyExport(int number) public unsafe static int Main(string[] args) { Console.WriteLine($"main: {args.Length}"); - // workaround to force the interpreter to initialize wasm_native_to_interp_ftndesc for MyExport - if (args.Length > 10000) { - ((IntPtr)(delegate* unmanaged)&MyExport).ToString(); - } - MyImport(); return 0; } diff --git a/src/mono/wasi/Wasi.Build.Tests/PInvokeTableGeneratorTests.cs b/src/mono/wasi/Wasi.Build.Tests/PInvokeTableGeneratorTests.cs new file mode 100644 index 0000000000000..6a47c0364658d --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/PInvokeTableGeneratorTests.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using Xunit; +using Xunit.Abstractions; +using Wasm.Build.Tests; + +#nullable enable + +namespace Wasi.Build.Tests; + +public class PInvokeTableGeneratorTests : BuildTestBase +{ + public PInvokeTableGeneratorTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + [Fact] + public void InteropSupportForUnmanagedEntryPointWithoutDelegate() + { + string config = "Release"; + string id = $"{config}_{GetRandomId()}"; + string projectFile = CreateWasmTemplateProject(id, "wasiconsole"); + string code = + """ + using System; + using System.Runtime.InteropServices; + public unsafe class Test + { + [UnmanagedCallersOnly(EntryPoint = "ManagedFunc")] + public static int MyExport(int number) + { + // called from MyImport aka UnmanagedFunc + Console.WriteLine($"MyExport({number}) -> 42"); + return 42; + } + + [DllImport("*", EntryPoint = "UnmanagedFunc")] + public static extern void MyImport(); // calls ManagedFunc aka MyExport + + public unsafe static int Main(string[] args) + { + Console.WriteLine($"main: {args.Length}"); + MyImport(); + return 0; + } + } + """; + string cCode = + """ + #include + + int ManagedFunc(int number); + + void UnmanagedFunc() + { + int ret = 0; + printf("UnmanagedFunc calling ManagedFunc\n"); + ret = ManagedFunc(123); + printf("ManagedFunc returned %d\n", ret); + } + """; + File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), code); + File.WriteAllText(Path.Combine(_projectDir!, "local.c"), cCode); + string extraProperties = @"false + true"; + AddItemsPropertiesToProject(projectFile, extraProperties: extraProperties, extraItems: @""); + string projectName = Path.GetFileNameWithoutExtension(projectFile); + var buildArgs = new BuildArgs(projectName, config, AOT: true, ProjectFileContents: id, ExtraBuildArgs: null); + buildArgs = ExpandBuildArgs(buildArgs); + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: false, + CreateProject: false, + Publish: true + )); + + CommandResult res = new RunCommand(s_buildEnv, _testOutput) + .WithWorkingDirectory(_projectDir!) + .ExecuteWithCapturedOutput($"run --no-silent --no-build -c {config}") + .EnsureSuccessful(); + Assert.Contains("MyExport(123) -> 42", res.Output); + } +} diff --git a/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs b/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs index e4aa070d88ea3..402325b1275ff 100644 --- a/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs +++ b/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs @@ -323,6 +323,14 @@ private void EmitNativeToInterp(StreamWriter w, List callbacks) // Only blittable parameter/return types are supposed. int cb_index = 0; + w.Write(@"#include + #include + #include + #include + #include + #include ""runtime.h"" + "); + // Arguments to interp entry functions in the runtime w.WriteLine($"InterpFtnDesc wasm_native_to_interp_ftndescs[{callbacks.Count}] = {{}};"); @@ -371,7 +379,16 @@ private void EmitNativeToInterp(StreamWriter w, List callbacks) if (!is_void) sb.Append($" {MapType(method.ReturnType)} res;\n"); - //sb.Append($" printf(\"{entry_name} called\\n\");\n"); + // In case when null force interpreter to initialize the pointers + sb.Append($" if (!(WasmInterpEntrySig_{cb_index})wasm_native_to_interp_ftndescs [{cb_index}].func) {{\n"); + var assemblyFullName = cb.Method.DeclaringType == null ? "" : cb.Method.DeclaringType.Assembly.FullName; + var assemblyName = assemblyFullName != null && assemblyFullName.Split(',').Length > 0 ? assemblyFullName.Split(',')[0].Trim() : ""; + var typeName = cb.Method.DeclaringType == null || cb.Method.DeclaringType.FullName == null ? "" : cb.Method.DeclaringType.FullName; + var methodName = cb.Method.Name; + int numParams = method.GetParameters().Length; + sb.Append($" mono_wasm_marshal_get_managed_wrapper (\"{assemblyName}\", \"{typeName}\", \"{methodName}\", {numParams});\n"); + sb.Append($" }}\n"); + sb.Append($" ((WasmInterpEntrySig_{cb_index})wasm_native_to_interp_ftndescs [{cb_index}].func) ("); if (!is_void) {