Skip to content

Upstream WASM native AOT runtime from feature/NativeAOT-LLVM#126633

Open
MichalStrehovsky wants to merge 3 commits intodotnet:mainfrom
MichalStrehovsky:integrate-naotwasm
Open

Upstream WASM native AOT runtime from feature/NativeAOT-LLVM#126633
MichalStrehovsky wants to merge 3 commits intodotnet:mainfrom
MichalStrehovsky:integrate-naotwasm

Conversation

@MichalStrehovsky
Copy link
Copy Markdown
Member

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...

  • What exception handling model(s?) do we want
  • Upstream WASI?
  • Does WASM RyuJIT use the same calling conventions as LLVM RyuJIT?
  • We'll need to do cleanups

Cc @dotnet/ilc-contrib @yowl @dotnet/wasm-contrib

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, no dlopen, 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/SetSP are 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 to IP/SP to keep invariants sane.

    inline void SetIP(PCODE IP) { }
    inline void SetSP(uintptr_t SP) { }
};

src/coreclr/nativeaot/Runtime/unix/NativeContext.h:12

  • NativeContext.h is wrapped in #ifndef HOST_WASM, which means the later #elif defined(HOST_WASM) branch inside NATIVE_CONTEXT is dead/unreachable. Either remove the outer #ifndef HOST_WASM or 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()
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
__attribute__((weak)) void _initialize()
extern "C" __attribute__((weak)) void _initialize()

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +13
// 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;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: “Recieve” should be “Receive”.

Copilot uses AI. Check for mistakes.
Comment on lines 294 to 300
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; }

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +33
static bool s_finalizing = false;

// Recursive wait on finalization is a no-op.
if (g_FinalizationRequestPending && !g_FinalizationInProgress)
{
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
// TODO-LLVM-Cleanup: replace with with PLATFORM_THREAD_LOCAL after merge.
extern __thread SparseVirtualUnwindFrame* t_pLastSparseVirtualUnwindFrame;

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: duplicated word “with” in “replace with with PLATFORM_THREAD_LOCAL”.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +15
//
// TODO-LLVM-Cleanup: replace with with PLATFORM_THREAD_LOCAL after merge.
__thread SparseVirtualUnwindFrame* t_pLastSparseVirtualUnwindFrame = nullptr;

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: duplicated word “with” in “replace with with PLATFORM_THREAD_LOCAL”.

Copilot uses AI. Check for mistakes.
@pavelsavara pavelsavara added the arch-wasm WebAssembly architecture label Apr 8, 2026
@@ -0,0 +1,12 @@
# Build two static libraries for the two exception handling models.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There are 3 models below not two.
But we we really want to support anything but modern wasm exceptions going forward ?

cc @kg

Copy link
Copy Markdown
Contributor

@SingleAccretion SingleAccretion Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CppExceptionHandling can be dropped. The emulated model should be kept, it is important for compatibility in the broad sense.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this part will need to be changed for browser as we integrate some of the native JS libraries.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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), {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +492 to +496
#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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we reordering the members?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +39 to +43
# 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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this removing the dependency?

lambda((size_t*)&R15());
}

#elif defined(HOST_WASM)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this dead? It looks like it's wrapped in a ifndef HOST_WASM

return copy.Extract();
}

#ifndef HOST_WASM
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it should be a wasi conditional, not a wasm one. Wasm emscripten has partial dlopen/dlsym support AFAIK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-NativeAOT-coreclr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants