You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue was drafted with the help of GitHub Copilot.
Summary
On CoreCLR-Wasm, user [UnmanagedCallersOnly] methods invoked via function pointer ((delegate* unmanaged<...>)&Method) or via a native callback (e.g. C code calling back into a UCO export) cannot be resolved at runtime. The user-generated reverse pinvoke table (reverse-pinvoke-table.cpp) is not linked into dotnet.native.wasm, so the runtime's reverse-thunk lookup fails and PortableEntryPoint::GetActualCode asserts HasNativeCode(), or wasm-ld fails with undefined symbol: <ManagedFunc> when a user .c file imports the export.
Two Wasm.Build.Tests are currently skipped on CoreCLR for this reason ([TestCategory("mono")] added in b4a93e8):
It is compiled once when CoreCLR is built and shipped inside libcoreclr_static.a (the runtime pack at runtimes/browser-wasm/native/libcoreclr_static.a). It also defines SystemInteropJS_* exports, so the archive object is always pulled in by the linker.
At startup, helpers.cpp:CreateReverseThunkHashTable seeds a hash table from g_ReverseThunks[]:
for (size_t i = 0; i < g_ReverseThunksCount; i++)
newTable->Add(&g_ReverseThunks[i]);
LookupThunk(MethodDesc*) hashes by (typeFullName + methodName) and returns (MethodDesc**, void* entryPoint).
src/mono/browser/build/BrowserWasmApp.CoreCLR.targets explicitly excludes reverse-pinvoke-table.cpp from compilation:
<!-- Note: reverse-pinvoke-table.cpp is NOT compiled separately because libcoreclr_static.a already contains callhelpers-reverse.cpp.o with the default reverse-thunk table. Compiling the app-generated version would cause duplicate symbol errors when the archive object is pulled in for other required symbols (e.g., SystemInteropJS_*). -->
So:
The user assembly's UCO methods are scanned and a reverse-pinvoke-table.cpp is generated.
The file is never compiled → Call_<sym> stubs and extern "C" exports are never produced.
For function-pointer invocation: managed code does (delegate* unmanaged<void>)&Method)() → interpreter InvokeCalliStub → PortableEntryPoint::GetActualCode(ftn) asserts HasNativeCode().
For native callback into UCO: user's local.c imports ManagedFunc → wasm-ld reports undefined symbol: ManagedFunc.
Additionally, the auto-enable logic in _CoreCLRSetWasmBuildNativeDefaults only sets WasmBuildNative=true when the project has NativeFileReference or NativeLibrary items. A project that uses UCO only via function pointers (no native files) never relinks at all.
How Mono solves this
Mono ships the reverse-thunk dispatch code as source, not as a precompiled object, and recompiles it during the app relink:
Runtime source ships in the pack
src/mono/browser/runtime/pinvoke.c is a small .c file that conditionally #includes either the user's generated header or an empty default:
#ifdefGEN_PINVOKE#include"pinvoke-table.h"// user-generated, written by ManagedToNativeGenerator#else#include"pinvoke-tables-default.h"// empty stubs, used when shipping the prebuilt wasm#endif
pinvoke.c is listed in _WasmRuntimePackSrcFile in src/mono/browser/build/BrowserWasmApp.targets. During an app relink, the build:
Generates pinvoke-table.h with wasm_native_to_interp_table[], wasm_native_to_interp_ftndescs[], and the per-export functions (decorated with __attribute__((export_name("...")))).
Compiles a freshpinvoke.c.o with -DGEN_PINVOKE=1 (see BrowserWasmApp.targets:212).
Links it into the new dotnet.native.wasm.
There is no precompiled pinvoke.c.o inside any static archive — so there is no duplicate-symbol conflict.
Lookup is by token + key, not by hash
wasm_dl_get_native_to_interp(token, key, extra_arg)bsearches the user's wasm_native_to_interp_table[], populates the global wasm_native_to_interp_ftndescs[] slot for that callback, and returns the per-callback wrapper. The wrapper internally calls mono_wasm_marshal_get_managed_wrapper on first use to resolve the interp entry function.
Generator side
src/tasks/WasmAppBuilder/mono/PInvokeTableGenerator.cs::EmitNativeToInterp writes the wasm_native_to_interp_table[] entries plus per-callback C functions. Exports are emitted as:
Rename the user table to g_AppReverseThunks / g_AppReverseThunksCount (avoids collisions with the framework table).
Emit a header with the table + a .cpp containing the Call_<sym> dispatch stubs and extern "C" wrappers. The extern "C" wrapper bug for non-void return types is already fixed in 70bf73d.
Update helpers.cpp:CreateReverseThunkHashTable to seed from both tables:
for (size_t i = 0; i < g_ReverseThunksCount; i++) newTable->Add(&g_ReverseThunks[i]);
for (size_t i = 0; i < g_AppReverseThunksCount; i++) newTable->Add(&g_AppReverseThunks[i]);
Update BrowserWasmApp.CoreCLR.targets to:
Drop the exclusion comment for reverse-pinvoke-table.cpp.
Add the new runtime source file to the relink compile list and pass -DGEN_PINVOKE=1.
Add reverse-pinvoke-table.cpp (containing Call_<sym> + extern "C" wrappers) to _WasmSourceFileToCompileGenerated.
Extend _CoreCLRSetWasmBuildNativeDefaults to also enable WasmBuildNative=true for OutputType=Exe browser-wasm CoreCLR projects (so apps with UCOs but no NativeFileReference still relink). Keep OutputType=Library untouched so project-reference libraries (e.g. LazyLibrary.csproj in WBT) are not affected.
Re-enable the skipped tests in PInvokeTableGeneratorTests (remove [TestCategory("mono")] on UnmanagedCallersOnly_Namespaced and UCOWithSpecialCharacters).
Alternative considered
Weak-symbol defaults in callhelpers-reverse.cpp for a renamed g_AppReverseThunks. Smaller change but less idiomatic in this repo and diverges from the Mono pattern that contributors already know.
Related
Commit 70bf73d — fixes the extern "C" wrapper return-type bug in the generator (prerequisite for non-void exports to link).
Commit b4a93e8 — temporarily skips the two affected WBT tests on CoreCLR.
Note
This issue was drafted with the help of GitHub Copilot.
Summary
On CoreCLR-Wasm, user
[UnmanagedCallersOnly]methods invoked via function pointer ((delegate* unmanaged<...>)&Method) or via a native callback (e.g. C code calling back into a UCO export) cannot be resolved at runtime. The user-generated reverse pinvoke table (reverse-pinvoke-table.cpp) is not linked intodotnet.native.wasm, so the runtime's reverse-thunk lookup fails andPortableEntryPoint::GetActualCodeassertsHasNativeCode(), orwasm-ldfails withundefined symbol: <ManagedFunc>when a user.cfile imports the export.Two Wasm.Build.Tests are currently skipped on CoreCLR for this reason (
[TestCategory("mono")]added in b4a93e8):PInvokeTableGeneratorTests.UnmanagedCallersOnly_NamespacedPInvokeTableGeneratorTests.UCOWithSpecialCharactersCurrent state on CoreCLR-Wasm
Runtime side
src/coreclr/vm/wasm/callhelpers-reverse.cppis a ~1300-line generated source containing the framework's reverse-thunk table:It is compiled once when CoreCLR is built and shipped inside
libcoreclr_static.a(the runtime pack atruntimes/browser-wasm/native/libcoreclr_static.a). It also definesSystemInteropJS_*exports, so the archive object is always pulled in by the linker.At startup,
helpers.cpp:CreateReverseThunkHashTableseeds a hash table fromg_ReverseThunks[]:LookupThunk(MethodDesc*)hashes by(typeFullName + methodName)and returns(MethodDesc**, void* entryPoint).Generator side
src/tasks/WasmAppBuilder/coreclr/PInvokeTableGenerator.csemitsreverse-pinvoke-table.cppcontaining:Call_<sym>interpreter dispatch stubs (one per user UCO method).extern "C" <retType> <EntryPoint>(...)C-callable wrappers (when[UnmanagedCallersOnly(EntryPoint = "...")]).Why the user file isn't linked today
src/mono/browser/build/BrowserWasmApp.CoreCLR.targetsexplicitly excludesreverse-pinvoke-table.cppfrom compilation:So:
reverse-pinvoke-table.cppis generated.Call_<sym>stubs andextern "C"exports are never produced.(delegate* unmanaged<void>)&Method)()→ interpreterInvokeCalliStub→PortableEntryPoint::GetActualCode(ftn)assertsHasNativeCode().local.cimportsManagedFunc→wasm-ldreportsundefined symbol: ManagedFunc.Additionally, the auto-enable logic in
_CoreCLRSetWasmBuildNativeDefaultsonly setsWasmBuildNative=truewhen the project hasNativeFileReferenceorNativeLibraryitems. A project that uses UCO only via function pointers (no native files) never relinks at all.How Mono solves this
Mono ships the reverse-thunk dispatch code as source, not as a precompiled object, and recompiles it during the app relink:
Runtime source ships in the pack
src/mono/browser/runtime/pinvoke.cis a small.cfile that conditionally#includes either the user's generated header or an empty default:pinvoke.cis listed in_WasmRuntimePackSrcFileinsrc/mono/browser/build/BrowserWasmApp.targets. During an app relink, the build:pinvoke-table.hwithwasm_native_to_interp_table[],wasm_native_to_interp_ftndescs[], and the per-export functions (decorated with__attribute__((export_name("...")))).pinvoke.c.owith-DGEN_PINVOKE=1(seeBrowserWasmApp.targets:212).dotnet.native.wasm.There is no precompiled
pinvoke.c.oinside any static archive — so there is no duplicate-symbol conflict.Lookup is by token + key, not by hash
wasm_dl_get_native_to_interp(token, key, extra_arg)bsearches the user'swasm_native_to_interp_table[], populates the globalwasm_native_to_interp_ftndescs[]slot for that callback, and returns the per-callback wrapper. The wrapper internally callsmono_wasm_marshal_get_managed_wrapperon first use to resolve the interp entry function.Generator side
src/tasks/WasmAppBuilder/mono/PInvokeTableGenerator.cs::EmitNativeToInterpwrites thewasm_native_to_interp_table[]entries plus per-callback C functions. Exports are emitted as:No separate
extern "C"wrapper is needed (Mono emits a single function that is both the table entry and the wasm export).Proposal: mirror the Mono approach on CoreCLR
Apply the same "ship source, recompile on relink" pattern:
Split
callhelpers-reverse.cppinto:callhelpers-reverse-builtin.cpp— keeps the frameworkg_ReverseThunkstable andSystemInteropJS_*exports. Stays inlibcoreclr_static.a..afiles) that conditionally includes the app-generated header:Update
PInvokeTableGenerator.csto:g_AppReverseThunks/g_AppReverseThunksCount(avoids collisions with the framework table)..cppcontaining theCall_<sym>dispatch stubs andextern "C"wrappers. Theextern "C"wrapper bug for non-voidreturn types is already fixed in 70bf73d.Update
helpers.cpp:CreateReverseThunkHashTableto seed from both tables:Update
BrowserWasmApp.CoreCLR.targetsto:reverse-pinvoke-table.cpp.-DGEN_PINVOKE=1.reverse-pinvoke-table.cpp(containingCall_<sym>+extern "C"wrappers) to_WasmSourceFileToCompileGenerated._CoreCLRSetWasmBuildNativeDefaultsto also enableWasmBuildNative=trueforOutputType=Exebrowser-wasm CoreCLR projects (so apps with UCOs but noNativeFileReferencestill relink). KeepOutputType=Libraryuntouched so project-reference libraries (e.g.LazyLibrary.csprojin WBT) are not affected.Re-enable the skipped tests in
PInvokeTableGeneratorTests(remove[TestCategory("mono")]onUnmanagedCallersOnly_NamespacedandUCOWithSpecialCharacters).Alternative considered
Weak-symbol defaults in
callhelpers-reverse.cppfor a renamedg_AppReverseThunks. Smaller change but less idiomatic in this repo and diverges from the Mono pattern that contributors already know.Related
extern "C"wrapper return-type bug in the generator (prerequisite for non-voidexports to link)./cc @maraf