Skip to content

[wasm][wasi] CoreCLR wasi draft#130051

Draft
lewing wants to merge 66 commits into
dotnet:mainfrom
lewing:wasi-bringup
Draft

[wasm][wasi] CoreCLR wasi draft#130051
lewing wants to merge 66 commits into
dotnet:mainfrom
lewing:wasi-bringup

Conversation

@lewing

@lewing lewing commented Jun 30, 2026

Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings June 30, 2026 18:44
@github-actions github-actions Bot added the area-Infrastructure-coreclr Only use for closed issues label Jun 30, 2026
@lewing

lewing commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

lewing and others added 24 commits June 30, 2026 13:58
…ratedstringthunks

The new R2R string-thunk infrastructure added in dotnet#127483 uses WebcilDecoder
inside a TARGET_WASM block. FEATURE_WEBCIL was only enabled for
CLR_CMAKE_TARGET_BROWSER, so the WASI build hit 'unknown type name WebcilDecoder'.
Enable FEATURE_WEBCIL for WASI too and add a guarded include.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reapplies the build-system subset of the WASI-target seed commit from
PR dotnet#128465 (2d2e1fb), adapted to the current pr/128997 base:

- eng/Subsets.props: register TargetsWasi as a CoreCLR-supported OS,
  default flavor + DefaultCoreClrSubsets (dropped provision.wasisdk —
  there is no AcquireWasiSdk.proj; SDK acquisition is wired through
  runtime.proj and native.wasm.targets instead).
- eng/native.wasm.targets: import AcquireWasiSdk.targets; mirror the
  Browser+CoreCLR ItemGroup for Wasi+CoreCLR (GEN_PINVOKE +
  PERFTRACING listen-port disables).
- eng/native/configurecompiler.cmake: emit TARGET_WASI under the unix
  branch so it pairs with TARGET_BROWSER.
- eng/native/configureplatform.cmake: set CLR_CMAKE_HOST_UNIX +
  CLR_CMAKE_TARGET_UNIX for WASI.
- src/coreclr/CMakeLists.txt: add WASI to FEATURE_STANDALONE_GC opt-out;
  add the WASI compile-options block (emulated mman/signal/clocks/getpid,
  sjlj for now (Phase 6 will switch to native wasm exceptions),
  F_DUPFD_CLOEXEC/O_CLOEXEC defines, pal/wasi shim include dir);
  exclude WASI from test-watchdog and tools/aot/jitinterface.
- src/coreclr/clrfeatures.cmake: untouched (FEATURE_EVENT_TRACE
  condition was reworked upstream — main now enables event_trace for
  browser CoreCLR via dotnet#128997, so the seed's exclusion no longer
  applies. Leave WASI on the same path; revisit if the WASI build
  fails on event_trace).
- src/coreclr/dlls/mscoree/exports.cpp: untouched (seed added debug
  fprintfs later reverted in commit 3ca3028).
- src/coreclr/gc/unix/*: untouched (the gc/wasm/ subdir added in
  dotnet#127328 already handles WASI; gc/unix is no longer compiled for
  wasm targets).
- src/coreclr/runtime.proj: add AcquireWasiSdk to
  BuildRuntimeDependsOnTargets for Wasi.
- src/coreclr/tools/CMakeLists.txt: exclude WASI from superpmi.
- src/coreclr/debug/debug-pal/CMakeLists.txt: keep
  DEBUG_PAL_REFEREENCE_DIAGNOSTICSERVER off for WASI.
- src/coreclr/debug/debug-pal/unix/twowaypipe.cpp: stub mkfifo() on
  WASI (ENOTSUP).
- src/coreclr/pal/src/CMakeLists.txt: skip libunwind on WASM hosts;
  exclude debug/debug.cpp and exception/signal.cpp for WASI; skip
  coreclrpal_dac libunwind on wasm. (mmap-wasi.c will be added in
  Phase 3.)
- src/coreclr/pal/src/configure.cmake: guard pthread checks behind
  PTHREAD_LIBRARY (WASI has no pthread); HAVE_SCHED_OTHER_ASSIGNABLE=0
  for WASI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cmake try_run() cannot execute wasm32 binaries on the build host, so
the configure step fails with 'invoked in cross-compiling mode, please
set the following cache variables appropriately'. Pre-set the four
checks invoked from src/coreclr/pal/src/configure.cmake:

- REALPATH_SUPPORTS_NONEXISTENT_FILES: WASI realpath rejects
  nonexistent paths.
- HAVE_WORKING_GETTIMEOFDAY / HAVE_WORKING_CLOCK_GETTIME: provided
  via WASI's _WASI_EMULATED_PROCESS_CLOCKS shim.
- HAVE_PROCFS_STATM: WASI has no /proc filesystem.

With this, 'build.sh clr.runtime -os wasi' now reaches PAL source
compilation; remaining failures are missing pal/wasi/ shim headers,
which Phase 3 (PAL stubs) will supply.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Investigation-first approach: probe wasi-sdk 33 / wasm32-wasip2 surface,
build, and apply the minimum viable fix for each gap. No -isystem header
masquerading; one explicit pal_wasi_missing.h pulled in at the sites that
need it, plus skip-file for sources where the gap is structural.

Strategies applied:

** Skip-file (structural gap) **
- src/coreclr/pal/src/CMakeLists.txt: thread/context.cpp joins the
  existing debug/debug.cpp + exception/signal.cpp exclusion list for
  TARGET_WASI. The whole file is heavily POSIX (uc_mcontext, ptrace,
  FPE_*/T_* signal codes) and provides functions the WASI interpreter
  never actually invokes.
- src/coreclr/pal/src/exception/seh.cpp: don't #include seh-unwind.cpp
  on TARGET_WASI (libunwind is unavailable in wasi-sdk).

