diff --git a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs index 50b273b53cc963..3be221bc216195 100644 --- a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs @@ -177,6 +177,57 @@ public async Task UnmanagedCallersOnly_Namespaced(Configuration config, bool aot } } + [Theory] + [BuildAndRun()] + public async Task UnmanagedCallersOnly_Nested(Configuration config, bool aot) + { + // Regression coverage for the wasm reverse-P/Invoke (native-to-interp) thunk key of a + // nested [UnmanagedCallersOnly] type. Reflection reports the enclosing namespace for a + // nested type while the runtime reads the (empty) metadata namespace; if PInvokeCollector + // emits the reflection namespace the key never matches, so the lookup returns null - + // CoreCLR asserts on the first cold ldftn and Mono traps as "null function". Executed on + // browser-wasm in CI for both the Mono and CoreCLR generators (the wasi leg is build-only). + ProjectInfo info = CopyTestAsset(config, aot, TestAsset.WasmBasicTestApp, "cb_nested"); + string programRelativePath = Path.Combine("Common", "Program.cs"); + ReplaceFile(programRelativePath, Path.Combine(BuildEnvironment.TestAssetsPath, "EntryPoints", "PInvoke", "UnmanagedCallbackNested.cs")); + + string output = PublishForVariadicFunctionTests(info, config, aot); + Assert.DoesNotMatch(".*(warning|error).*>[A-Z0-9]+__Foo", output); + + RunResult result = await RunForPublishWithWebServer(new BrowserRunOptions( + config, + TestScenario: "DotnetRun", + ExpectedExitCode: 42 + )); + Assert.Contains("Namespaced.Outer.Nested.C", result.TestOutput); + Assert.Contains("Namespaced.Outer.Nested.Deeper.D", result.TestOutput); + } + + [Theory] + [BuildAndRun()] + [ActiveIssue("https://github.com/dotnet/runtime/issues/130739")] + public async Task UnmanagedCallersOnly_NestedConflict(Configuration config, bool aot) + { + // The reverse-P/Invoke thunk key drops the enclosing-type chain, keying only on the + // simple type name plus the (empty) nested namespace. Two nested types that share a + // simple name under different enclosing types therefore collide and currently fail the + // build. This encodes the desired behavior (both callbacks resolve and run) and is + // skipped until #130739 removes the limitation. + ProjectInfo info = CopyTestAsset(config, aot, TestAsset.WasmBasicTestApp, "cb_nested_conflict"); + string programRelativePath = Path.Combine("Common", "Program.cs"); + ReplaceFile(programRelativePath, Path.Combine(BuildEnvironment.TestAssetsPath, "EntryPoints", "PInvoke", "UnmanagedCallbackNestedConflict.cs")); + + PublishForVariadicFunctionTests(info, config, aot); + + RunResult result = await RunForPublishWithWebServer(new BrowserRunOptions( + config, + TestScenario: "DotnetRun", + ExpectedExitCode: 42 + )); + Assert.Contains("Conflicting.OuterA.Conflict.C", result.TestOutput); + Assert.Contains("Conflicting.OuterB.Conflict.C", result.TestOutput); + } + [Theory] [BuildAndRun()] // The test fetches WasmAppBuilder.dll from the Microsoft.NET.Runtime.WebAssembly.Sdk diff --git a/src/mono/wasm/testassets/EntryPoints/PInvoke/UnmanagedCallbackNested.cs b/src/mono/wasm/testassets/EntryPoints/PInvoke/UnmanagedCallbackNested.cs new file mode 100644 index 00000000000000..ef4c1dc1995570 --- /dev/null +++ b/src/mono/wasm/testassets/EntryPoints/PInvoke/UnmanagedCallbackNested.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.InteropServices; + +public class Test +{ + public unsafe static int Main() + { + // Cold ldftn of a nested [UnmanagedCallersOnly] callback. The reverse-thunk key + // must use the metadata namespace (empty for nested types) to match the runtime, + // otherwise the lookup misses and the interpreter asserts at method-compile time. + ((delegate* unmanaged)&Namespaced.Outer.Nested.C)(); + ((delegate* unmanaged)&Namespaced.Outer.Nested.Deeper.D)(); + return 42; + } +} + +namespace Namespaced +{ + public class Outer + { + public class Nested + { + [UnmanagedCallersOnly] + public static void C() + { + Console.WriteLine("TestOutput -> Namespaced.Outer.Nested.C"); + } + + public class Deeper + { + [UnmanagedCallersOnly] + public static void D() + { + Console.WriteLine("TestOutput -> Namespaced.Outer.Nested.Deeper.D"); + } + } + } + } +} diff --git a/src/mono/wasm/testassets/EntryPoints/PInvoke/UnmanagedCallbackNestedConflict.cs b/src/mono/wasm/testassets/EntryPoints/PInvoke/UnmanagedCallbackNestedConflict.cs new file mode 100644 index 00000000000000..64b469e4c9edf7 --- /dev/null +++ b/src/mono/wasm/testassets/EntryPoints/PInvoke/UnmanagedCallbackNestedConflict.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.InteropServices; + +public class Test +{ + public unsafe static int Main() + { + // Two nested types share the simple name "Conflict" under different enclosing types. + // The reverse-thunk key drops the enclosing-type chain (Method#argCount:Assembly::Type), + // so both callbacks currently map to the same key and the build fails (#130739). + ((delegate* unmanaged)&Conflicting.OuterA.Conflict.C)(); + ((delegate* unmanaged)&Conflicting.OuterB.Conflict.C)(); + return 42; + } +} + +namespace Conflicting +{ + public class OuterA + { + public class Conflict + { + [UnmanagedCallersOnly] + public static void C() + { + Console.WriteLine("TestOutput -> Conflicting.OuterA.Conflict.C"); + } + } + } + + public class OuterB + { + public class Conflict + { + [UnmanagedCallersOnly] + public static void C() + { + Console.WriteLine("TestOutput -> Conflicting.OuterB.Conflict.C"); + } + } + } +} diff --git a/src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs b/src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs index 29ccf6c864ec78..150a2e50361454 100644 --- a/src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs +++ b/src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs @@ -336,7 +336,14 @@ public PInvokeCallback(MethodInfo method) TypeFullName = t.FullName!; AssemblyName = t.Module!.Assembly!.GetName()!.Name!; AssemblyFQName = t.Module!.Assembly!.GetName()!.FullName!; - Namespace = t.Namespace; + // Nested types: the runtime reverse-thunk key (vm/wasm/helpers.cpp GetHashCode -> + // GetFullyQualifiedNameInfo) reports an empty namespace for nested types, so match that + // here or the emitted g_ReverseThunks key won't be found at lookup time (#130129). + // This key drops the enclosing-type chain, so nested types with the same simple name in + // different namespaces collide; the duplicate-key check in PInvokeTableGenerator + // (EmitNativeToInterp) turns that into a build error. + // Tracked by https://github.com/dotnet/runtime/issues/130739. + Namespace = t.IsNested ? string.Empty : t.Namespace; MethodName = method.Name!; ReturnType = method.ReturnType!; IsVoid = ReturnType.Name == "Void"; diff --git a/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs b/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs index 1116f67cb56c66..1c774e60441a43 100644 --- a/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs +++ b/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs @@ -226,7 +226,12 @@ public PInvokeCallback(MethodInfo method) Method = method; TypeName = method.DeclaringType!.Name!; AssemblyName = method.DeclaringType!.Module!.Assembly!.GetName()!.Name!; - Namespace = method.DeclaringType!.Namespace; + // Nested types: the runtime native-to-interp key (browser/runtime/runtime.c get_native_to_interp + // via mono_class_get_namespace) reports an empty namespace for nested types, so match that here + // or the emitted wasm_native_to_interp_table key won't be found at lookup time. The token+name + // fallback in wasm_dl_get_native_to_interp cannot recover this because bsearch compares the key + // first, so a key mismatch defeats both lookups. + Namespace = method.DeclaringType!.IsNested ? string.Empty : method.DeclaringType!.Namespace; MethodName = method.Name!; ReturnType = method.ReturnType!; IsVoid = ReturnType.Name == "Void";