[wasm][wasi] CoreCLR wasi draft#130051
Draft
lewing wants to merge 66 commits into
Draft
Conversation
Member
Author
|
@copilot resolve the merge conflicts in this pull request |
…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>
jkotas
reviewed
Jul 1, 2026
Member
Author
|
🎉 First fully green CoreCLR-WASI runtime tests on CI! Build #20260701.102 (id=1490402), leg
Work items exercised (merged runners): Full path validated end-to-end: build → merged-runner discovery → |
…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>
…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>
3 tasks
…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>
…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>
This was referenced Jul 3, 2026
This was referenced Jul 5, 2026
lewing
added a commit
to lewing/runtimelab
that referenced
this pull request
Jul 6, 2026
On wasi-wasm, AsyncHelpers.NonBrowser.cs routes the compiler-generated
async entry point through a blocking Task.GetAwaiter().GetResult(). WASI
is single-threaded (!IsMultithreadingSupported), so that wait throws
PlatformNotSupportedException and `async Task Main` fails at runtime.
Add a WASI-specific AsyncHelpers partial that routes the entry point
through the WASI event loop
(WasiEventLoop.PollWasiEventLoopUntilResolved{,Void}), mirroring
dotnet/runtime#130051. The change is entirely managed and
codegen-agnostic; WasiEventLoop.PollWasiEventLoopUntilResolved{,Void}
already exist on this branch.
- AsyncHelpers.Wasi.cs (new): routes HandleAsyncEntryPoint(Task) /
HandleAsyncEntryPoint(Task<int>) through the WASI event loop.
- System.Private.CoreLib.Shared.projitems: compile AsyncHelpers.Wasi.cs
when TargetsWasi, exclude AsyncHelpers.NonBrowser.cs on WASI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.