** WASI branch in-place **
- src/coreclr/pal/src/include/pal/context.h: add a TARGET_WASI branch
  providing opaque native_context_t (placeholder member to avoid the
  empty-struct extern-C warning), and pull in pal_wasi_missing.h for
  siginfo_t.
- src/coreclr/pal/src/include/pal/signal.hpp,
  src/coreclr/pal/src/include/pal/process.h: include <signal.h> +
  pal_wasi_missing.h on TARGET_WASI so siginfo_t in declarations resolves.
- src/coreclr/pal/src/file/file.cpp: add an explicit TARGET_WASI no-op
  branch for FILE_FLAG_NO_BUFFERING (WASI has no uncached I/O) so we
  don't hit the 'Insufficient support for uncached I/O' #error.
- src/coreclr/pal/src/init/pal.cpp: gate <pwd.h> include, replace
  getsid(gPID) with gSID = gPID on WASI.
- src/coreclr/pal/src/loader/module.cpp: gate <link.h> include; the only
  user (dl_iterate_phdr) is already inside a non-WASM branch.
- src/coreclr/pal/src/map/virtual.cpp: skip the RLIMIT_AS/RLIMIT_DATA
  sizing block on WASI; wasm32 has a fixed 4 GiB cap and no rlimits.
- src/coreclr/pal/src/synchmgr/synchmanager.cpp,
  src/coreclr/pal/src/thread/process.cpp: gate the <sys/wait.h> include
  (already used inside non-WASM branches). process.cpp also skips the
  kill(SIGKILL) call on the external-process path (WASI is single-process).
- src/coreclr/pal/src/thread/thread.cpp: stub pthread_exit (wasi-libc
  has _Static_assert stubs under _WASI_STRICT_PTHREAD) with abort(),
  short-circuit InternalSetThreadPriority on WASI (just records the
  requested value), add a TARGET_WASI branch returning zero
  user-cpu-time (no per-thread cpu time API in WASI 0.2.8).
- src/coreclr/vm/wasm/cgencpu.h: split GetCurrentSP() into TARGET_BROWSER
  (emscripten_stack_get_current) vs TARGET_WASI (return nullptr; never
  invoked because the WASI interpreter walks the explicit frame chain).
- src/coreclr/minipal/Unix/doublemapping.cpp: skip the
  RLIMIT_AS/RLIMIT_FSIZE clipping on WASI; gate <link.h> include but
  still pull in pal_wasi_missing.h for the Dl_info type used in a
  struct that's referenced unconditionally (the dladdr call is
  FEATURE_MAP_THUNKS_FROM_IMAGE-only / Apple).

** New file: pal/src/include/pal/wasi/pal_wasi_missing.h **
Single explicit shim header (no -isystem interposition). Provides:
- siginfo_t (wasi-libc hides it behind __wasilibc_unmodified_upstream)
- struct passwd (no <pwd.h> in wasi-libc)
- Dl_info + dladdr() stub returning failure (wasi-libc <dlfcn.h> has
  dlopen but no Dl_info / dladdr)
- getuid/geteuid/getgid/getegid returning 0 (no user concept on WASI)
Each entry is tagged with a REPLACE-WHEN condition.

** WASI block in arch/wasm/stubs.cpp **
- Split DBG_DebugBreak / ThrowExceptionFromContextInternal into
  TARGET_BROWSER (emscripten) vs TARGET_WASI (__builtin_debugtrap + abort)
  branches.
- Move the unw_* trap stubs under #if defined(TARGET_BROWSER) — they
  satisfy emscripten's libunwind shim references; not needed on WASI
  because seh-unwind.cpp is excluded.
- Add WASI stubs for CONTEXT_GetRegisters, CONTEXTToNativeContext,
  CONTEXTFromNativeContext, GetNativeContextPC, GetNativeContextSP,
  CONTEXTGetExceptionCodeForSignal, getpwuid_r — symbols thread/context.cpp
  would otherwise provide, plus getpwuid_r the transitive pwd.h users
  may reference. All stubs assert at runtime since they're never reached
  in the WASI interpreter execution model.

Status after this commit: every coreclrpal .cpp compiles for wasi-wasm
under wasi-sdk 33 / LLVM 22.1.0. Build now reaches the link stage,
exposing host (corerun) and eventpipe-ipc issues that belong to Phase 5
and a follow-up respectively.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Phase 2 commit excluded WASI from DEBUG_PAL_REFEREENCE_DIAGNOSTICSERVER
to mirror the seed, but that left ds_ipc_pal_init / ds_ipc_alloc /
ds_ipc_close / ds_ipc_pal_shutdown / ds_ipc_stream_* unresolved at link
time because eventpipe_objs (always built when FEATURE_PERFTRACING is on)
references them through ds-server.c.

Re-enable DEBUG_PAL_REFEREENCE_DIAGNOSTICSERVER on the Unix branch
unconditionally (the seed's WASI carve-out wasn't justified — we still
need the symbols to link). The default Unix fallback selects
ds-ipc-pal-socket.c with DS_IPC_PAL_UDS, but wasi-libc's sockaddr_un has
no sun_path field (WASI has no UNIX-domain sockets), so add
-DFEATURE_PERFTRACING_PAL_TCP=1 to the WASI+CoreCLR cmake args. That
selects AF_INET/AF_INET6 instead, which WASI 0.2.8 sockets supports.

Listen-port construction stays disabled
(FEATURE_PERFTRACING_DISABLE_PERFTRACING_LISTEN_PORTS=1 +
FEATURE_PERFTRACING_DISABLE_DEFAULT_LISTEN_PORT=1) so the diag server
PAL is present at link time but never actually opens a port at runtime.

Phase 4 of the cherry-pick plan is otherwise a no-op:
* The exceptionhandling.cpp / interpexec.cpp 'set interpreter context'
  patch (#91e51f80d37) was reverted in the same series (#6a56938540d).
  Net effect zero; main already matches.
* The FEATURE_WEBCIL+webcildecoder include in pregeneratedstringthunks
  is already done in Phase 1 commit c9dc241.
* WasiFinalizerScheduler deferred per plan.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Working CoreCLR-WASI corerun: builds, links as a WASI Component
(wasm-component-ld), runs under nesm and wasmtime 45, prints USAGE in
~200 ms.

** Linker / EH model (Phase 6 folded into Phase 5) **
- eng/native/configureplatform.cmake: switch CLR_CMAKE_TARGET_WASI from
  -fexceptions (sjlj) to -fwasm-exceptions + -mllvm
  -wasm-use-legacy-eh=false. sjlj cannot handle the interpreter
  ResumeAfterCatch throw-from-catch pattern; the new try_table/throw_ref
  proposal is what wasmtime 45 supports natively. Pulls in wasi-sdk 33's
  eh-flavored libc++/libc++abi/libunwind automatically.
- eng/native/configureplatform.cmake: add browser-style project-wide link
  options for WASI (-fwasm-exceptions, -Wno-unused-command-line-argument,
  -Wl,--error-limit=0). Clear CMAKE_C/CXX_IMPLICIT_LINK_LIBRARIES and
  CMAKE_C/CXX_STANDARD_LIBRARIES so CMake doesn't add a default -lstdc++
  (wasi-sdk ships libc++ only). Link -lc -lc++ -lc++abi -lunwind -ldl
  plus the WASI emulated libc helpers (process-clocks, signal, mman,
  getpid).
- src/coreclr/CMakeLists.txt: drop -mllvm -wasm-enable-sjlj for WASI
  (was the sjlj setup we tried first). Document the EH model decision
  inline. Disable FEATURE_EVENT_TRACE and FEATURE_PERFTRACING for the
  initial WASI bring-up so the diagnostic-server PAL (ds-ipc-pal-socket.c)
  doesn't get pulled in — revisit when WASI sockets are validated.

** Host (Phase 5 proper) **
- src/coreclr/hosts/corerun/CMakeLists.txt: use wasm-component-ld
  (target-local via target_link_options, pointed at the wasi-sdk
  binary by absolute path because clang's -fuse-ld doesn't accept
  custom linker names) so the output is a WASI Component, not a
  plain core wasm module. Add stack-size and initial-memory tuning
  matching the Mono-WASI runtime. configure.cmake is included
  unconditionally (its check_symbol_exists/check_struct_has_member
  helpers gracefully return FALSE on WASI without failing the build).
  Skip CMAKE_DL_LIBS / -rdynamic on WASM hosts.
- src/coreclr/hosts/corerun/corerun.hpp: gate try_load_library's
  dlopen call behind #ifdef TARGET_WASM (returns false on WASM; callers
  treat it as 'library not available').

** PAL stubs to satisfy the link (Phase 5 link-stage discovery) **
- src/coreclr/pal/src/arch/wasm/stubs.cpp: add WASI-block stubs for the
  symbols seh-unwind.cpp (excluded) and debug/debug.cpp (excluded)
  would normally provide:
    * RaiseException (SEH entry), DebugBreak, OutputDebugStringA/W
    * PAL_FreeExceptionRecords, AllocateExceptionRecords, PAL_VirtualUnwind
    * Signal-handling: SEHInitializeSignals / SEHCleanupSignals /
      UnmaskActivationSignal / PAL_EnableCrashReportBeforeSignalChaining
      (C++ linkage to match pal/src/include/pal/signal.hpp declarations)
    * GetThreadContext, FlushInstructionCache
    * SystemJS_GetLocaleInfo (browser-only entry; null on WASI)
  Most stubs assert at runtime — they exist purely so the linker resolves
  references in code paths that never run on the WASI interpreter.

** Install (eng/native/functions.cmake) **
- install_clr: distinguish CLR_CMAKE_TARGET_BROWSER (install .js+.wasm
  pair per emscripten) from CLR_CMAKE_TARGET_WASI (install the single
  wasm-component-ld binary as one file). Previous unified WASM branch
  expected the emscripten .js sidecar which doesn't exist on WASI.

** Eventpipe / cdac deferral **
- eng/native.wasm.targets: drop the FEATURE_PERFTRACING_PAL_TCP=1 plumbing
  added earlier (Phase 4) — eventpipe is now disabled entirely for WASI
  via FEATURE_PERFTRACING=0 in coreclr/CMakeLists.txt. Listen-port
  disables retained for symmetry with browser-CoreCLR and for when we
  re-enable PERFTRACING.
- src/coreclr/debug/debug-pal/CMakeLists.txt: restore the WASI exclusion
  of DEBUG_PAL_REFEREENCE_DIAGNOSTICSERVER (Phase 4 enabled it; with
  PERFTRACING off the diag-server PAL isn't needed). cdac kept on by
  default; switch off if/when it complicates the wasm-component-ld link.

Verified end-to-end: corerun.wasm (88 MB) parses as a Component
(3 core modules, 18 wasi:* imports, canonical functions), instantiates
under nesm with --sandbox, prints USAGE on stderr, exit code 0,
elapsed ~200ms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…CLOEXEC)

The defined F_DUPFD_CLOEXEC=1030 in coreclr/CMakeLists.txt was a
compile-only hack — WASI's fcntl rejects unknown commands at runtime,
returning -1, so init_std_handle() failed and coreclr_initialize bailed
with ERROR_PALINIT_STD_HANDLES (HRESULT 0x8007ff07).

WASI has no dup() either (wasi-libc declares it only under
__wasilibc_unmodified_upstream). Since WASI has no fork/exec there is
no concurrent fd race; bind the PAL handle to the original stdio fileno
directly. FILECloseHandle on standard streams already avoids the
fclose path so reusing the fd is safe.

