Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment thread
lewing marked this conversation as resolved.

[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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void>)&Namespaced.Outer.Nested.C)();
((delegate* unmanaged<void>)&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");
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<void>)&Conflicting.OuterA.Conflict.C)();
((delegate* unmanaged<void>)&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");
}
}
}
}
9 changes: 8 additions & 1 deletion src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Comment thread
lewing marked this conversation as resolved.
// 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;
Comment thread
lewing marked this conversation as resolved.
MethodName = method.Name!;
ReturnType = method.ReturnType!;
IsVoid = ReturnType.Name == "Void";
Expand Down
7 changes: 6 additions & 1 deletion src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down