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
{{ message }}
Repository navigation
[wasm] reverse P/Invoke thunk key drops enclosing type for nested [UnmanagedCallersOnly] methods #130739
The CoreCLR wasm reverse-P/Invoke thunk table is keyed by {MethodName}#{argCount}:{AssemblyName}:{Namespace}:{TypeName}. The key is produced on two sides that must agree:
Runtime lookup — src/coreclr/vm/wasm/helpers.cpp (GetHashCode → MethodTable::GetFullyQualifiedNameInfo → GetNameOfTypeDef), which reads the namespace straight from ECMA-335 metadata.
For a nested type the metadata namespace is empty, and the key uses only the simple nested type name (the enclosing type chain is dropped on both sides). So two genuinely-distinct nested [UnmanagedCallersOnly] callbacks that share the same simple type name, method name, argument count, and assembly — but differ only by enclosing type/namespace — collapse to the same key, e.g.:
The generator detects duplicate keys and fails the build loudly
(PInvokeTableGenerator.cs: "Two callbacks with the same Name and number of arguments … are not supported"), so this is a build-time error, never a silent mis-dispatch.
A recent fix (matching the generator's namespace to the runtime's metadata truth — empty namespace for nested types) made the nested-type key agree with the runtime lookup so nested [UnmanagedCallersOnly] callbacks resolve at all; before that they silently missed the lookup and asserted at precode_portable.cpp:35. See the CoreCLR-WASI library-test bring-up (#130129).
Why this is acceptable for now
This name-hash reverse-thunk table is a wasm-interpreter bring-up mechanism and is expected to be replaced once R2R can generate the reverse thunks directly. Until then, the build-time collision error is sufficient — a program hitting it is rare and gets a clear diagnostic rather than incorrect behavior.
Possible robust fix (if the generator persists)
Include the full enclosing-type chain in the key on both sides (generator + runtime GetHashCode) so nested types are fully disambiguated, and add a CoreCLR nested-type [UnmanagedCallersOnly] test (there is currently no nested-type coverage; UnmanagedCallbackNamespaced covers only top-level types on the Mono path).
Note
This issue was filed with the assistance of GitHub Copilot.
Per discussion, the robust long-term fix is not appending the enclosing-type name to the key — that is only a half-measure (still collides on overloads, still lossy). The reverse-thunk keying is already a documented FIXME (PInvokeCollector.cs:345-352), and the correct redesign is a metadata-token + assembly key (e.g. {MetadataToken:X8}:{AssemblyName}), which is fully unique and immune to all name-based collisions (nesting, overloads, generics).
Changing the key format touches the runtime side (src/coreclr/vm/wasm/helpers.cppGetHashCode) and invalidates every baked callhelpers-reverse.cpp, so it must regenerate + recommit the browser and wasi baked tables in lockstep — hence tracked separately from the minimal nested-namespace fix.
Note: this whole generator-based reverse-thunk table is expected to be superseded once R2R can emit the reverse thunks directly, so the token-based redesign is only warranted if the generator persists.
Note
This comment was added with the assistance of GitHub Copilot.
Design constraint for whoever picks this up: the reverse-P/Invoke thunk table must stay name-keyed, not token-keyed.
The generator (PInvokeCollector) records method.MetadataToken, but it is used only as an ordering tiebreaker in PInvokeCallbackComparer, never in the lookup key. Both sides key on names:
Metadata tokens are renumbered by the IL linker (illink), so a token-keyed table would break unless the generator ran after linking and could relink to the final token space. That is why the key is string/name based today.
Consequence: the fix for this collision is to extend the name-based key with the enclosing-type chain (e.g. include the full nested type path rather than just Type.Name), keeping the generator and helpers.cpp in lockstep. Switching to the metadata token to disambiguate nested types is a dead end for the trimmed/relinked case.
The residual limitation is currently exercised by a skipped [ActiveIssue] test (Wasm.Build.TestsUnmanagedCallersOnly_NestedConflict) added in #130740; removing the attribute once the key is extended will lock the fix.
Note
This comment was generated with the assistance of GitHub Copilot.
Correction / better prior art for my earlier comment: the metadata token is not a dead end for disambiguating nested collisions. The Mono path already solved this exact class of problem and the proven pattern is token-first with a name-only fallback.
See #107113 (fixing #107212, where one wasm_native_to_interp slot was reused for methods with different signatures). It changed wasm_dl_get_native_to_interp (src/mono/browser/runtime/pinvoke.c) to:
// comparison must match the one used in the PInvokeTableGenerator to ensure the same orderresult=bsearch(&needle, table, count, sizeof(UnmanagedExport), export_compare_key_and_token);
if (!result) {
// assembly may have been trimmed / modified, try to find by key onlyresult=bsearch(&needle, table, count, sizeof(UnmanagedExport), export_compare_key);
}
So the token is the secondary disambiguator (two nested Conflict types have distinct tokens), and the name-only fallback covers the case where trimming/relinking renumbered the token. That is why tokens alone are unsafe but tokens-with-name-fallback are robust.
The CoreCLR path is not yet set up this way:
The generator already carries the token (PInvokeCallback.Token) but uses it only as an ordering tiebreaker in PInvokeCallbackComparer; it is not emitted into g_ReverseThunks.
LookupThunk / GetHashCode (src/coreclr/vm/wasm/helpers.cpp) key purely on GetFullyQualifiedNameInfo (name + namespace), with no token and no fallback.
So the natural fix for this issue is to mirror Mono: emit the token per g_ReverseThunks entry and make LookupThunk match by (name-key, token) first, falling back to name-key only when the token miss is due to trimming. That disambiguates the nested collision without depending on token stability. (Extending the name key with the full enclosing-type chain is an alternative, but the token+fallback scheme is the shipped, battle-tested one.)
The skipped [ActiveIssue] test UnmanagedCallersOnly_NestedConflict in #130740 encodes the target behavior and can be un-skipped once this lands.
Note
This comment was generated with the assistance of GitHub Copilot.
changed the title [-][wasm] CoreCLR reverse P/Invoke thunk key drops enclosing type for nested [UnmanagedCallersOnly] methods[/-][+][wasm] reverse P/Invoke thunk key drops enclosing type for nested [UnmanagedCallersOnly] methods[/+]on Jul 15, 2026
So the natural fix for this issue is to mirror Mono: emit the token per g_ReverseThunks entry and make LookupThunk match by (name-key, token) first, falling back to name-key only when the token miss is due to trimming. That disambiguates the nested collision without depending on token stability. (Extending the name key with the full enclosing-type chain is an alternative, but the token+fallback scheme is the shipped, battle-tested one.)
I think including the full enclosing-type chain in Source is safer. It does not depend on metadata-token stability and only changes nested-type entries. The generator and runtime must construct this key consistently, and the generated helper EntryName should include the same chain to avoid symbol collisions.
The token approach would be a good if we could guarantee that generation and execution always use the same final managed metadata, after trimming or other rewriting. Otherwise, the name-only fallback is safe only while the name key is unique. Once duplicate keys are allowed, the fallback becomes ambiguous. As Blazor becomes more trimming-friendly, this will matter more unless the generator processes the post-trim assemblies. Trimming is only the most visible case; recompilation or other IL/metadata rewriting between generation and execution can also renumber tokens.
There are other known generator limitations, such as overloads with the same name and arity, and generic methods or types. We can further improve the generator to support these where possible, or keep them unsupported and reject them explicitly with clear diagnostics, as it rejects nested-type collisions today. Generics would first require broader UCO support.
Activity
dotnet-policy-service commentedon Jul 14, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
lewing commentedon Jul 14, 2026
Robust fix direction
Per discussion, the robust long-term fix is not appending the enclosing-type name to the key — that is only a half-measure (still collides on overloads, still lossy). The reverse-thunk keying is already a documented
FIXME(PInvokeCollector.cs:345-352), and the correct redesign is a metadata-token + assembly key (e.g.{MetadataToken:X8}:{AssemblyName}), which is fully unique and immune to all name-based collisions (nesting, overloads, generics).Changing the key format touches the runtime side (
src/coreclr/vm/wasm/helpers.cppGetHashCode) and invalidates every bakedcallhelpers-reverse.cpp, so it must regenerate + recommit the browser and wasi baked tables in lockstep — hence tracked separately from the minimal nested-namespace fix.Note: this whole generator-based reverse-thunk table is expected to be superseded once R2R can emit the reverse thunks directly, so the token-based redesign is only warranted if the generator persists.
Note
This comment was added with the assistance of GitHub Copilot.
[wasm] Add skipped nested UCO name-collision test for dotnet#130739
lewing commentedon Jul 15, 2026
Design constraint for whoever picks this up: the reverse-P/Invoke thunk table must stay name-keyed, not token-keyed.
The generator (
PInvokeCollector) recordsmethod.MetadataToken, but it is used only as an ordering tiebreaker inPInvokeCallbackComparer, never in the lookup key. Both sides key on names:Method#argCount:Assembly:Namespace:TypeMethodTable::GetFullyQualifiedNameInfo(src/coreclr/vm/wasm/helpers.cppGetHashCode)Metadata tokens are renumbered by the IL linker (illink), so a token-keyed table would break unless the generator ran after linking and could relink to the final token space. That is why the key is string/name based today.
Consequence: the fix for this collision is to extend the name-based key with the enclosing-type chain (e.g. include the full nested type path rather than just
Type.Name), keeping the generator andhelpers.cppin lockstep. Switching to the metadata token to disambiguate nested types is a dead end for the trimmed/relinked case.The residual limitation is currently exercised by a skipped
[ActiveIssue]test (Wasm.Build.TestsUnmanagedCallersOnly_NestedConflict) added in #130740; removing the attribute once the key is extended will lock the fix.Note
This comment was generated with the assistance of GitHub Copilot.
lewing commentedon Jul 15, 2026
Correction / better prior art for my earlier comment: the metadata token is not a dead end for disambiguating nested collisions. The Mono path already solved this exact class of problem and the proven pattern is token-first with a name-only fallback.
See #107113 (fixing #107212, where one
wasm_native_to_interpslot was reused for methods with different signatures). It changedwasm_dl_get_native_to_interp(src/mono/browser/runtime/pinvoke.c) to:So the token is the secondary disambiguator (two nested
Conflicttypes have distinct tokens), and the name-only fallback covers the case where trimming/relinking renumbered the token. That is why tokens alone are unsafe but tokens-with-name-fallback are robust.The CoreCLR path is not yet set up this way:
PInvokeCallback.Token) but uses it only as an ordering tiebreaker inPInvokeCallbackComparer; it is not emitted intog_ReverseThunks.LookupThunk/GetHashCode(src/coreclr/vm/wasm/helpers.cpp) key purely onGetFullyQualifiedNameInfo(name + namespace), with no token and no fallback.So the natural fix for this issue is to mirror Mono: emit the token per
g_ReverseThunksentry and makeLookupThunkmatch by (name-key, token) first, falling back to name-key only when the token miss is due to trimming. That disambiguates the nested collision without depending on token stability. (Extending the name key with the full enclosing-type chain is an alternative, but the token+fallback scheme is the shipped, battle-tested one.)The skipped
[ActiveIssue]testUnmanagedCallersOnly_NestedConflictin #130740 encodes the target behavior and can be un-skipped once this lands.Note
This comment was generated with the assistance of GitHub Copilot.
[wasi] Address PR review: correct stale 'prebuilt corerun' comments +…
[-][wasm] CoreCLR reverse P/Invoke thunk key drops enclosing type for nested [UnmanagedCallersOnly] methods[/-][+][wasm] reverse P/Invoke thunk key drops enclosing type for nested [UnmanagedCallersOnly] methods[/+][wasm] Fix reverse P/Invoke thunk key for nested [UnmanagedCallersOnl…
dotnet-policy-service commentedon Jul 15, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
See info in area-owners.md if you want to be subscribed.
radekdoulik commentedon Jul 16, 2026
I think including the full enclosing-type chain in Source is safer. It does not depend on metadata-token stability and only changes nested-type entries. The generator and runtime must construct this key consistently, and the generated helper EntryName should include the same chain to avoid symbol collisions.
The token approach would be a good if we could guarantee that generation and execution always use the same final managed metadata, after trimming or other rewriting. Otherwise, the name-only fallback is safe only while the name key is unique. Once duplicate keys are allowed, the fallback becomes ambiguous. As Blazor becomes more trimming-friendly, this will matter more unless the generator processes the post-trim assemblies. Trimming is only the most visible case; recompilation or other IL/metadata rewriting between generation and execution can also renumber tokens.
There are other known generator limitations, such as overloads with the same name and arity, and generic methods or types. We can further improve the generator to support these where possible, or keep them unsupported and reject them explicitly with clear diagnostics, as it rejects nested-type collisions today. Generics would first require broader UCO support.
[wasi] Correct CoreCLR-WASI smoke leg known-blocker note
[wasi] Address PR review: correct stale 'prebuilt corerun' comments +…
[wasi] Stand up CoreCLR-WASI library-test leg (per-app corerun relink) (