After this fix coreclr_initialize() reaches its next phase (heap setup);
the runtime now reports 0x8007000e (ERROR_NOT_ENOUGH_MEMORY) instead
of the PAL stdio failure.

Verified end-to-end: corerun.wasm under nesm now builds the full TPA
list (180+ assemblies including System.Private.CoreLib), parses
arguments, and reaches coreclr_initialize.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three small fixes to get coreclr_initialize past GCHeap::Initialize and
Thread::InitThread on WASI:

* pal/src/thread/thread.cpp: add TARGET_WASI branch to GetStackBase and
  GetStackLimit. wasi-libc's pthread_getattr_np / pthread_attr_getstack
  silently failed under _WASI_STRICT_PTHREAD, returning NULL and tripping
  Thread::SetStackLimits -> ThrowOutOfMemory. Hardcoded bounds match
  the corerun CMakeLists.txt -Wl,-z,stack-size=8388608 with --stack-first.

* pal/src/CMakeLists.txt + pal/src/arch/wasm/mmap-wasi.c: add a small
  arena/malloc-based mmap shim for WASI. The bundled wasi-emulated-mman
  mishandles PROT_NONE, anonymous reuse, and file-backed mappings used by
  PAL map/map.cpp for assembly loading.

* hosts/corerun/CMakeLists.txt: bump corerun initial/max linear memory
  from 50 MiB to 128 MiB / 4 GiB so the GC's segment-mode initial heap
  reservation (48 MiB SOH+LOH+POH on wasm32, no USE_REGIONS) fits.

With these in place HelloWorld now gets past GC init, Thread setup,
FinalizerThreadCreate, Assembly::Initialize and fails inside
SystemDomain::Init while trying to load '/System.Private.CoreLib.dll'
(empty m_SystemDirectory -- GetClrModuleDirectory returns '/' under
WASI). That is the next item.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* minipal_getexepath: split TARGET_WASM into TARGET_BROWSER (unchanged,
  returns '/') and TARGET_WASI (returns CORE_ROOT + '/corerun.wasm').
  SystemDomain::Init derives the system directory from the exe path; on
  WASI argv[0] is unreliable so we use the host-provided CORE_ROOT.

* pal/src/map/map.cpp InternalCreateFileMapping: WASI has no dup() and
  rejects fcntl(F_DUPFD_CLOEXEC). Re-open the file by its path so the
  mapping owns a separate fd. Same fix pattern as init_std_handle.

With these in place HelloWorld now loads System.Private.CoreLib, binds
the default app domain, and reaches the CILInterp path. It currently
asserts inside PortableEntryPoint::GetActualCode while compiling an
ldftn (real interpreter integration work, no longer PAL plumbing).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EnableFinalization is called from inside the GC. The previous WASI
implementation ran FinalizerThreadWorkerIteration synchronously from
there, which is unsafe:

- FinalizerThreadWorkerIteration declares GC_TRIGGERS, so finalizers
  can request another GC while we are mid-collection.
- It transitions thread mode via EnablePreemptiveGC and back to
  cooperative, which is not legal from inside a GC.
- Finalizer-triggered allocations can recurse into EnableFinalization
  with unbounded stack depth.
- Tests that rely on the semantics of finalizers running on a
  dedicated context (GC.WaitForPendingFinalizers and friends) observe
  the wrong behavior.

Mirror the browser pattern: native EnableFinalization calls
SystemJS_ScheduleFinalization, which on WASI dispatches to a function
pointer that managed code installed at startup. The managed callback
queues SystemJS_ExecuteFinalizationCallback through
ThreadPool.UnsafeQueueUserWorkItem so it runs from WasiEventLoop's
ThreadPoolWorkQueue.Dispatch() pump, at a point where mode transitions
and re-entry into the GC are safe.

Pieces:

- src/coreclr/vm/finalizerthread.cpp
  - Hoist SystemJS_ExecuteFinalizationCallback out of the
    TARGET_BROWSER block so WASI can call back into it.
  - Add WasiFinalizerScheduler_Register QCall that stores the managed
    callback pointer, and a SystemJS_ScheduleFinalization
    implementation for WASI that forwards to it.
  - Replace the synchronous FinalizerThreadWorkerIteration call in
    EnableFinalization with SystemJS_ScheduleFinalization.

- src/coreclr/vm/wasm/entrypoints.h, src/coreclr/vm/qcallentrypoints.cpp
  - Declare and register the new WASI entry points.

- src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiFinalizerScheduler.cs
  - New managed-side bridge. ModuleInitializer-driven Register call
    installs the function pointer; the [UnmanagedCallersOnly] callback
    queues the work onto ThreadPool.

Followups:
- Rename SystemJS_* to a host-neutral prefix; the JS naming is
  inherited from the browser path and is misleading for WASI.
- Consider exposing SystemJS_ExecuteFinalizationCallback under a
  WASI-specific name once the rename lands.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DetachThread runs from the C++ thread_local destructor at process exit,
*after* the EBR thread record for this thread has already been marked
detached by an earlier dtor. On WASI EnableFinalization defers to a
managed ScheduleFinalization callback that posts to ThreadPool — running
that callback at shutdown triggers JIT of a method whose IL contains a
string literal, which re-enters EBR and trips the
"Attempt to reattach detached thread" assert in ebr.cpp:144.

Skipping the EnableFinalization call on WASI is safe in this context:
there is no other thread that could observe leftover detached-thread
state, the runtime is exiting, and the deferred finalization scheduler
would not get a chance to drain anyway.

