[wasm][interpreter] Defer managed calli cookie resolution to execution time#125455
[wasm][interpreter] Defer managed calli cookie resolution to execution time#125455radekdoulik wants to merge 7 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @BrzVlad, @janvorli, @kg |
|
To consider: with the lazy approach, is the cache check overhead in execution phase ok? Is there better way how to cache the cookie? Also note that the unmanaged path is currently not deferring the cookie resolution to execution phase to avoid the cache check overhead. |
There was a problem hiding this comment.
Pull request overview
This PR changes how the interpreter handles calli signature cookies on portable entry point platforms (notably WASM): instead of requiring the compiler to resolve the cookie up-front, it defers cookie resolution to execution time (and caches it), avoiding compile-time asserts and eliminating the need for the WASM generator’s hard-coded “missing cookie” pre-generation list.
Changes:
- Interpreter compiler now encodes managed
callisites with a signature-token + runtime cookie cache slot (via a newCalliFlags::DeferredCookie). - Interpreter executor (
interpexec.cpp) routes portable-entry-point targets that have aMethodDescthroughCALL_INTERP_METHOD, and lazily resolves/caches cookies for portable entry points without aMethodDesc. - WASM build/gen cleanup removes the
missingCookieslist and removes the corresponding pre-generated interp-to-managed thunks/stubs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs | Removes the temporary missingCookies signature list from thunk/stub generation inputs. |
| src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp | Removes previously pre-generated signature thunk helpers and their entries from g_wasmThunks. |
| src/coreclr/vm/precode_portable.hpp | Adds PortableEntryPoint::TryGetMethodDesc API for non-asserting lookup. |
| src/coreclr/vm/precode_portable.cpp | Implements TryGetMethodDesc (returns nullptr rather than asserting). |
| src/coreclr/vm/interpexec.cpp | Adds runtime cookie resolution/caching path for deferred managed calli signatures on portable entry points. |
| src/coreclr/interpreter/inc/interpretershared.h | Adds CalliFlags::DeferredCookie to signal runtime resolution. |
| src/coreclr/interpreter/compiler.h | Extends EmitCalli to accept a deferredCookie parameter (defaulted). |
| src/coreclr/interpreter/compiler.cpp | Emits deferred-cookie data items for managed calli on portable entry points; keeps eager cookies for unmanaged calli. |
src/coreclr/interpreter/compiler.cpp
Outdated
| #ifdef FEATURE_PORTABLE_ENTRYPOINTS | ||
| // On platforms with portable entrypoints, managed calli targets with a MethodDesc | ||
| // are dispatched via CALL_INTERP_METHOD at execution time, which derives the cookie | ||
| // from the MethodDesc. For targets without a MethodDesc (JIT helper PEs), the cookie |
There was a problem hiding this comment.
How many different JIT helper PEs that get called via CALLI are there? Would it be easier to just turn them into FCalls with MethodDescs to avoid this special casing?
There was a problem hiding this comment.
I am not sure how many there are in libraries. The case I was hitting is through ActivatorCache::CreateUninitializedObject where the _pfnAllocator function pointer is set to newobj allocator.
I asked copilot to find more places like this and it found 2 more places, in BoxCache and CreateUninitializedCache - both have calli to newobj allocator as well.
I think user code cannot have function pointer to something without method desc, so it should be hit just by code in our libraries.
So if I understand your idea, it would mean turning the newobj allocators to FCalls and add assert in that path, so that we find it out in case there are more scenarios or when we create new such scenario in libraries.
There was a problem hiding this comment.
Yes, that's the idea. Since it is all about newobj allocators, you may be able to use one dummy FCall for all of them. E.g. replace portableEntryPoint->Init((void*)pfnHelper) in getHelperFtnStatic with:
if (ftnNum >= CORINFO_HELP_NEWFAST && ftnNum <= CORINFO_HELP_NEWSFAST_ALIGN8_FINALIZE)
{
// CoreLib calls newobj helpers via calli. Give these helper a MethodDesc to allow interpreter to find the method signature.
portableEntryPoint->Init((void*)pfnHelper, CoreLibBinder::GetMethod(METHOD__RUNTIME_HELPERS__NEWOBJ_DUMMY);
}
else
{
portableEntryPoint->Init((void*)pfnHelper);
}
On portable entry point platforms, calli instructions targeting managed code previously required the compiler to compute a signature cookie at compile time via GetCookieForInterpreterCalliSig. This caused assertion failures for delegate shuffle thunks and required
a hard-coded missingCookies table in the WASM build generator to pre-generate interp-to-native stubs for every possible managed calli signature.
Implements #121222
Changes
Compiler (compiler.cpp, compiler.h, interpretershared.h):
resolved cookie.
Executor (interpexec.cpp):
Portable entry points (precode_portable.cpp, precode_portable.hpp):
Generator cleanup (ManagedToNativeGenerator.cs, callhelpers-interp-to-managed.cpp):