Upstream WASM native AOT runtime from feature/NativeAOT-LLVM#126633
Upstream WASM native AOT runtime from feature/NativeAOT-LLVM#126633MichalStrehovsky wants to merge 3 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib |
There was a problem hiding this comment.
Pull request overview
This PR brings in a WebAssembly-focused NativeAOT runtime slice (from feature/NativeAOT-LLVM) and wires it into the NativeAOT build, including WASM-specific PAL/shadow-stack handling and selectable exception-handling models.
Changes:
- Add a WASM-specific NativeAOT runtime implementation (alloc fast paths, P/Invoke transitions, stack trace support, interface dispatch, write barriers, single-threaded finalization).
- Adjust NativeAOT/Runtime portable + Unix PAL/build logic to support WASM/WASI constraints (no
mmap, nodlopen, different stack bounds, etc.). - Introduce multiple exception handling model libraries for WASM and update build integration to pick at link time.
Reviewed changes
Copilot reviewed 46 out of 46 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/nativeaot/Runtime/wasm/WriteBarriers.cpp | WASM write-barrier FCalls (including no-shadow-stack variants). |
| src/coreclr/nativeaot/Runtime/wasm/wasm.h | WASM runtime helper declarations (incl. sparse unwind frame + NO_SS marker). |
| src/coreclr/nativeaot/Runtime/wasm/wasi.h | WASI-specific shims (e.g., getrlimit/rlimit constants). |
| src/coreclr/nativeaot/Runtime/wasm/StubDispatch.cpp | WASM-specific cached interface dispatch resolver. |
| src/coreclr/nativeaot/Runtime/wasm/StackTraceIpCanary.cpp | Browser stack-trace canary accessor (workaround for toolchain issue). |
| src/coreclr/nativeaot/Runtime/wasm/StackTrace.cpp | Browser stack trace interop + RhFindMethodStartAddress override for WASM. |
| src/coreclr/nativeaot/Runtime/wasm/PInvoke.cpp | WASM shadow-stack aware P/Invoke + reverse-P/Invoke transitions. |
| src/coreclr/nativeaot/Runtime/wasm/PalWasm.h | WASM PAL surface for stack bounds (single-threaded). |
| src/coreclr/nativeaot/Runtime/wasm/PalWasm.cpp | WASM PAL impl for stack bounds, virtual alloc/protect stubs, WASI TLS dtor workaround. |
| src/coreclr/nativeaot/Runtime/wasm/PalCreateDump.cpp | WASM stubbed crash dump support. |
| src/coreclr/nativeaot/Runtime/wasm/GcStress.cpp | WASM GC stress helpers and object validation helper. |
| src/coreclr/nativeaot/Runtime/wasm/FinalizerHelpers.SingleThreaded.cpp | Single-threaded finalization path for WASM. |
| src/coreclr/nativeaot/Runtime/wasm/ExceptionHandling/ExceptionHandling.Wasm.cpp | Native wasm-exceptions EH model implementation. |
| src/coreclr/nativeaot/Runtime/wasm/ExceptionHandling/ExceptionHandling.Emulated.cpp | Emulated EH model implementation for WASM. |
| src/coreclr/nativeaot/Runtime/wasm/ExceptionHandling/ExceptionHandling.Cpp.cpp | C++ exceptions EH model implementation. |
| src/coreclr/nativeaot/Runtime/wasm/ExceptionHandling/ExceptionHandling.cpp | Shared EH helpers (sparse unwind frame TLS + helpers). |
| src/coreclr/nativeaot/Runtime/wasm/ExceptionHandling/CMakeLists.txt | Builds/install static libs for the three EH models. |
| src/coreclr/nativeaot/Runtime/wasm/AsmOffsetsCpu.h | WASM asm-offset definitions for EH/stack iterator structures. |
| src/coreclr/nativeaot/Runtime/wasm/AllocFast.cpp | WASM allocation fast paths using shadow stack and optional finalization. |
| src/coreclr/nativeaot/Runtime/unix/UnixSignals.h | Gate signal handler APIs for WASI builds. |
| src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp | WASM/WASI conditionals for PAL features (threads/dlopen/mmap/cache flush/stack bounds). |
| src/coreclr/nativeaot/Runtime/unix/PalCreateDump.h | WASI-friendly siginfo_t forward-declare workaround. |
| src/coreclr/nativeaot/Runtime/unix/PalCreateDump.cpp | Host-WASM gating for dump creation implementation. |
| src/coreclr/nativeaot/Runtime/unix/NativeContext.h | Host-WASM gating and placeholder branch for WASM contexts. |
| src/coreclr/nativeaot/Runtime/unix/configure.cmake | Skip pthread feature probes for WASI targets. |
| src/coreclr/nativeaot/Runtime/unix/cgroupcpu.cpp | Include WASI rlimit shim when targeting WASI. |
| src/coreclr/nativeaot/Runtime/thread.inl | Host-WASM conditionals around hijack/thread caching in transition helpers. |
| src/coreclr/nativeaot/Runtime/thread.h | Add shadow stack state and WASM-specific scanning/transition helpers. |
| src/coreclr/nativeaot/Runtime/thread.cpp | WASM shadow stack allocation + root scanning changes; adjust init callback linkage and transition helpers. |
| src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp | Exclude stack-walk implementation sections from HOST_WASM builds. |
| src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp | Avoid duplicate RhFindMethodStartAddress for WASM. |
| src/coreclr/nativeaot/Runtime/RhConfigValues.h | Default gcConservative=1 on HOST_WASM. |
| src/coreclr/nativeaot/Runtime/regdisplay.h | Adjust WASM REGDISPLAY accessor to use stored IP. |
| src/coreclr/nativeaot/Runtime/Portable/CMakeLists.txt | Portable runtime build tweaks (asm offsets generation + standalone GC libs). |
| src/coreclr/nativeaot/Runtime/portable.cpp | Exclude portable allocator fast-path implementation for HOST_WASM; adjust helpers. |
| src/coreclr/nativeaot/Runtime/inc/rhbinder.h | Change portable PInvokeTransitionFrame layout under HOST_WASM. |
| src/coreclr/nativeaot/Runtime/inc/CommonTypes.h | Provide Win32-ish typedefs for TARGET_WASM builds. |
| src/coreclr/nativeaot/Runtime/GCHelpers.cpp | Ensure WASM doesn’t touch m_RIP path; run single-thread finalizers after collection. |
| src/coreclr/nativeaot/Runtime/eventpipe/CMakeLists.txt | EMSDK python selection workaround for manifest/codegen. |
| src/coreclr/nativeaot/Runtime/CommonMacros.h | Add shadow-stack parameter injection to FCALL/FCIMPL signatures on HOST_WASM. |
| src/coreclr/nativeaot/Runtime/CMakeLists.txt | Wire WASM sources and override unix dump impl; add EH model subdir; add browser-only canary object. |
| src/coreclr/nativeaot/CMakeLists.txt | WASM/WASI build option adjustments and managed-thread feature define. |
| src/coreclr/nativeaot/Bootstrap/main.cpp | WASI _initialize ctor calling + init callback plumbing; skip OS module registration for HOST_WASM. |
| src/coreclr/nativeaot/Bootstrap/base/CMakeLists.txt | Build stdc++compat for WASI host builds too. |
| src/coreclr/gc/env/gcenv.base.h | Adjust placement of HOST_BROWSER YieldProcessor/MemoryBarrier definitions. |
| src/coreclr/CMakeLists.txt | Relax NativeAOT build gating (i386 only blocked on non-Windows). |
Comments suppressed due to low confidence (2)
src/coreclr/nativeaot/Runtime/regdisplay.h:303
- In the TARGET_WASM REGDISPLAY,
SetIP/SetSPare no-ops, which makes the type internally inconsistent (fields exist but setters don’t update them). If these setters are intentionally unused on WASM, consider removing the fields or at least assigning toIP/SPto keep invariants sane.
inline void SetIP(PCODE IP) { }
inline void SetSP(uintptr_t SP) { }
};
src/coreclr/nativeaot/Runtime/unix/NativeContext.h:12
NativeContext.his wrapped in#ifndef HOST_WASM, which means the later#elif defined(HOST_WASM)branch insideNATIVE_CONTEXTis dead/unreachable. Either remove the outer#ifndef HOST_WASMor restructure the guards so the WASM-specific branch can actually be compiled when needed.
#ifndef HOST_WASM
#if HAVE_UCONTEXT_H
#include <ucontext.h>
#else
#include <signal.h>
#endif
|
|
||
| // CustomNativeMain programs are built using the same libbootstrapperdll as NATIVEAOT_DLL but wasi-libc will not provide an _initialize implementation, | ||
| // so create a dummy one here and make it weak to allow wasi-libc to provide the real implementation for WASI reactor components. | ||
| __attribute__((weak)) void _initialize() |
There was a problem hiding this comment.
On TARGET_WASI, _initialize is declared with C linkage but the weak fallback definition lacks extern "C", which will cause a linkage mismatch/ODR error (and the host won’t find the expected _initialize symbol). Define the weak fallback with extern "C" (and keep the __attribute__((weak))) so the declaration and definition have identical linkage.
| __attribute__((weak)) void _initialize() | |
| extern "C" __attribute__((weak)) void _initialize() |
| // Recieve pointers as JS-native types to avoid BigInt overheads on 64 bit | ||
| // and the need to worry about normalizing large (> int.MaxValue) values. | ||
| using JSPointerType = double; |
There was a problem hiding this comment.
Typo in comment: “Recieve” should be “Receive”.
| uintptr_t SP; | ||
| PCODE IP; | ||
|
|
||
| inline PCODE GetIP() { return NULL; } | ||
| inline PCODE GetIP() { return IP; } | ||
| inline uintptr_t GetSP() { return 0; } | ||
| inline uintptr_t GetFP() { return 0; } | ||
|
|
There was a problem hiding this comment.
In the TARGET_WASM REGDISPLAY, GetSP() always returns 0 even though the struct stores SP. This makes the accessor inconsistent with the stored state and can break any stack-walk/EH logic that relies on GetSP() for WASM.
| static bool s_finalizing = false; | ||
|
|
||
| // Recursive wait on finalization is a no-op. | ||
| if (g_FinalizationRequestPending && !g_FinalizationInProgress) | ||
| { |
There was a problem hiding this comment.
Unused local static s_finalizing is declared but never read or written. This adds confusion (and may trigger -Wunused-variable in some builds). Remove it or wire it into the reentrancy logic (e.g., use it instead of/alongside g_FinalizationInProgress).
| // TODO-LLVM-Cleanup: replace with with PLATFORM_THREAD_LOCAL after merge. | ||
| extern __thread SparseVirtualUnwindFrame* t_pLastSparseVirtualUnwindFrame; | ||
|
|
There was a problem hiding this comment.
Typo in comment: duplicated word “with” in “replace with with PLATFORM_THREAD_LOCAL”.
| // | ||
| // TODO-LLVM-Cleanup: replace with with PLATFORM_THREAD_LOCAL after merge. | ||
| __thread SparseVirtualUnwindFrame* t_pLastSparseVirtualUnwindFrame = nullptr; | ||
|
|
There was a problem hiding this comment.
Typo in comment: duplicated word “with” in “replace with with PLATFORM_THREAD_LOCAL”.
| @@ -0,0 +1,12 @@ | |||
| # Build two static libraries for the two exception handling models. | |||
There was a problem hiding this comment.
nit: There are 3 models below not two.
But we we really want to support anything but modern wasm exceptions going forward ?
cc @kg
There was a problem hiding this comment.
CppExceptionHandling can be dropped. The emulated model should be kept, it is important for compatibility in the broad sense.
There was a problem hiding this comment.
Is there a comment or md file somewhere that explains what the three modes are? The existence of multiple EH modes selected at link time has added a lot of complexity and pain to mono wasm over time (IMO) so it would be ideal if we could converge on one model for both naot and r2r
There was a problem hiding this comment.
There is an EH document we could also copy over, https://github.com/dotnet/runtimelab/blob/feature/NativeAOT-LLVM/docs/design/coreclr/botr/nativeaot-wasm-exception-handling.md, however, it doesn't really address the "EH modes", only the virtual unwind.
As you recall, in .NET EH, we have two phases: 1st - search for handlers using virtual unwind, 2nd - execute second-pass handlers (finallys and faults). For the second pass, after it is done, we need to return control to the catch, which is upstack of the EH dispatch code.
This is where the "EH modes" come in. The first scheme we had was based on Emscripten JS exceptions - you throw a "C++" exception that you then catch in a "C++ way".
The second scheme is based on native WASM EH (it is simpler), you throw a WASM exception, catch a WASM exception.
The third and final scheme works by checking a special global value RhpExceptionThrown on return from any call that may 'throw' and early-returning if it is set, unwinding 'manually'. The benefit is that you don't need any support from the WASM enginges with this scheme. The downside is code size (naturally).
Now that we have native WASM EH support in Wasmtime, it is more viable to only use the second scheme, however, I worry that would close the door permanently to any project that would want to use our code without WASM EH (the new WASM EH turns out to soft-require WASM GC support).
In any case, the bulk of support for these schemes is not in this tiny link-time library, but the Jit itself.
There was a problem hiding this comment.
The third and final scheme works by checking a special global value RhpExceptionThrown on return from any call that may 'throw' and early-returning if it is set, unwinding 'manually'. The benefit is that you don't need any support from the WASM enginges with this scheme. The downside is code size (naturally).
@jakobbotsch tells me this is how Swift EH works as well (returning an error status from a call as a second return value instead of in a global).
| Thread* pThread = ThreadStore::GetCurrentThread(); | ||
| Object* obj = (Object*)RhpGcAlloc(pEEType, uFlags, numElements, pFrame); | ||
|
|
||
| #ifndef FEATURE_WASM_MANAGED_THREADS |
There was a problem hiding this comment.
This is running finalizer inline, right ?
We discussed it here #114096 (comment)
For browser/coreCLR we already have solution via JS setTimeout
For WASIp2+ this could also be a "timer" via pollable with timeout. I think we have that for Mono, but I'm not sure.
cc @jkotas
There was a problem hiding this comment.
Yeah this part will need to be changed for browser as we integrate some of the native JS libraries.
There was a problem hiding this comment.
We discussed it here #114096 (comment)
This was ok as something quick for experiment. It should be reverted from this PR (or wrapped in TODOs)
| #include "ObjectLayout.h" | ||
|
|
||
| // | ||
| // WASM-specific allocators: we define them to use a shadow stack argument to avoid saving it on the fast path. |
There was a problem hiding this comment.
Does this allocator avoid mmap ? That would be great.
| // is a heavy operation that should ideally be done in preemptive mode. However, doing it this way allows us to | ||
| // avoid the complexity of skipping the PI stub managed frame that can be part of a QCall sequence in Debug code. | ||
| // | ||
| EM_JS(int32_t, RhpGetCurrentBrowserThreadStackTrace, (void* pShadowStack, JSPointerType pOutputBuffer, int allFramesAsJS), { |
There was a problem hiding this comment.
EM_JS is code-smell in my opinion.
This could be a separate js file, probably in shape of emscripten library via --js-library emcc flag.
If integrated with src/native/libs/System.Native.Browser later, this could live there.
It doesn't have to be on this PR.
| #ifndef HOST_WASM | ||
| Thread* m_pThread; // Cached so that GetThread is only called once per method | ||
| uint32_t m_Flags; // PInvokeTransitionFrameFlags. | ||
| TgtPTR_Void m_RIP; // PInvokeTransitionFrameFlags. | ||
| #endif // HOST_WASM |
There was a problem hiding this comment.
It is an artifact of optimization work I did some time back to remove all these members. An intermediate stage in that work was having m_pThread only (therefore it was made the first field). It doesn't really matter.
| # The AsmOffsets.cs is consumed later by the managed build | ||
| TARGET PortableRuntime | ||
| POST_BUILD | ||
| COMMAND ${CMAKE_CXX_COMPILER} ${COMPILER_LANGUAGE} ${DEFINITIONS} ${PREPROCESSOR_FLAGS} | ||
| -I"${ARCH_SOURCES_DIR}" "${ASM_OFFSETS_CSPP}" >"${CMAKE_CURRENT_BINARY_DIR}/AsmOffsets.cs" |
| lambda((size_t*)&R15()); | ||
| } | ||
|
|
||
| #elif defined(HOST_WASM) |
There was a problem hiding this comment.
Is this dead? It looks like it's wrapped in a ifndef HOST_WASM
| return copy.Extract(); | ||
| } | ||
|
|
||
| #ifndef HOST_WASM |
There was a problem hiding this comment.
This seems like it should be a wasi conditional, not a wasm one. Wasm emscripten has partial dlopen/dlsym support AFAIK
Draft for now. I think we'll want to iterate on what exactly we want to upstream, but this is now buildable.
Should match the strategy suggested by @SingleAccretion.
I also split this into 3 commits with the hope that if we do a merge commit, this won't cause too much destruction in the native AOT-LLVM branch. The commit d89c986 has files from NativeAOT-LLVM as they are right now, rebased on top of main, so "take theirs" should be a good merge strategy for those files, but they may just merge clean. The only exception are Runtime/CMakeLists.txt and Runtime/Portable/CMakeLists.txt that didn't build so I had to make changes and they will conflict in a couple file. Maybe "take theirs" should be the merge strategy too...
Cc @dotnet/ilc-contrib @yowl @dotnet/wasm-contrib