With this fix HelloWorld runs end-to-end:

  $ wasmtime run -W exceptions=y -W function-references=y \
      --dir=/tmp/wasi-core-root::/coreroot \
      --env CORE_ROOT=/coreroot \
      corerun.wasm /coreroot/HelloWorld.dll
  Hello from CoreCLR-WASI corerun!

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CoreCLR System.Private.CoreLib.csproj has an ItemGroup gated on
'$(TargetsUnix)' or '$(TargetsBrowser)' that includes the Unix
Interop.Libraries.cs shim (which maps Kernel32 = QCall so the bridged
Windows API names used throughout the BCL resolve). TargetsWasi was
missing from the condition, so 'build.sh clr.corelib -os wasi -arch wasm'
failed with:

  error CS0117: 'Interop.Libraries' does not contain a definition for
                'Kernel32'

on every Common/Interop/Windows/Kernel32/*.cs source the BCL pulls in
under the FeatureCoreCLR-gated ItemGroup further down.

This is purely a build-config gap (the wasi build was using a stale
artifacts/obj copy until now, masking the issue). With this fix
'./build.sh clr.corelib -os wasi -arch wasm -c release' produces a
fresh, AsmOffsets-correct CoreLib that runs HelloWorld and the smoke
test (including try/catch/rethrow/finally) end-to-end under wasmtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These three GENERATED files are emitted by the ManagedToNativeGenerator
MSBuild task in src/tasks/WasmAppBuilder/coreclr/. They are normally
regenerated at build time when a user wasm app links corerun + BCL,
but the copies checked into src/coreclr/vm/wasm/ are also used directly
by the standalone corerun build, which has no equivalent regeneration
step. They drift relative to the current System.Private.CoreLib + BCL
whenever upstream adds or removes UnmanagedCallersOnly / managed-callable
helpers.

Re-ran ManagedToNativeGenerator over /tmp/wasi-core-root/*.dll (the
runtime pack we ship for HelloWorld + smoke testing), excluding the
Network/Http assemblies that pull in wasi:http imports the corerun
component doesn't declare.

Side effect: the regenerated callhelpers-reverse.cpp already contains
the WasiFinalizerScheduler.ScheduleFinalization entry that ca1f976
added by hand, so the manual addition is dropped here.

Follow-up: wire this regeneration into the runtime build proper instead
of relying on a hand-run regen step, so the checked-in files cannot
drift again.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native WASI CoreCLR build sets FEATURE_PERFTRACING=0
(src/coreclr/CMakeLists.txt:52, under CLR_CMAKE_TARGET_WASI), so none
of the EventPipeInternal_* QCalls (CreateProvider, DefineEvent, Enable,
WriteEventData, etc.) are registered on the native side.

But the managed System.Private.CoreLib build inherited the default
FeaturePerfTracing=true from clr.featuredefines.props, so the IL of
EventProvider/EventPipeInternal still emitted PInvoke calls into those
unresolved QCalls. The PInvoke function pointer resolves to NULL at
runtime, and any subsequent EventSource activation traps on a NULL
calli.

The path that surfaced this:

  string s = $"value={42}";

  -> DefaultInterpolatedStringHandler rents a char[] from
     ArrayPool<char>.Shared
  -> first touch of ArrayPoolEventSource.Log triggers its .cctor
  -> EventSource(string,Guid) base ctor builds an EventProvider
  -> EventProvider on non-Windows calls EventPipeInternal.CreateProvider
  -> LibraryImport-generated stub <CreateProvider>g____PInvoke|4_0
     loads a NULL function pointer and traps via call_indirect

string.Format (and string concatenation) don't hit this because they
do not exercise ArrayPool<char>.Shared.

Setting FeaturePerfTracing=false for TargetsWasi keeps the managed and
native sides consistent: EventProvider falls back to the no-op
EventProviderImpl path (Register/Unregister/Enable are all stubs), and
EventSource activation no longer reaches any QCall.
src/native/libs/build-native.proj imports AcquireWasiSdk.targets
unconditionally. For TargetsBrowser/TargetsWasi builds the project
also imports eng/native.wasm.targets (via line 3), which in turn
imports AcquireWasiSdk.targets — producing MSB4011 'project cannot
be imported again' under TargetsWasi.

Guard the direct import on '!TargetsWasi'; native.wasm.targets
covers the WASI case.
Mirrors the browser-wasm CoreCLR build entry in runtime.yml using a
new wasi-wasm-coreclr-build-only.yml template that produces a
runtime pack + corerun.wasm artifact under wasi.wasm.Release.

Build-only for now: wasmtime is not yet distributed in our helix
images and src/tests for wasi-coreclr is still being validated.
A follow-up will add wasi-wasm-coreclr-runtime-tests.yml that
consumes BuildArtifacts_wasi_wasm_*_Release_CoreCLR and runs
selected CoreCLR runtime tests under wasmtime.
Two oversights from earlier commits in this branch:

1. src/native/libs/build-native.proj had a direct unconditional
   <Import Project="eng/AcquireWasiSdk.targets" /> that duplicated the
   import already done by eng/native.wasm.targets (imported above on
   line 3 whenever TargetsBrowser or TargetsWasi). For both browser
   and wasi builds this produced MSB4011 'cannot be imported again'.

   Earlier commit 8e788a9 narrowed the condition to skip the
   direct import for TargetsWasi only — but TargetsBrowser hit the
   same duplicate. The AcquireWasiSdk targets file is only needed
   when BuildNativeCommonDependsOnTargets references the
   'AcquireWasiSdk' target (Condition TargetsWasi=true), and in that
   case it's brought in by native.wasm.targets. Remove the direct
   import entirely.

2. The thunk split commit (1d75035) added
   src/coreclr/vm/wasm/{browser,wasi}/callhelpers-*.cpp but did not
   delete the stale shared copies still sitting at
   src/coreclr/vm/wasm/. CMake never references them after the split
   (CMakeLists.txt selects the subdir), but they confuse readers and
   risk drift. Remove the dead files.
The earlier WASI bring-up commit (f945fdd) scoped the new
try_load_library short-circuit on TARGET_WASM, matching the
pre-existing TARGET_WASM scoping of try_load_hostpolicy and
try_load_coreclr from the browser-corerun PR (39ca828).

But emscripten does support dlopen when the module is built with
-sMAIN_MODULE / -fPIC, so TARGET_WASM is broader than necessary
for this particular function. Only wasi-libc has no dlopen at all.

Narrow the guard to TARGET_WASI so a future browser build that
opts into emscripten dynamic linking can still load auxiliary
libraries through try_load_library without changes here. The
other TARGET_WASM-scoped dlopen paths (try_load_hostpolicy,
try_load_coreclr) are left as-is — they were already TARGET_WASM-
scoped before this branch and are not part of this commit's scope.
Commit 70fa307 originally added 'F_DUPFD_CLOEXEC=1030' as a
'compile-only hack' to satisfy code that referenced the constant. It
also added 'O_CLOEXEC=0' for the same reason.

Subsequent commits (70fa307, a085400) replaced both PAL use
sites with explicit TARGET_WASI branches:

  * pal/src/file/file.cpp:2427 — uses fileno(stream) directly on WASI
  * pal/src/map/map.cpp:469 — re-opens the file by path on WASI

The remaining O_CLOEXEC references (process.cpp, threadsusp.cpp,
perfjitdump.cpp) are in code paths not reached on WASI (fork/exec,
signal-based thread suspension, JIT-dump). Defining the constants
is no longer load-bearing.

Remove the defines and leave a NOTE so future code that touches
fcntl on WASI prefers an explicit TARGET_WASI branch over re-adding
the macro fallback. Verified: full corerun rebuild succeeds and
Smoke.dll passes 10/10 under wasmtime.
The previous design had FinalizerThread::EnableFinalization() call
SystemJS_ScheduleFinalization, which dispatched to a managed callback
registered via WasiFinalizerScheduler.Register. That callback used
ThreadPool.UnsafeQueueUserWorkItem to defer
SystemJS_ExecuteFinalizationCallback to a safe point.

This crashed any test that triggered a GC during managed allocation:

    new Object[i]
      RhpNewPtrArrayFast → AllocateSzArray → GCHeap::Alloc
      → GarbageCollectGeneration → EnableFinalization
      → SystemJS_ScheduleFinalization → managed ScheduleFinalization
      → ThreadPool.UnsafeQueueUserWorkItem
      → re-enters managed code (allocation + lock) FROM INSIDE THE GC
      → wasm trap

EnableFinalization runs inside the GC. Crossing back into managed code
to perform a ThreadPool enqueue is not safe even when the queueing
itself is 'lock free' — the enqueue path still allocates and the
interpreter has to step into it, both of which re-enter the GC.

Replace the design with a pure native atomic flag:

  * SystemJS_ScheduleFinalization just sets s_finalizationPending
    (volatile store — the only safe operation from inside the GC).
  * Add SystemJS_TryClearPendingFinalization QCall that atomically
    clears and returns the previous value.
  * WasiEventLoop's PollWasiEventLoopUntilResolved loop calls
    WasiFinalizerScheduler.DrainIfPending() between ThreadPool
    iterations. DrainIfPending calls SystemJS_ExecuteFinalizationCallback
    when the flag was set — at a safe point with no GC in progress.
  * Drop WasiFinalizerScheduler.Register / ScheduleFinalization /
    the ModuleInitializer entirely.
  * Drop WasiFinalizerScheduler_Register from the QCall registry and
    entrypoints.h.

Regenerate the WASI thunks (callhelpers-{interp-to-managed,reverse}.cpp)
to drop the now-orphaned ScheduleFinalization reverse-thunk entry.

Verified: Smoke.dll 10/10 still passes; GC/API/GC/MaxGeneration
('Test_MaxGeneration.TestEntryPoint') now runs to completion and
reports 'Passed test'.
The shim provided a bump allocator for anonymous mmap mappings and a
malloc+read fallback for file-backed mappings on WASI. Its rationale
was that the GC reserved/released large anonymous regions via mmap,
and wasi-emulated-mman's malloc-backed implementation could recycle
those addresses for subsequent file-backed mappings — which the GC
would then zero, corrupting loaded assembly data.

PR dotnet#127328 ([browser][coreclr] WASM-specific GC OS layer; no mmap/decommit)
made the GC use posix_memalign directly via gc/wasm/gcenv.cpp and the
WASM branch in pal/src/virtual/virtual.cpp. The GC no longer goes through
mmap at all, so the GC↔file-mmap address-reuse conflict can't happen.

The only remaining mmap callers in the WASI build are in
pal/src/map/map.cpp for PE image loading (compiled because
CLR_CMAKE_HOST_UNIX is set for WASI per
eng/native/configureplatform.cmake:222-225). wasi-emulated-mman handles
those file-backed MAP_PRIVATE mappings fine.

Verified: corerun rebuilds clean, Smoke.dll 10/10 still passes,
GC/API/GC/MaxGeneration test still runs to completion under wasmtime
with the shim removed.
Replace the abort-stub for RaiseException in arch/wasm/stubs.cpp with
the actual Unix implementation (matched against seh-unwind.cpp:757).

RaiseException is the WIN32-style SEH entry the CoreCLR VM uses to
raise managed exceptions (CLR throws -> RealCOMPlusThrow ->
RaiseTheExceptionInternalOnly -> RaiseException). On Unix this
allocates an EXCEPTION_RECORD + CONTEXT pair, fills the record from
the SEH args, and throws PAL_SEHException — a C++ throw that
DispatchManagedException catches up the stack.

With -fwasm-exceptions the C++ throw lowers to wasm-native
try_table/throw_ref and libc++abi handles the unwind without
libunwind, so the implementation works on WASI as-is (the CONTEXT
capture / PAL_VirtualUnwind step that the Unix path performs is
already guarded by #ifndef TARGET_WASM in seh-unwind.cpp itself).

Strict improvement over the abort: tests that used to fail-fast
during the first managed throw now make it through that point.

Note: some test paths still crash later in EH dispatch (the
PAL_SEHException doesn't find its catch handler), which is the
same shared interp+EH bug as the SfiNext rethrow-in-catch-funclet
case — orthogonal to this stub.
Turn on ReadyToRun compilation of System.Private.CoreLib for the wasi wasm
target and fix the wasm managed calling-convention local indexing so the
generated module is well-typed.

- crossgen-corelib.proj: stop forcing PublishReadyToRun=false for wasi, and
  broaden the wasm container-format selection from browser-only to all wasm
  targets (TargetsWasm) so wasi uses the wasm R2R container too.

- CorInfoImpl.cs: set CORJIT_FLAG_PORTABLE_ENTRY_POINTS for all wasm32 targets
  rather than only browser. crossgen's WasmLowering.GetSignature unconditionally
  appends the hidden Portable Entry Point parameter to every managed method's
  wasm signature, but the JIT only reserves the PEP's wasm local slot when this
  flag is set. With the flag gated on browser only, wasi method bodies laid out
  their locals one index too low, dropping i64 values into i32-declared slots
  (~912 functions / ~4200 sites in CoreLib). Gating on Wasm32 matches the
  signature crossgen already emits.

With these changes (and the wasm LowerRetStruct fix) the R2R-compiled CoreLib
validates and instantiates under wasmtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A local-only smoke suite under src/coreclr/wasi/tests/ that builds a
managed test assembly with the repo's .dotnet SDK, stages a CORE_ROOT
layout (corerun.wasm + freshly-built System.Private.CoreLib.dll +
runtime-pack BCL + test dll), and runs it under wasmtime with
exception-handling enabled and DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true
(the staged runtime pack does not bundle ICU data; without invariant
the GlobalizationMode+Settings cctor FailFasts the moment any code path
reads a CoreLib resource string).

Pass/fail is signaled by a 'WASI-SMOKE-PASS:<name>' sentinel on stdout
because corerun on WASI does not currently propagate the managed Main
return value — corerun_shutdown overwrites it with latched_exit_code
from coreclr_shutdown_2 (separate follow-up).

The runner auto-provisions wasmtime via the existing Mono provisioning
target (eng/testing/wasi-provisioning.targets), reusing the version
pinned in src/mono/wasi/wasmtime-version.txt; falls back to a user
install at ~/.wasmtime/bin/wasmtime or an explicit $WASMTIME override.

Tests live in self-contained csproj+Program.cs pairs under
smoke/<Name>/, with an empty Directory.Build.props/targets at the
tests root that blocks inheritance from the repo's Arcade plumbing so
they build like any external app. Known-failing scenarios live under
known-broken/ (excluded from the default suite to keep the green
baseline clean, but selectable by name for debugging) with a README
describing the underlying bug.

Initial baseline:

  smoke (default):  HelloWorld, ThrowCatch, DeepEh    (green)
  known-broken:     Finalization                      (no host loop)

Usage:

  ./src/coreclr/wasi/tests/run-tests.sh            # default suite
  ./src/coreclr/wasi/tests/run-tests.sh HelloWorld # one test
  ./src/coreclr/wasi/tests/run-tests.sh --list     # enumerate

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 1, 2026 19:22

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 79 out of 79 changed files in this pull request and generated 4 comments.

Comment thread src/coreclr/wasi/tests/run-tests.sh
Comment thread src/coreclr/pal/src/include/pal/wasi/pal_wasi_missing.h
Comment thread src/coreclr/wasi/tests/known-broken/README.md Outdated
Comment thread src/coreclr/wasi/tests/run.cs Outdated
Comment thread src/coreclr/vm/finalizerthread.cpp Outdated
@lewing

lewing commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

🎉 First fully green CoreCLR-WASI runtime tests on CI!

Build #20260701.102 (id=1490402), leg wasi-wasm linux Release CoreCLR_WASI_RuntimeTests:

  • 19 Helix work items dispatched, all completed successfully in ~5 min end-to-end
  • 3,498 tests total · 3,489 passed · 0 failed · 9 skipped
  • Helix job: ea4375cc-20e8-43f0-b8c0-5aead430bf2d

Work items exercised (merged runners): JIT.CodeGenBringUpTests_{d,do,r,ro}, JIT.Generics, Directed_{1,2,3}, Methodical_{d1,d2,do,others,r1,r2,ro}, jit64_{1,3,4,5}, JIT.opt, Loader, LoaderClassloaderGenerics, LoaderClassloaderRegressions, Exceptions, reflection.

Full path validated end-to-end: build → merged-runner discovery → helixpublishwitharcade.proj dispatch → wasmtime staged into Helix correlation payload → coreutils timeout wraps wasmtime → tests execute → xunit trx returned to AzDO.

lewing and others added 3 commits July 1, 2026 16:52
…review)

Non-WASM FinalizerThreadWorkerIteration doesn't swallow user finalizer
exceptions -- unhandled exceptions FailFast through
INSTALL_UNHANDLED_MANAGED_EXCEPTION_TRAP. My earlier EX_CATCH masked
that contract on WASM and hid real bugs.

Remove the outer EX_TRY/EX_CATCH; keep the INSTALL_UNHANDLED_MANAGED_EXCEPTION_TRAP
so unhandled managed exceptions from finalizers reach the runtime's
FailFast path exactly like every other target.

Finalizer smoke test still passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- cgencpu.h: drop TARGET_WASI-specific elif so GetCurrentSP() has an
  unconditional fallback return for any non-browser wasm target
- run-tests.sh: correct header comment (sentinel-based, not exit-100),
  remove undocumented COREROOT env override, replace ineffective
  trap-on-RETURN with explicit per-iteration staging cleanup
- run.cs: drop unused relPath / wasmtimePath parameters from
  RunOneAsync and its caller; fix WASMTIME probe-order comment to
  match ProbeWasmtime ($WASMTIME > ~/.wasmtime > $PATH)
- README.md: fix WASMTIME probe-order description to match the code
- known-broken/README.md: promote Finalization to smoke/ (it now
  passes after AsyncHelpers.Wasi + wasi:clocks + WasiEventLoop
  bring-up); drop stale Task.Wait rationale

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The unconditional import in eng/native.wasm.targets was fatal for
browser-wasm CoreCLR_WasmBuildTests: the Helix WBT correlation payload
ships AcquireEmscriptenSdk.targets but not AcquireWasiSdk.targets, so
every WasmBasicTestApp.csproj build died at MSBuild import evaluation
with MSB4019 before restore emitted a single object file (~95 identical
failures per run across both linux and windows WBT legs).

AcquireWasiSdk.targets itself is already internally guarded on
TargetsWasi=true, so restricting the import site to the same gate is
equivalent for real WASI builds. The additional Exists() check is a
belt-and-braces guard against future correlation-payload drift.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 1, 2026 23:13

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 79 out of 79 changed files in this pull request and generated 3 comments.

Comment thread src/coreclr/pal/src/file/file.cpp
Comment thread src/coreclr/vm/wasm/cgencpu.h
Comment thread src/tests/Common/helixpublishwitharcade.proj
…ore wasmtime stage

- pal/src/file/file.cpp: On WASI, init_std_handle borrows fileno(stream) for
  stdin/stdout/stderr because WASI has no dup(). Guard both the error-path
  close(new_fd) in init_std_handle and the close(unix_fd) in
  FileCleanupRoutine so a partial init failure or non-shutdown object
  release cannot close the real std fd out from under the process. The
  fShutdown branch of cleanup already skipped close, but object refcount
  releases outside shutdown were not covered.

- helixpublishwitharcade.proj: MakeDir $(WasmtimeDirForHelixPayload)
  before the Copy that stages wasmtime. Matches the pattern in
  sendtohelixhelp.proj and avoids intermittent failures when the
  destination directory tree hasn't been created yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 2, 2026 16:48

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 79 out of 79 changed files in this pull request and generated 3 comments.

Comment thread src/coreclr/vm/finalizerthread.cpp Outdated
Comment thread src/coreclr/pal/src/include/pal/wasi/pal_wasi_missing.h
Comment thread src/coreclr/wasi/tests/run.cs
lewing and others added 2 commits July 2, 2026 12:54
…ing QCall

- finalizerthread.cpp: Split the shared browser/WASI finalizer callback
  into an inline helper RunFinalizerIterationOnCurrentThread() plus
  two thin entry-point shells: SystemJS_ExecuteFinalizationCallback
  (browser, invoked from a JS microtask — not a QCall) and
  WasiFinalizer_RunWorker (WASI, invoked from managed WasiEventLoop
  via [LibraryImport(RuntimeHelpers.QCall)]). The WASI variant now
  uses the canonical QCALLTYPE + QCALL_CONTRACT + BEGIN_QCALL/END_QCALL
  pattern required for QCall entry points per src/coreclr/vm/qcall.h.
  Local smoke sweep (AsyncMain + Finalization + Finalizer) still 3/3.

- run.cs: Normalize coreRootAbs with a trailing directory separator
  before the StartsWith prefix filter so a Core_Root sibling directory
  that shares a prefix (e.g., Core_Root_something) isn't accidentally
  filtered out.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fied)

self_override5 now passes locally after merging main into the bring-up
branch (dfbb581) — dotnet#126124 ("[wasm] Fix interpreter crash with
MethodImpl .override on PortableEntryPoints") is in the merged history
and the local sweep no longer trips the trap.

The exclusion entry itself was already removed in an earlier revert
pass; only the stale parenthetical note remained. Drop it.

Tracked in dotnet#130064 (comment).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 2, 2026 20:02

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 79 out of 79 changed files in this pull request and generated 4 comments.

Comment thread src/coreclr/wasi/tests/provision-wasmtime.proj Outdated
Comment thread src/coreclr/wasi/tests/README.md Outdated
Comment thread src/coreclr/wasi/tests/run.cs Outdated
Comment thread src/coreclr/wasi/tests/run-tests.sh
lewing and others added 2 commits July 2, 2026 16:01
…romContextInternal

Both branches were a dead-noise assert with basically identical
messages ("...not implemented on wasm" vs "...not implemented on
wasi"). Neither is ever hit in the WASI or browser CoreCLR
interpreter execution model — the entrypoint only exists because the
Windows-shaped signature is referenced elsewhere in the PAL surface.

Collapse back to a single \_ASSERT and drop the misleading "WASI uses
native wasm exceptions" comment (nothing in this file dispatches to
native wasm EH; that plumbing lives in `src/coreclr/vm/wasm/`).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- provision-wasmtime.proj: comment on InstallWasmtimeForTests overstated
  what the flag does. ProvisionWasmtime is gated on !Exists($(WasmtimeDir)),
  so the flag never forces a re-provision when the dir already exists.
  Rewrite the comment to describe the actual behavior (delete the dir to
  force re-provision).
- README.md: exit marker corerun emits is 'WASM EXIT <n>' (under
  DOTNET_WASI_PRINT_EXIT_CODE=1), not 'CORERUN_EXIT_CODE'. Match reality.
- run.cs: Rel() now normalizes to forward slashes so --list output and
  known-failures.txt matching are identical on Windows and Unix.

Not applying: Copilot's claim that run-tests.sh:65 'sort -z' breaks on
macOS is a false positive — BSD sort ships with -z (visible in the
usage synopsis and confirmed by `printf 'b\0a\0' | sort -z`).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-PAL-coreclr only for closed issues os-wasi Related to WASI variant of arch-wasm

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants