From dda55357de922960fcee46d8cbd2aede784c40c0 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 14:54:07 -0500 Subject: [PATCH 01/12] [wasi] Add CoreCLR-WASI corehost (wasihost) Stand up a proper, statically-linked corehost for the CoreCLR-WASI library-test leg, mirroring browserhost and the static apphost, so the leg can run on a shipping host archive instead of relinking the coreclr-internal corerun. corerun keeps its role for CoreCLR runtime tests. New src/native/corehost/wasihost/ produces libWasiHost.a (OUTPUT_NAME WasiHost), a self-contained STATIC host archive installed to sharedFramework. It is linked per-app (whole-archive) by WasiApp.CoreCLR.targets together with the runtime-pack static libraries and the app-generated P/Invoke callhelpers. The host is thin (browserhost-style): it builds the CoreCLR init properties from CORE_ROOT, sets host_runtime_contract.pinvoke_override to the app-generated callhelpers_pinvoke_override (coreclr_initialize forwards it to PInvokeOverride::SetPInvokeOverride, the same registration corerun performs), preloads icudt.dat via a weak GlobalizationNative_LoadICUData, and runs coreclr_initialize / coreclr_execute_assembly with a real wasi:cli/run main(). It reuses the corerun pal header for CORE_ROOT/TPA/path handling so assembly discovery stays identical to the corerun-based host. Build enablement: - corehost/CMakeLists.txt: add a CLR_CMAKE_TARGET_WASI branch that builds only wasihost (no hostmisc dependency). - corehost.proj: acquire the wasi-sdk and pass WASI_SDK_PATH to the native build; stage libWasiHost.a into the runtime pack native dir. - eng/liveBuilds.targets: package libWasiHost.a from HostSharedFrameworkDir for wasi CoreCLR (mirrors the browser libBrowserHost.a entry). - Directory.Build.props: add the libWasiHost.a platform manifest entry. Foundation host for the CoreCLR-WASI library-test leg (#130745). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- eng/liveBuilds.targets | 15 +- .../Directory.Build.props | 1 + src/native/corehost/CMakeLists.txt | 8 +- src/native/corehost/corehost.proj | 27 +- src/native/corehost/wasihost/CMakeLists.txt | 48 +++ src/native/corehost/wasihost/config.h.in | 10 + src/native/corehost/wasihost/configure.cmake | 15 + src/native/corehost/wasihost/wasihost.cpp | 323 ++++++++++++++++++ 8 files changed, 440 insertions(+), 7 deletions(-) create mode 100644 src/native/corehost/wasihost/CMakeLists.txt create mode 100644 src/native/corehost/wasihost/config.h.in create mode 100644 src/native/corehost/wasihost/configure.cmake create mode 100644 src/native/corehost/wasihost/wasihost.cpp diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 5f71357d1e165e..2f4c9beaeb4d6f 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -325,10 +325,17 @@ IsNative="true" /> - + + + + diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index bd717001f5e476..3720e00b56f0c7 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -282,6 +282,7 @@ + diff --git a/src/native/corehost/CMakeLists.txt b/src/native/corehost/CMakeLists.txt index 180994d6ae86ab..f95b7a051557a0 100644 --- a/src/native/corehost/CMakeLists.txt +++ b/src/native/corehost/CMakeLists.txt @@ -81,7 +81,7 @@ if(CLR_CMAKE_TARGET_LINUX OR CLR_CMAKE_TARGET_SUNOS) add_link_options(LINKER:-Bsymbolic) endif() -if(NOT CLR_CMAKE_TARGET_BROWSER) +if(NOT CLR_CMAKE_TARGET_BROWSER AND NOT CLR_CMAKE_TARGET_WASI) add_library(fxr_resolver INTERFACE) target_sources(fxr_resolver INTERFACE fxr_resolver.c) target_include_directories(fxr_resolver INTERFACE fxr) @@ -112,9 +112,13 @@ if(NOT CLR_CMAKE_TARGET_BROWSER) if(CLR_CMAKE_BUILD_HOST_TESTS) add_subdirectory(test) endif() -else() +elseif(CLR_CMAKE_TARGET_BROWSER) add_subdirectory(hostmisc) add_subdirectory(browserhost) +else() # CLR_CMAKE_TARGET_WASI + # The wasi host is a self-contained static archive (libWasiHost.a) linked per-app; it does not + # depend on hostmisc (it shares the corerun pal header instead), so only wasihost is built here. + add_subdirectory(wasihost) endif() # If there's a dynamic ASAN runtime, then install it in the directories where we put our executables. diff --git a/src/native/corehost/corehost.proj b/src/native/corehost/corehost.proj index cb8d246824a806..c1bb87a03fef44 100644 --- a/src/native/corehost/corehost.proj +++ b/src/native/corehost/corehost.proj @@ -14,6 +14,10 @@ GenerateRuntimeVersionFile $(BuildCoreHostDependsOn);InitializeSourceControlInformationFromSourceControlManager AcquireEmscriptenSdk;$(BuildCoreHostDependsOn);GenerateEmccExports;ResolveRuntimeFilesFromLocalBuild + + AcquireWasiSdk;$(BuildCoreHostDependsOn) BuildCoreHostOnWindows BuildCoreHostOnUnix $(ArtifactsObjDir)$(TargetRid).$(Configuration)\ @@ -92,12 +96,19 @@ $(BuildArgs) -cmakeargs "-DCLR_CMAKE_BUILD_HOST_PRODUCT=$(BuildNativeHostProduct.ToUpper())" + + + <_CoreHostBuildEnvironmentVariables>WASI_SDK_PATH=$(RuntimeBuildWasiSdkPath) + + - + + + + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(HostSharedFrameworkDir)libWasiHost.a" /> + + + + + diff --git a/src/native/corehost/wasihost/CMakeLists.txt b/src/native/corehost/wasihost/CMakeLists.txt new file mode 100644 index 00000000000000..0d3faad1427727 --- /dev/null +++ b/src/native/corehost/wasihost/CMakeLists.txt @@ -0,0 +1,48 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the MIT license. + +project(wasihost) +set(DOTNET_PROJECT_NAME "WasiHost-Static") + +# HAVE_DIRENT_D_TYPE / HAVE_GETAUXVAL for the shared corerun pal header. +include(configure.cmake) + +set(WASIHOST_SOURCES + ./wasihost.cpp +) + +# Reuse the corerun pal (path handling, CORE_ROOT/TPA directory enumeration, absolute-path +# resolution) so the host's assembly discovery stays identical to the validated corerun-based host. +# The header is self-contained (only and minipal headers), so no corerun runtime object +# is linked into this archive. +set(WASIHOST_CORERUN_DIR ${CLR_REPO_ROOT_DIR}/src/coreclr/hosts/corerun) + +add_library(WasiHost-Static + STATIC + ${WASIHOST_SOURCES} +) +set_target_properties(WasiHost-Static PROPERTIES OUTPUT_NAME WasiHost CLEAN_DIRECT_OUTPUT 1) + +# The shared corerun pal header compiles the wasi paths against the emulated wasi-libc facilities +# (mmap/getpid/signal/process-clocks). The coreclr build sets these globally for wasi; the corehost +# build does not, so set them here. The matching -lwasi-emulated-* libraries are supplied at the +# per-app relink (src/mono/wasi/build/WasiApp.CoreCLR.targets). +if (CLR_CMAKE_TARGET_WASI) + target_compile_definitions(WasiHost-Static PRIVATE + _WASI_EMULATED_MMAN + _WASI_EMULATED_GETPID + _WASI_EMULATED_SIGNAL + _WASI_EMULATED_PROCESS_CLOCKS) +endif() + +target_include_directories(WasiHost-Static PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} # generated config.h + ${WASIHOST_CORERUN_DIR} # corerun.hpp (shared pal) + ${CLR_SRC_NATIVE_DIR} # minipal/*.h +) + +# The runtime static libraries (coreclr_static, System.Native, minipal, ...) and the app-generated +# callhelpers are NOT linked here; they are supplied at the per-app relink from the runtime pack +# (src/mono/wasi/build/WasiApp.CoreCLR.targets), which pulls this archive whole-archive so +# main()/_start are included. +install(TARGETS WasiHost-Static DESTINATION sharedFramework COMPONENT runtime) diff --git a/src/native/corehost/wasihost/config.h.in b/src/native/corehost/wasihost/config.h.in new file mode 100644 index 00000000000000..0c2e459443b136 --- /dev/null +++ b/src/native/corehost/wasihost/config.h.in @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __CONFIG_H__ +#define __CONFIG_H__ + +#cmakedefine01 HAVE_GETAUXVAL +#cmakedefine01 HAVE_DIRENT_D_TYPE + +#endif // __CONFIG_H__ diff --git a/src/native/corehost/wasihost/configure.cmake b/src/native/corehost/wasihost/configure.cmake new file mode 100644 index 00000000000000..a24a6e69d05b3e --- /dev/null +++ b/src/native/corehost/wasihost/configure.cmake @@ -0,0 +1,15 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the MIT license. + +# Mirrors src/coreclr/hosts/corerun/configure.cmake: the shared corerun pal header (corerun.hpp) +# consumes HAVE_DIRENT_D_TYPE / HAVE_GETAUXVAL from this generated config.h. The corehost build +# does not include the CMake check modules globally (unlike the coreclr build), so include them. +include(CheckSymbolExists) +include(CheckStructHasMember) + +check_symbol_exists(getauxval sys/auxv.h HAVE_GETAUXVAL) +check_struct_has_member ("struct dirent" d_type dirent.h HAVE_DIRENT_D_TYPE) + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in + ${CMAKE_CURRENT_BINARY_DIR}/config.h) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp new file mode 100644 index 00000000000000..641e155f613914 --- /dev/null +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -0,0 +1,323 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// CoreCLR-WASI host. +// +// This is the shipping corehost for the CoreCLR-WASI library-test leg. It is built as a +// static archive (libWasiHost.a) and linked per-app by the WASI app builder +// (src/mono/wasi/build/WasiApp.CoreCLR.targets), whole-archive so main()/_start are pulled, +// together with the runtime-pack static libraries (libcoreclr_static.a, libSystem.Native.a, +// ...) and the app-generated P/Invoke callhelpers. This mirrors the browser host +// (src/native/corehost/browserhost) and the static apphost (src/native/corehost/apphost/static); +// the coreclr-internal corerun (src/coreclr/hosts/corerun) keeps its role for CoreCLR runtime +// tests. +// +// The host is a thin one: it constructs the CoreCLR initialization properties from CORE_ROOT +// and calls coreclr_initialize / coreclr_execute_assembly directly (declared extern against the +// statically linked runtime), the same shape as browserhost.cpp but with a real wasi:cli/run +// main() (there is no JS driver). The path/TPA logic is shared with corerun via its pal header so +// the CORE_ROOT resolution, directory enumeration and absolute-path handling stay identical to the +// validated corerun-based host. +// +// See https://github.com/dotnet/runtime/issues/130129. +// + +#include +#include +#include +#include + +// Shared pal (path handling, directory enumeration, CORE_ROOT/TPA helpers). The header is +// self-contained and TARGET_WASM-guarded; only header-only helpers are used here, so no corerun +// runtime object is required in this archive. +#include "corerun.hpp" + +#include + +using pal::char_t; +using pal::string_t; + +namespace envvar +{ + const char_t* const coreRoot = W("CORE_ROOT"); + const char_t* const coreLibraries = W("CORE_LIBRARIES"); + const char_t* const printExitCode = W("DOTNET_WASI_PRINT_EXIT_CODE"); +} + +// CoreCLR entry points. On wasi the runtime is statically linked, so these are resolved at the +// per-app relink from libcoreclr_static.a (declared extern, as browserhost.cpp does) rather than +// looked up dynamically. +extern "C" +{ + int coreclr_initialize( + const char* exePath, + const char* appDomainFriendlyName, + int propertyCount, + const char** propertyKeys, + const char** propertyValues, + void** hostHandle, + unsigned int* domainId); + + int coreclr_execute_assembly( + void* hostHandle, + unsigned int domainId, + int argc, + const char** argv, + const char* managedAssemblyPath, + unsigned int* exitCode); + + int coreclr_shutdown_2( + void* hostHandle, + unsigned int domainId, + int* latchedExitCode); + + int coreclr_set_error_writer(void (*errorWriter)(const char* line)); +} + +// The app-generated P/Invoke resolver, produced per-app by ManagedToNativeGenerator and linked at +// the relink (replacing libcoreclr_gen_static.a). Passed to the runtime via the host contract +// below (pinvoke_override); coreclr_initialize forwards it to PInvokeOverride::SetPInvokeOverride +// with Source::RuntimeConfiguration - the same registration corerun performs via +// add_pinvoke_override() - so the app callhelpers are hooked and reverse thunks for the app/test +// [UnmanagedCallersOnly] methods resolve (else precode_portable.cpp asserts). See +// coreclr_initialize in src/coreclr/dlls/mscoree/exports.cpp. Declared as a plain (C++-mangled) +// function to match the generated definition, as corerun/browserhost do - not extern "C". +const void* callhelpers_pinvoke_override(const char* library_name, const char* entry_point_name); + +// Fake implementations to satisfy the linker without pulling +// libSystem.Runtime.InteropServices.JavaScript.Native (a browser-only library) into the wasi +// relink; these JS interop QCall targets are referenced by libcoreclr_static.a but never called on +// wasi. Ported from src/coreclr/hosts/corerun/wasm/pinvoke_override.cpp. +extern "C" +{ + void* SystemInteropJS_BindJSImportST(void*) { std::abort(); } + void SystemInteropJS_CancelPromise(void*) { std::abort(); } + void SystemInteropJS_InvokeJSFunction(void*, void*) { std::abort(); } + void SystemInteropJS_InvokeJSImportST(int32_t, void*) { std::abort(); } + void SystemInteropJS_ReleaseCSOwnedObject(void*) { std::abort(); } + void SystemInteropJS_ResolveOrRejectPromise(void*) { std::abort(); } +} + +// Provided by libSystem.Globalization.Native.a when globalization is linked (the non-invariant +// per-app relink). Declared weak so an invariant relink (which omits that archive) leaves the +// reference null and the host ICU preload below is skipped. +extern "C" __attribute__((weak)) int32_t GlobalizationNative_LoadICUData(const char* path); + +// Initialization properties, kept alive for the lifetime of the process so the host runtime +// contract callback can serve them. +static std::vector s_property_keys; +static std::vector s_property_values; + +static void log_error_info(const char* line) +{ + std::fprintf(stderr, "%s\n", line); +} + +// N.B. CoreCLR doesn't always use the first instance of an assembly on the TPA list (ni's may be +// preferred over il even if they appear later). Include only the first instance of a simple name. +static string_t build_tpa(const string_t& core_root, const string_t& core_libraries) +{ + static const char_t* const tpa_extensions[] = + { + W(".dll"), + W(".exe"), + nullptr + }; + + std::set name_set; + pal::stringstream_t tpa_list; + + for (const char_t* const* curr_ext = tpa_extensions; *curr_ext != nullptr; ++curr_ext) + { + const char_t* ext = *curr_ext; + const size_t ext_len = pal::strlen(ext); + + for (const string_t& dir : { core_libraries, core_root }) + { + if (dir.empty()) + continue; + + string_t tmp = pal::build_file_list(dir, ext, [&](const char_t* file) + { + string_t file_local{ file }; + + if (pal::string_ends_with(file_local, ext_len, ext)) + file_local = file_local.substr(0, file_local.length() - ext_len); + + return name_set.insert(file_local).second; + }); + + tpa_list << tmp; + } + } + + return tpa_list.str(); +} + +static size_t HOST_CONTRACT_CALLTYPE get_runtime_property( + const char* key, + /*out*/ char* value_buffer, + size_t value_buffer_size, + void* /*contract_context*/) +{ + for (size_t i = 0; i < s_property_keys.size(); ++i) + { + if (s_property_keys[i] == key) + { + const std::string& value = s_property_values[i]; + size_t len = value.length(); + if (value_buffer != nullptr && value_buffer_size > len) + ::memcpy(value_buffer, value.c_str(), len + 1); + + return len + 1; + } + } + + return (size_t)-1; +} + +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + std::fprintf(stderr, "USAGE: %s [arguments]\n", argc > 0 ? argv[0] : "wasihost"); + return -1; + } + + string_t exe_path = pal::get_exe_path(); + + // The first argument is the managed entry assembly; the rest are passed to it. + string_t entry_assembly = pal::get_absolute_path(argv[1]); + int entry_argc = argc - 2; + const char** entry_argv = entry_argc > 0 ? (const char**)&argv[2] : nullptr; + + // The application directory is where the entry assembly lives. + string_t app_path; + { + string_t file; + pal::split_path_to_dir_filename(entry_assembly, app_path, file); + pal::ensure_trailing_delimiter(app_path); + } + + pal::stringstream_t native_search_dirs; + native_search_dirs << app_path << pal::env_path_delim; + + string_t core_libs = pal::getenv(envvar::coreLibraries); + if (!core_libs.empty() && core_libs != app_path) + { + pal::ensure_trailing_delimiter(core_libs); + native_search_dirs << core_libs << pal::env_path_delim; + } + + // CORE_ROOT locates the framework assemblies (and, on non-wasm, the runtime). On wasi the + // runtime is statically linked, so CORE_ROOT is only used to build the TPA list. Fall back to + // the host's own directory when unset. + string_t core_root = pal::getenv(envvar::coreRoot); + if (core_root.empty()) + { + string_t file; + pal::split_path_to_dir_filename(exe_path, core_root, file); + } + pal::ensure_trailing_delimiter(core_root); + native_search_dirs << core_root << pal::env_path_delim; + + string_t tpa_list = build_tpa(core_root, core_libs); + + s_property_keys.push_back("TRUSTED_PLATFORM_ASSEMBLIES"); + s_property_values.push_back(tpa_list); + + s_property_keys.push_back("APP_PATHS"); + s_property_values.push_back(app_path); + + s_property_keys.push_back("NATIVE_DLL_SEARCH_DIRECTORIES"); + s_property_values.push_back(native_search_dirs.str()); + + host_runtime_contract host_contract = { + sizeof(host_runtime_contract), + nullptr, + &get_runtime_property, + nullptr, + &callhelpers_pinvoke_override }; + { + std::stringstream ss; + ss << "0x" << std::hex << (size_t)(&host_contract); + s_property_keys.push_back(HOST_PROPERTY_RUNTIME_CONTRACT); + s_property_values.push_back(ss.str()); + } + + std::vector property_keys; + std::vector property_values; + for (const std::string& key : s_property_keys) + property_keys.push_back(key.c_str()); + for (const std::string& value : s_property_values) + property_values.push_back(value.c_str()); + + coreclr_set_error_writer(log_error_info); + + void* host_handle = nullptr; + unsigned int domain_id = 0; + int result = coreclr_initialize( + exe_path.c_str(), + "wasihost", + (int)property_keys.size(), + property_keys.data(), + property_values.data(), + &host_handle, + &domain_id); + if (result < 0) + { + std::fprintf(stderr, "coreclr_initialize failed - Error: 0x%08x\n", result); + return -1; + } + + coreclr_set_error_writer(nullptr); + + // The static ICU shim requires the host to preload icudt.dat before managed globalization + // initializes: GlobalizationNative_LoadICU() (the no-path entry the managed side calls on wasi) + // returns 0 unless the data was already set, and falls back to invariant. This mirrors the + // browser JS host calling wasm_load_icu_data. GlobalizationNative_LoadICUData is only linked + // when globalization is enabled (the non-invariant per-app relink), so the reference is weak - + // when globalization is not linked it is null and this is skipped (invariant). A missing + // icudt.dat also falls back to invariant (the call just fails). + if (GlobalizationNative_LoadICUData != nullptr) + { + string_t icu_data_path = app_path; + icu_data_path.append(W("icudt.dat")); + GlobalizationNative_LoadICUData(icu_data_path.c_str()); + } + + int exit_code = 0; + result = coreclr_execute_assembly( + host_handle, + domain_id, + entry_argc, + entry_argv, + entry_assembly.c_str(), + (unsigned int*)&exit_code); + if (result < 0) + { + std::fprintf(stderr, "coreclr_execute_assembly failed - Error: 0x%08x\n", result); + return -1; + } + + int latched_exit_code = exit_code; + int shutdown_result = coreclr_shutdown_2(host_handle, domain_id, &latched_exit_code); + if (shutdown_result < 0) + { + std::fprintf(stderr, "coreclr_shutdown_2 failed - Error: 0x%08x\n", shutdown_result); + latched_exit_code = -1; + } + + // wasi:cli/exit's exit(status: result) only signals ok/err, so wasmtime collapses any non-zero + // Main return to host exit 1. When DOTNET_WASI_PRINT_EXIT_CODE=1, emit a "WASM EXIT " marker + // on stderr matching Mono (src/mono/wasi/runtime/main.c); the WASI launcher recovers the value + // from it. wasi:cli/exit.exit-with-code(status-code: u8) is stable as of WASI 0.3.0/Preview 3 + // (@since(0.3.0)), but this host targets wasip2, where that world still gates it behind + // @unstable(feature = cli-exit-with-code); adopting it (and dropping this marker + the launcher + // parser) is gated on moving to a wasip3 target and toolchain support. See corerun.cpp. + if (pal::getenv(envvar::printExitCode) == W("1")) + std::fprintf(stderr, "WASM EXIT %d\n", latched_exit_code); + + return latched_exit_code; +} From a334566ac757763e7e14106e12f8623a9dcec440 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 15:31:48 -0500 Subject: [PATCH 02/12] Make host_runtime_contract static in wasihost The runtime contract must remain valid for the process lifetime (the runtime may call back through it any time after coreclr_initialize), and its address is handed to the runtime via HOST_RUNTIME_CONTRACT. Storing it in automatic storage risked use-after-scope under refactoring; make it static, mirroring browserhost. Addresses PR review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- src/native/corehost/wasihost/wasihost.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index 641e155f613914..ddc6ad581a2d72 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -233,7 +233,10 @@ int main(int argc, char* argv[]) s_property_keys.push_back("NATIVE_DLL_SEARCH_DIRECTORIES"); s_property_values.push_back(native_search_dirs.str()); - host_runtime_contract host_contract = { + // Static storage: the contract must stay valid for the process lifetime (the runtime may call + // back through it any time after coreclr_initialize), and its address is handed to the runtime + // via HOST_RUNTIME_CONTRACT below. Mirrors browserhost's static host_contract. + static host_runtime_contract host_contract = { sizeof(host_runtime_contract), nullptr, &get_runtime_property, From e453f5472611a8bc5b230eb058f0ac780cb0b8ca Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 15:35:11 -0500 Subject: [PATCH 03/12] Trim wasihost comments to match repo conventions The initial comments read like a design doc. Cut narration and redundant explanation, keeping only the non-obvious rationale (contract wiring, weak ICU symbol, mangled callhelper decl, static lifetime). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- src/native/corehost/wasihost/CMakeLists.txt | 18 ++-- src/native/corehost/wasihost/wasihost.cpp | 94 ++++++--------------- 2 files changed, 33 insertions(+), 79 deletions(-) diff --git a/src/native/corehost/wasihost/CMakeLists.txt b/src/native/corehost/wasihost/CMakeLists.txt index 0d3faad1427727..6349e3dfcb7bce 100644 --- a/src/native/corehost/wasihost/CMakeLists.txt +++ b/src/native/corehost/wasihost/CMakeLists.txt @@ -11,10 +11,8 @@ set(WASIHOST_SOURCES ./wasihost.cpp ) -# Reuse the corerun pal (path handling, CORE_ROOT/TPA directory enumeration, absolute-path -# resolution) so the host's assembly discovery stays identical to the validated corerun-based host. -# The header is self-contained (only and minipal headers), so no corerun runtime object -# is linked into this archive. +# Reuse the corerun pal header (path/CORE_ROOT/TPA helpers); it's header-only, so no corerun object +# is linked here. set(WASIHOST_CORERUN_DIR ${CLR_REPO_ROOT_DIR}/src/coreclr/hosts/corerun) add_library(WasiHost-Static @@ -23,10 +21,8 @@ add_library(WasiHost-Static ) set_target_properties(WasiHost-Static PROPERTIES OUTPUT_NAME WasiHost CLEAN_DIRECT_OUTPUT 1) -# The shared corerun pal header compiles the wasi paths against the emulated wasi-libc facilities -# (mmap/getpid/signal/process-clocks). The coreclr build sets these globally for wasi; the corehost -# build does not, so set them here. The matching -lwasi-emulated-* libraries are supplied at the -# per-app relink (src/mono/wasi/build/WasiApp.CoreCLR.targets). +# The coreclr build sets these wasi emulation defines globally; the corehost build does not. The +# matching -lwasi-emulated-* libraries are supplied at the per-app relink. if (CLR_CMAKE_TARGET_WASI) target_compile_definitions(WasiHost-Static PRIVATE _WASI_EMULATED_MMAN @@ -41,8 +37,6 @@ target_include_directories(WasiHost-Static PRIVATE ${CLR_SRC_NATIVE_DIR} # minipal/*.h ) -# The runtime static libraries (coreclr_static, System.Native, minipal, ...) and the app-generated -# callhelpers are NOT linked here; they are supplied at the per-app relink from the runtime pack -# (src/mono/wasi/build/WasiApp.CoreCLR.targets), which pulls this archive whole-archive so -# main()/_start are included. +# The runtime static libraries and the app-generated callhelpers are supplied at the per-app relink +# (src/mono/wasi/build/WasiApp.CoreCLR.targets), which pulls this archive whole-archive for main(). install(TARGETS WasiHost-Static DESTINATION sharedFramework COMPONENT runtime) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index ddc6ad581a2d72..e20c98124f38a9 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -1,36 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// -// CoreCLR-WASI host. -// -// This is the shipping corehost for the CoreCLR-WASI library-test leg. It is built as a -// static archive (libWasiHost.a) and linked per-app by the WASI app builder -// (src/mono/wasi/build/WasiApp.CoreCLR.targets), whole-archive so main()/_start are pulled, -// together with the runtime-pack static libraries (libcoreclr_static.a, libSystem.Native.a, -// ...) and the app-generated P/Invoke callhelpers. This mirrors the browser host -// (src/native/corehost/browserhost) and the static apphost (src/native/corehost/apphost/static); -// the coreclr-internal corerun (src/coreclr/hosts/corerun) keeps its role for CoreCLR runtime -// tests. -// -// The host is a thin one: it constructs the CoreCLR initialization properties from CORE_ROOT -// and calls coreclr_initialize / coreclr_execute_assembly directly (declared extern against the -// statically linked runtime), the same shape as browserhost.cpp but with a real wasi:cli/run -// main() (there is no JS driver). The path/TPA logic is shared with corerun via its pal header so -// the CORE_ROOT resolution, directory enumeration and absolute-path handling stay identical to the -// validated corerun-based host. -// +// Thin CoreCLR-WASI corehost, built as a static archive (libWasiHost.a) and linked per-app by the +// WASI app builder (src/mono/wasi/build/WasiApp.CoreCLR.targets) against the statically-linked +// runtime. Mirrors browserhost, but with a real wasi:cli/run main() instead of a JS driver. // See https://github.com/dotnet/runtime/issues/130129. -// #include #include #include #include -// Shared pal (path handling, directory enumeration, CORE_ROOT/TPA helpers). The header is -// self-contained and TARGET_WASM-guarded; only header-only helpers are used here, so no corerun -// runtime object is required in this archive. +// Shared pal (path handling, CORE_ROOT/TPA helpers); header-only, so no corerun object is linked. #include "corerun.hpp" #include @@ -45,9 +26,7 @@ namespace envvar const char_t* const printExitCode = W("DOTNET_WASI_PRINT_EXIT_CODE"); } -// CoreCLR entry points. On wasi the runtime is statically linked, so these are resolved at the -// per-app relink from libcoreclr_static.a (declared extern, as browserhost.cpp does) rather than -// looked up dynamically. +// Statically linked at the per-app relink, so declared extern here (as browserhost does). extern "C" { int coreclr_initialize( @@ -75,20 +54,13 @@ extern "C" int coreclr_set_error_writer(void (*errorWriter)(const char* line)); } -// The app-generated P/Invoke resolver, produced per-app by ManagedToNativeGenerator and linked at -// the relink (replacing libcoreclr_gen_static.a). Passed to the runtime via the host contract -// below (pinvoke_override); coreclr_initialize forwards it to PInvokeOverride::SetPInvokeOverride -// with Source::RuntimeConfiguration - the same registration corerun performs via -// add_pinvoke_override() - so the app callhelpers are hooked and reverse thunks for the app/test -// [UnmanagedCallersOnly] methods resolve (else precode_portable.cpp asserts). See -// coreclr_initialize in src/coreclr/dlls/mscoree/exports.cpp. Declared as a plain (C++-mangled) -// function to match the generated definition, as corerun/browserhost do - not extern "C". +// App-generated P/Invoke resolver (per-app, replaces libcoreclr_gen_static.a at the relink), passed +// to the runtime via the host contract below so app callhelpers and reverse thunks resolve. Plain +// C++ linkage to match the generated definition (not extern "C"). const void* callhelpers_pinvoke_override(const char* library_name, const char* entry_point_name); -// Fake implementations to satisfy the linker without pulling -// libSystem.Runtime.InteropServices.JavaScript.Native (a browser-only library) into the wasi -// relink; these JS interop QCall targets are referenced by libcoreclr_static.a but never called on -// wasi. Ported from src/coreclr/hosts/corerun/wasm/pinvoke_override.cpp. +// JS interop QCall targets referenced by libcoreclr_static.a but never called on wasi; stubbed so +// the relink doesn't pull the browser-only libSystem.Runtime.InteropServices.JavaScript.Native. extern "C" { void* SystemInteropJS_BindJSImportST(void*) { std::abort(); } @@ -99,13 +71,11 @@ extern "C" void SystemInteropJS_ResolveOrRejectPromise(void*) { std::abort(); } } -// Provided by libSystem.Globalization.Native.a when globalization is linked (the non-invariant -// per-app relink). Declared weak so an invariant relink (which omits that archive) leaves the -// reference null and the host ICU preload below is skipped. +// Weak: only linked (from libSystem.Globalization.Native.a) in a non-invariant relink; null and +// skipped otherwise. extern "C" __attribute__((weak)) int32_t GlobalizationNative_LoadICUData(const char* path); -// Initialization properties, kept alive for the lifetime of the process so the host runtime -// contract callback can serve them. +// Init properties, kept alive for the process so the runtime contract callback can serve them. static std::vector s_property_keys; static std::vector s_property_values; @@ -114,8 +84,8 @@ static void log_error_info(const char* line) std::fprintf(stderr, "%s\n", line); } -// N.B. CoreCLR doesn't always use the first instance of an assembly on the TPA list (ni's may be -// preferred over il even if they appear later). Include only the first instance of a simple name. +// Include only the first instance of each simple assembly name (CoreCLR may otherwise prefer a +// later ni over an earlier il). static string_t build_tpa(const string_t& core_root, const string_t& core_libraries) { static const char_t* const tpa_extensions[] = @@ -187,12 +157,11 @@ int main(int argc, char* argv[]) string_t exe_path = pal::get_exe_path(); - // The first argument is the managed entry assembly; the rest are passed to it. + // argv[1] is the managed entry assembly; argv[2..] are passed to it. string_t entry_assembly = pal::get_absolute_path(argv[1]); int entry_argc = argc - 2; const char** entry_argv = entry_argc > 0 ? (const char**)&argv[2] : nullptr; - // The application directory is where the entry assembly lives. string_t app_path; { string_t file; @@ -210,9 +179,8 @@ int main(int argc, char* argv[]) native_search_dirs << core_libs << pal::env_path_delim; } - // CORE_ROOT locates the framework assemblies (and, on non-wasm, the runtime). On wasi the - // runtime is statically linked, so CORE_ROOT is only used to build the TPA list. Fall back to - // the host's own directory when unset. + // CORE_ROOT locates the framework assemblies for the TPA list (the runtime itself is static on + // wasi). Fall back to the host's own directory when unset. string_t core_root = pal::getenv(envvar::coreRoot); if (core_root.empty()) { @@ -233,9 +201,8 @@ int main(int argc, char* argv[]) s_property_keys.push_back("NATIVE_DLL_SEARCH_DIRECTORIES"); s_property_values.push_back(native_search_dirs.str()); - // Static storage: the contract must stay valid for the process lifetime (the runtime may call - // back through it any time after coreclr_initialize), and its address is handed to the runtime - // via HOST_RUNTIME_CONTRACT below. Mirrors browserhost's static host_contract. + // Static: the contract must outlive coreclr_initialize (the runtime keeps its address). The + // pinvoke_override field is forwarded to PInvokeOverride::SetPInvokeOverride by the runtime. static host_runtime_contract host_contract = { sizeof(host_runtime_contract), nullptr, @@ -276,13 +243,9 @@ int main(int argc, char* argv[]) coreclr_set_error_writer(nullptr); - // The static ICU shim requires the host to preload icudt.dat before managed globalization - // initializes: GlobalizationNative_LoadICU() (the no-path entry the managed side calls on wasi) - // returns 0 unless the data was already set, and falls back to invariant. This mirrors the - // browser JS host calling wasm_load_icu_data. GlobalizationNative_LoadICUData is only linked - // when globalization is enabled (the non-invariant per-app relink), so the reference is weak - - // when globalization is not linked it is null and this is skipped (invariant). A missing - // icudt.dat also falls back to invariant (the call just fails). + // The static ICU shim needs icudt.dat preloaded before managed globalization inits, otherwise it + // falls back to invariant (mirrors the browser JS host's wasm_load_icu_data). Skipped for + // invariant relinks (weak symbol null) and tolerant of a missing file. if (GlobalizationNative_LoadICUData != nullptr) { string_t icu_data_path = app_path; @@ -312,13 +275,10 @@ int main(int argc, char* argv[]) latched_exit_code = -1; } - // wasi:cli/exit's exit(status: result) only signals ok/err, so wasmtime collapses any non-zero - // Main return to host exit 1. When DOTNET_WASI_PRINT_EXIT_CODE=1, emit a "WASM EXIT " marker - // on stderr matching Mono (src/mono/wasi/runtime/main.c); the WASI launcher recovers the value - // from it. wasi:cli/exit.exit-with-code(status-code: u8) is stable as of WASI 0.3.0/Preview 3 - // (@since(0.3.0)), but this host targets wasip2, where that world still gates it behind - // @unstable(feature = cli-exit-with-code); adopting it (and dropping this marker + the launcher - // parser) is gated on moving to a wasip3 target and toolchain support. See corerun.cpp. + // wasi:cli/exit's exit() only signals ok/err, so wasmtime collapses a non-zero result to host + // exit 1. Under DOTNET_WASI_PRINT_EXIT_CODE=1, emit a "WASM EXIT " marker the WASI launcher + // parses (matching Mono). exit-with-code is stable in WASI 0.3 but still @unstable in the wasip2 + // world this targets; see corerun.cpp. if (pal::getenv(envvar::printExitCode) == W("1")) std::fprintf(stderr, "WASM EXIT %d\n", latched_exit_code); From 9f9119de78184ff366f27f976794fd29345ed684 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 15:40:56 -0500 Subject: [PATCH 04/12] Address PR review: entry-arg type safety, Windows WASI_SDK_PATH, fail-loud copy - wasihost.cpp: build the entry-assembly argument slice as a std::vector instead of casting char** to const char**. - corehost.proj: pass WASI_SDK_PATH to the Windows corehost build too (BuildCoreHostOnWindows), not just the Unix path. - corehost.proj: drop the Exists() guard on CopyWasiNativeFiles so a missing libWasiHost.a fails the build (like browser's CopyWasmNativeFiles) rather than silently yielding an incomplete runtime pack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- src/native/corehost/corehost.proj | 10 +++++++--- src/native/corehost/wasihost/wasihost.cpp | 12 +++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/native/corehost/corehost.proj b/src/native/corehost/corehost.proj index c1bb87a03fef44..91e5c0fe79a99e 100644 --- a/src/native/corehost/corehost.proj +++ b/src/native/corehost/corehost.proj @@ -180,13 +180,18 @@ $(BuildArgs) -cmakeargs "-DCLR_CMAKE_BUILD_HOST_PRODUCT=$(BuildNativeHostProduct.ToUpper())" + + + <_CoreHostBuildEnvironmentVariables>WASI_SDK_PATH=$(RuntimeBuildWasiSdkPath) + + - + @@ -218,8 +223,7 @@ + SkipUnchangedFiles="true" /> 0 ? (const char**)&argv[2] : nullptr; + std::vector entry_argv; + for (int i = 2; i < argc; ++i) + entry_argv.push_back(argv[i]); string_t app_path; { @@ -257,8 +259,8 @@ int main(int argc, char* argv[]) result = coreclr_execute_assembly( host_handle, domain_id, - entry_argc, - entry_argv, + (int)entry_argv.size(), + entry_argv.data(), entry_assembly.c_str(), (unsigned int*)&exit_code); if (result < 0) From b9e887eb3275b4cfe4d5de01f1b0ae60b1682727 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 15:59:05 -0500 Subject: [PATCH 05/12] Address review: drop wasi-dead JS interop stubs, default CORE_ROOT to app dir - Remove the SystemInteropJS_* linker stubs: the wasi libcoreclr_static.a does not reference them (verified with llvm-nm; they exist in corerun only because it is shared with the browser build). Relink + run confirmed unaffected. - Replace the CORE_ROOT-empty fallback (which used get_exe_path, itself synthesized from CORE_ROOT on wasi and thus circular) with a default to the entry assembly's directory, since the wasi bundle co-locates the framework with the app. CORE_ROOT remains an optional override. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- src/native/corehost/wasihost/wasihost.cpp | 25 +++++------------------ 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index 2ae91eff9b8b34..285e6f08fd9520 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -7,7 +7,6 @@ // See https://github.com/dotnet/runtime/issues/130129. #include -#include #include #include @@ -59,18 +58,6 @@ extern "C" // C++ linkage to match the generated definition (not extern "C"). const void* callhelpers_pinvoke_override(const char* library_name, const char* entry_point_name); -// JS interop QCall targets referenced by libcoreclr_static.a but never called on wasi; stubbed so -// the relink doesn't pull the browser-only libSystem.Runtime.InteropServices.JavaScript.Native. -extern "C" -{ - void* SystemInteropJS_BindJSImportST(void*) { std::abort(); } - void SystemInteropJS_CancelPromise(void*) { std::abort(); } - void SystemInteropJS_InvokeJSFunction(void*, void*) { std::abort(); } - void SystemInteropJS_InvokeJSImportST(int32_t, void*) { std::abort(); } - void SystemInteropJS_ReleaseCSOwnedObject(void*) { std::abort(); } - void SystemInteropJS_ResolveOrRejectPromise(void*) { std::abort(); } -} - // Weak: only linked (from libSystem.Globalization.Native.a) in a non-invariant relink; null and // skipped otherwise. extern "C" __attribute__((weak)) int32_t GlobalizationNative_LoadICUData(const char* path); @@ -155,8 +142,6 @@ int main(int argc, char* argv[]) return -1; } - string_t exe_path = pal::get_exe_path(); - // argv[1] is the managed entry assembly; argv[2..] are passed to it. Copy the slice into a // const char* vector rather than casting char** to const char**. string_t entry_assembly = pal::get_absolute_path(argv[1]); @@ -182,16 +167,16 @@ int main(int argc, char* argv[]) } // CORE_ROOT locates the framework assemblies for the TPA list (the runtime itself is static on - // wasi). Fall back to the host's own directory when unset. + // wasi). On wasi the bundle co-locates the framework with the entry assembly, so default to the + // entry assembly's directory; CORE_ROOT remains an optional override. string_t core_root = pal::getenv(envvar::coreRoot); if (core_root.empty()) - { - string_t file; - pal::split_path_to_dir_filename(exe_path, core_root, file); - } + core_root = app_path; pal::ensure_trailing_delimiter(core_root); native_search_dirs << core_root << pal::env_path_delim; + string_t exe_path = pal::get_exe_path(); + string_t tpa_list = build_tpa(core_root, core_libs); s_property_keys.push_back("TRUSTED_PLATFORM_ASSEMBLIES"); From 1b59189ee85bd77e1e81718c15d1a433f86e8bc2 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 16:01:06 -0500 Subject: [PATCH 06/12] Address review: fix host-build ordering on Windows, drop dead-path refs - CopyWasiNativeFiles: depend on both BuildCoreHostOnUnix and BuildCoreHostOnWindows (one is skipped by its HostOS condition) instead of $(CopyWasmNativeFilesDependsOn), which selects the Unix target for wasi and would leave the copy unordered against the host build on a Windows host. - Replace references to the not-yet-present src/mono/wasi/build/WasiApp.CoreCLR.targets path (it lands with the consumer leg) with "the CoreCLR-WASI app builder". Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- eng/liveBuilds.targets | 2 +- src/native/corehost/corehost.proj | 8 +++++--- src/native/corehost/wasihost/CMakeLists.txt | 2 +- src/native/corehost/wasihost/wasihost.cpp | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 2f4c9beaeb4d6f..a474069f8d6e2d 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -329,7 +329,7 @@ $(CoreCLRSharedFrameworkDir)) and the libs.native static libs (already provided by the unconditional glob above). corerun.wasm is not part of the runtime pack. The host archive (libWasiHost.a) is a separate deliverable built by the host subset and staged from - $(HostSharedFrameworkDir); the per-app relink (WasiApp.CoreCLR.targets) links it. Runtime + $(HostSharedFrameworkDir); the per-app relink (the CoreCLR-WASI app builder) links it. Runtime tests for Helix are built without the host subset, so the host archive is excluded there (mirrors the browser libBrowserHost.a handling). --> diff --git a/src/native/corehost/corehost.proj b/src/native/corehost/corehost.proj index 91e5c0fe79a99e..3ed9077121c66f 100644 --- a/src/native/corehost/corehost.proj +++ b/src/native/corehost/corehost.proj @@ -214,9 +214,11 @@ - + per-app relink (the CoreCLR-WASI app builder) can consume it. Mirrors the browser + CopyWasmNativeFiles above; the runtime .a libraries come from the coreclr build. Depends on + both host-build targets explicitly (one is skipped by its HostOS condition) so the copy runs + after the archive is produced regardless of host OS. --> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(HostSharedFrameworkDir)libWasiHost.a" /> diff --git a/src/native/corehost/wasihost/CMakeLists.txt b/src/native/corehost/wasihost/CMakeLists.txt index 6349e3dfcb7bce..284835cfc6eb0c 100644 --- a/src/native/corehost/wasihost/CMakeLists.txt +++ b/src/native/corehost/wasihost/CMakeLists.txt @@ -38,5 +38,5 @@ target_include_directories(WasiHost-Static PRIVATE ) # The runtime static libraries and the app-generated callhelpers are supplied at the per-app relink -# (src/mono/wasi/build/WasiApp.CoreCLR.targets), which pulls this archive whole-archive for main(). +# by the CoreCLR-WASI app builder, which pulls this archive whole-archive for main(). install(TARGETS WasiHost-Static DESTINATION sharedFramework COMPONENT runtime) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index 285e6f08fd9520..7a120613368ed1 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // Thin CoreCLR-WASI corehost, built as a static archive (libWasiHost.a) and linked per-app by the -// WASI app builder (src/mono/wasi/build/WasiApp.CoreCLR.targets) against the statically-linked -// runtime. Mirrors browserhost, but with a real wasi:cli/run main() instead of a JS driver. +// CoreCLR-WASI app builder against the statically-linked runtime. Mirrors browserhost, but with a +// real wasi:cli/run main() instead of a JS driver. // See https://github.com/dotnet/runtime/issues/130129. #include From 16c2edcde026c2c4894e967e16be30080e0eb688 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 18:16:07 -0500 Subject: [PATCH 07/12] Build host.native for wasi CoreCLR so the runtime pack finds libWasiHost.a The wasi CoreCLR runtime pack now ships libWasiHost.a (manifest + liveBuilds inputs), but DefaultCoreClrSubsets for wasi omitted host.native, so the CI CoreCLR build legs (clr+libs+packs) assembled the pack before the host archive existed and failed in GenerateSharedFrameworkDepsFile with FileNotFoundException on libWasiHost.a. Add host.native to the wasi CoreCLR subset, mirroring browser (which already includes it), so the host is built before packs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- eng/Subsets.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Subsets.props b/eng/Subsets.props index be4331b5e2fda6..14b2d3eedab02a 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -112,7 +112,7 @@ clr.native+clr.corelib+clr.tools+clr.nativecorelib+clr.packages+clr.nativeaotlibs+clr.crossarchtools clr.native+clr.corelib+clr.tools+clr.nativecorelib+clr.packages+clr.nativeaotlibs+clr.crossarchtools provision.emsdk+clr.native+clr.corelib+clr.nativecorelib+clr.tools+clr.packages+clr.crossarchtools+libs.native+host.native - clr.native+clr.corelib+clr.nativecorelib+clr.tools+clr.packages+clr.crossarchtools+libs.native + clr.native+clr.corelib+clr.nativecorelib+clr.tools+clr.packages+clr.crossarchtools+libs.native+host.native clr.iltools+clr.packages From e6f6f8dce3ff31f46e66c84c61e052c83213d36e Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 15 Jul 2026 23:04:54 -0500 Subject: [PATCH 08/12] Address review: explicit includes and normalize CORE_LIBRARIES before compare - Add the standard headers wasihost.cpp directly uses (, , , ) instead of relying on transitive includes from corerun.hpp. - Normalize CORE_LIBRARIES with a trailing delimiter before comparing it to app_path, so an unterminated CORE_LIBRARIES equal to the app dir isn't duplicated in NATIVE_DLL_SEARCH_DIRECTORIES. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- src/native/corehost/wasihost/wasihost.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index 7a120613368ed1..941148a1056f87 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -7,8 +7,12 @@ // See https://github.com/dotnet/runtime/issues/130129. #include +#include +#include #include #include +#include +#include // Shared pal (path handling, CORE_ROOT/TPA helpers); header-only, so no corerun object is linked. #include "corerun.hpp" @@ -160,10 +164,11 @@ int main(int argc, char* argv[]) native_search_dirs << app_path << pal::env_path_delim; string_t core_libs = pal::getenv(envvar::coreLibraries); - if (!core_libs.empty() && core_libs != app_path) + if (!core_libs.empty()) { pal::ensure_trailing_delimiter(core_libs); - native_search_dirs << core_libs << pal::env_path_delim; + if (core_libs != app_path) + native_search_dirs << core_libs << pal::env_path_delim; } // CORE_ROOT locates the framework assemblies for the TPA list (the runtime itself is static on From dd20757c4667109adc42f8383c2c5db1ca653321 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 16 Jul 2026 12:46:27 -0500 Subject: [PATCH 09/12] Drop NATIVE_DLL_SEARCH_DIRECTORIES on wasi; trim comments The wasi host set NATIVE_DLL_SEARCH_DIRECTORIES, but it is dead on wasi: every P/Invoke is resolved by the pinvoke_override before the runtime's native-library search runs (dllimport.cpp), and wasm has no shared-library/dlopen support (pal/loader/module.cpp), so the search directories can never contribute a load. Remove the property and its native_search_dirs plumbing; keep TRUSTED_PLATFORM_ASSEMBLIES (assembly binding) and APP_PATHS (managed probing). Validated end-to-end (relink + run: VersionTests 251/251, culture-aware StringComparerTests 13/13). Also trim the verbose comments added to the shared build files to match repo conventions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- eng/liveBuilds.targets | 9 ++------- src/native/corehost/CMakeLists.txt | 3 +-- src/native/corehost/corehost.proj | 16 +++++----------- src/native/corehost/wasihost/wasihost.cpp | 14 ++++---------- 4 files changed, 12 insertions(+), 30 deletions(-) diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index a474069f8d6e2d..85becf0f76299e 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -325,13 +325,8 @@ IsNative="true" /> - + diff --git a/src/native/corehost/CMakeLists.txt b/src/native/corehost/CMakeLists.txt index f95b7a051557a0..d7d95b26c7d5c3 100644 --- a/src/native/corehost/CMakeLists.txt +++ b/src/native/corehost/CMakeLists.txt @@ -116,8 +116,7 @@ elseif(CLR_CMAKE_TARGET_BROWSER) add_subdirectory(hostmisc) add_subdirectory(browserhost) else() # CLR_CMAKE_TARGET_WASI - # The wasi host is a self-contained static archive (libWasiHost.a) linked per-app; it does not - # depend on hostmisc (it shares the corerun pal header instead), so only wasihost is built here. + # The wasi host is self-contained (shares the corerun pal header, no hostmisc dependency). add_subdirectory(wasihost) endif() diff --git a/src/native/corehost/corehost.proj b/src/native/corehost/corehost.proj index 3ed9077121c66f..c07994e55ce3f6 100644 --- a/src/native/corehost/corehost.proj +++ b/src/native/corehost/corehost.proj @@ -14,9 +14,7 @@ GenerateRuntimeVersionFile $(BuildCoreHostDependsOn);InitializeSourceControlInformationFromSourceControlManager AcquireEmscriptenSdk;$(BuildCoreHostDependsOn);GenerateEmccExports;ResolveRuntimeFilesFromLocalBuild - + AcquireWasiSdk;$(BuildCoreHostDependsOn) BuildCoreHostOnWindows BuildCoreHostOnUnix @@ -96,9 +94,7 @@ $(BuildArgs) -cmakeargs "-DCLR_CMAKE_BUILD_HOST_PRODUCT=$(BuildNativeHostProduct.ToUpper())" - + <_CoreHostBuildEnvironmentVariables>WASI_SDK_PATH=$(RuntimeBuildWasiSdkPath) @@ -213,11 +209,9 @@ SkipUnchangedFiles="true" /> - + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(HostSharedFrameworkDir)libWasiHost.a" /> diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index 941148a1056f87..3a6f37dd66eaf9 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -160,16 +160,9 @@ int main(int argc, char* argv[]) pal::ensure_trailing_delimiter(app_path); } - pal::stringstream_t native_search_dirs; - native_search_dirs << app_path << pal::env_path_delim; - string_t core_libs = pal::getenv(envvar::coreLibraries); if (!core_libs.empty()) - { pal::ensure_trailing_delimiter(core_libs); - if (core_libs != app_path) - native_search_dirs << core_libs << pal::env_path_delim; - } // CORE_ROOT locates the framework assemblies for the TPA list (the runtime itself is static on // wasi). On wasi the bundle co-locates the framework with the entry assembly, so default to the @@ -178,7 +171,6 @@ int main(int argc, char* argv[]) if (core_root.empty()) core_root = app_path; pal::ensure_trailing_delimiter(core_root); - native_search_dirs << core_root << pal::env_path_delim; string_t exe_path = pal::get_exe_path(); @@ -190,8 +182,10 @@ int main(int argc, char* argv[]) s_property_keys.push_back("APP_PATHS"); s_property_values.push_back(app_path); - s_property_keys.push_back("NATIVE_DLL_SEARCH_DIRECTORIES"); - s_property_values.push_back(native_search_dirs.str()); + // NATIVE_DLL_SEARCH_DIRECTORIES is intentionally not set: on wasi native libraries are + // statically linked and every P/Invoke is resolved by the pinvoke_override (callhelpers) before + // the runtime's native-library search runs, and wasm has no shared-library/dlopen support, so + // the search directories can never contribute a load. // Static: the contract must outlive coreclr_initialize (the runtime keeps its address). The // pinvoke_override field is forwarded to PInvokeOverride::SetPInvokeOverride by the runtime. From ed76459daafb4a0a21f8df8726eacdc633f75739 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 16 Jul 2026 13:05:09 -0500 Subject: [PATCH 10/12] Load icudt.dat from core_root, not the entry-assembly dir icudt.dat is a framework asset staged with the framework (where CORE_ROOT points), so preload it from core_root rather than app_path. These are the same directory in the default bundle, but if CORE_ROOT is set to a different directory than the entry assembly, loading from app_path would miss the data and silently fall back to invariant. Addresses PR review. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- src/native/corehost/wasihost/wasihost.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index 3a6f37dd66eaf9..e8619a7e6d1cac 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -230,11 +230,12 @@ int main(int argc, char* argv[]) coreclr_set_error_writer(nullptr); // The static ICU shim needs icudt.dat preloaded before managed globalization inits, otherwise it - // falls back to invariant (mirrors the browser JS host's wasm_load_icu_data). Skipped for - // invariant relinks (weak symbol null) and tolerant of a missing file. + // falls back to invariant (mirrors the browser JS host's wasm_load_icu_data). icudt.dat is a + // framework asset, so load it from core_root. Skipped for invariant relinks (weak symbol null) + // and tolerant of a missing file. if (GlobalizationNative_LoadICUData != nullptr) { - string_t icu_data_path = app_path; + string_t icu_data_path = core_root; icu_data_path.append(W("icudt.dat")); GlobalizationNative_LoadICUData(icu_data_path.c_str()); } From 9f67dddf3bb020112fc6391b37b8c068e0c33591 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 16 Jul 2026 13:50:02 -0500 Subject: [PATCH 11/12] Use HOST_PROPERTY_* macros and named-field contract init - Use the canonical HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES / HOST_PROPERTY_APP_PATHS macros instead of string literals, matching the rest of corehost. - Initialize host_runtime_contract with { sizeof, nullptr } and assign the used callbacks by name (like browserhost) so a future field add/reorder can't silently mis-wire the function pointers. Addresses PR review. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e95d46c-7a1d-4aee-87a7-ef681e7347cc --- src/native/corehost/wasihost/wasihost.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index e8619a7e6d1cac..b266db6a17518f 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -176,10 +176,10 @@ int main(int argc, char* argv[]) string_t tpa_list = build_tpa(core_root, core_libs); - s_property_keys.push_back("TRUSTED_PLATFORM_ASSEMBLIES"); + s_property_keys.push_back(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES); s_property_values.push_back(tpa_list); - s_property_keys.push_back("APP_PATHS"); + s_property_keys.push_back(HOST_PROPERTY_APP_PATHS); s_property_values.push_back(app_path); // NATIVE_DLL_SEARCH_DIRECTORIES is intentionally not set: on wasi native libraries are @@ -187,14 +187,12 @@ int main(int argc, char* argv[]) // the runtime's native-library search runs, and wasm has no shared-library/dlopen support, so // the search directories can never contribute a load. - // Static: the contract must outlive coreclr_initialize (the runtime keeps its address). The - // pinvoke_override field is forwarded to PInvokeOverride::SetPInvokeOverride by the runtime. - static host_runtime_contract host_contract = { - sizeof(host_runtime_contract), - nullptr, - &get_runtime_property, - nullptr, - &callhelpers_pinvoke_override }; + // Static: the contract must outlive coreclr_initialize (the runtime keeps its address). Assign + // fields by name (not positionally) so contract layout changes can't mis-wire the callbacks. + // The pinvoke_override field is forwarded to PInvokeOverride::SetPInvokeOverride by the runtime. + static host_runtime_contract host_contract = { sizeof(host_runtime_contract), nullptr }; + host_contract.get_runtime_property = &get_runtime_property; + host_contract.pinvoke_override = &callhelpers_pinvoke_override; { std::stringstream ss; ss << "0x" << std::hex << (size_t)(&host_contract); From 7a6b466fe8aed9c05f1d12176e0755cb8cab2f95 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 16 Jul 2026 16:19:29 -0500 Subject: [PATCH 12/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/native/corehost/wasihost/wasihost.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/native/corehost/wasihost/wasihost.cpp b/src/native/corehost/wasihost/wasihost.cpp index b266db6a17518f..9d34a920480a9d 100644 --- a/src/native/corehost/wasihost/wasihost.cpp +++ b/src/native/corehost/wasihost/wasihost.cpp @@ -238,14 +238,14 @@ int main(int argc, char* argv[]) GlobalizationNative_LoadICUData(icu_data_path.c_str()); } - int exit_code = 0; + unsigned int exit_code = 0; result = coreclr_execute_assembly( host_handle, domain_id, (int)entry_argv.size(), entry_argv.data(), entry_assembly.c_str(), - (unsigned int*)&exit_code); + &exit_code); if (result < 0) { std::fprintf(stderr, "coreclr_execute_assembly failed - Error: 0x%08x\n", result);