Add .NET 10 support via NativeAOT-LLVM - #4741
Closed
JasonAtClockwork wants to merge 22 commits into
Closed
Conversation
The experimental NativeAOT-LLVM build path (EXPERIMENTAL_WASM_AOT=1) was missing several host function imports added in ABI versions 10.0-10.4, and the compiler package reference was hardcoded to Windows x64 only. Changes to SpacetimeDB.Runtime.targets: - Add missing spacetime_10.0 imports: datastore_update_bsatn, identity - Add all spacetime_10.1 imports: bytes_source_remaining_length - Add all spacetime_10.2 imports: get_jwt - Add all spacetime_10.3 imports: procedure_start_mut_tx, procedure_commit_mut_tx, procedure_abort_mut_tx, procedure_http_request - Add all spacetime_10.4 imports: datastore_index_scan_point_bsatn, datastore_delete_by_index_scan_point_bsatn - Replace hardcoded runtime.win-x64 package reference with runtime.$(NETCoreSdkPortableRuntimeIdentifier) so the AOT compiler package resolves correctly on both Windows x64 and Linux x64 - Use explicit version strings instead of $(SpacetimeNamespace) variable Changes to ci.yml: - Add AOT build smoketest in csharp-testsuite job to verify the NativeAOT-LLVM build path works on Linux x64 See #4514 for full context on the C# AOT situation.
#4601) # Description of Changes * Add dotnet-experimental feed + package source mapping for LLVM packages in `sdks/csharp/tools~/write-nuget-config.sh`, so generated NuGet.Config files include NativeAOT-LLVM prerequisites. * Make LLVM toolchain packages explicit dependencies in `SpacetimeDB.Runtime` to ensure restores succeed even when LLVM dependencies are only referenced through the `.nupkg`. * Import the LLVM targets from the package when `EXPERIMENTAL_WASM_AOT=1` to enable NativeAOT build steps without relying on downstream package reference resolution. # Context Changes are required to get `NativeAOT-LLVM` in #4515 to build correct, but moving the packages closer to the build, to ensure they get into the Nuget restore successfully. Additional changes where needed to `write-nuget-config.sh` to allow `Nuget.Config` files generated with required changes during regression testing. # API and ABI breaking changes None. # Expected complexity level and risk 2 (Low–moderate). Changes are scoped to build/restore infrastructure and package configuration. # Testing - [X] Built CLI locally - [X] Ran `run-regression-tests.sh` without errors --------- Co-authored-by: Jason Larabie <jason@clockworklabs.io>
Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Signed-off-by: Ryan <r.ekhoff@clockworklabs.io>
Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Signed-off-by: Ryan <r.ekhoff@clockworklabs.io>
Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Signed-off-by: Ryan <r.ekhoff@clockworklabs.io>
…f/update-nativeaot-llvm-infrastructure
…odule-path` is used
Signed-off-by: Ryan <r.ekhoff@clockworklabs.io>
pull Bot
pushed a commit
to Mu-L/SpacetimeDB
that referenced
this pull request
Jul 17, 2026
…ckworklabs#4915) # Description of changes Builds on clockworklabs#4741 to clean up and refine the NativeAOT-LLVM integration. The major changes: - **Standardized the repo on .NET 10 SDK** (`global.json` → `10.0.100`). The team is migrating to .NET 10 ahead of .NET 8 EOL. End users are unaffected as their SDK version is controlled by their own `global.json` emitted by `spacetime init`. - **Multi-target NuGet packages** instead of conditional single-target: - `SpacetimeDB.Runtime` now unconditionally targets `net8.0;net10.0` - `SpacetimeDB.BSATN.Runtime` now unconditionally targets `netstandard2.1;net8.0;net10.0` - NuGet packages ship both TFMs in a single `.nupkg`. No `EXPERIMENTAL_WASM_AOT` env var needed at pack time - `EXPERIMENTAL_WASM_AOT` define constant is set automatically when `TargetFramework == net10.0` - `ILCompiler.LLVM` package references conditioned on `TargetFramework == net10.0` (not env var) - **Centralized NativeAOT-LLVM build logic** in `SpacetimeDB.Runtime.props` and `SpacetimeDB.Runtime.targets`: - Moved `PublishTrimmed`, `SelfContained`, `WasmEnableThreads` into `.props` (previously duplicated across 4+ `.csproj` files) - Moved the `.wit`-stripping `UseWasiRuntimeOverlayWithoutComponentWit` target into `.targets` (was duplicated in every server `.csproj`) - Added `IlcLlvmTarget=wasm32-unknown-wasip1` override after ILCompiler.LLVM.targets import (previously passed as `/p:` arg) - Added `_InitializeWasiSdk` to `ObtainWasiSdk` BeforeTargets for .NET 10 compatibility - Consumer `.csproj` files no longer need any AOT boilerplate - **Refactored `csharp.rs` build logic** to support three build paths via `CsharpBuildPath` enum: - `Net8Jit` : stable .NET 8 path using `wasi-experimental` workload (Mono WASM) - `Net8Aot` : .NET 8 NativeAOT-LLVM (opt-in via `--native-aot` or `EXPERIMENTAL_WASM_AOT=1`) - `Net10Aot` : .NET 10 NativeAOT-LLVM (auto-detected when .NET 10 SDK is active) - Auto-detects .NET SDK version from `dotnet --version` (respects `global.json`) - Automatically sets `EXPERIMENTAL_WASM_AOT=1` for AOT paths so MSBuild conditionals activate - Unified `dotnet publish` command for all paths. Build-specific config handled by props/targets - Removed redundant `/p:IlcLlvmTarget` and `/p:WasmEnableThreads` CLI args (now in `.targets`) - **Refactored `init.rs`** for .NET version-aware scaffolding: - Added `--dotnet-version` CLI arg to explicitly select .NET 8 or 10 - Added `resolve_dotnet_major()` which auto-detects or prompts interactively when multiple SDKs are installed - .NET 10 auto-enables NativeAOT-LLVM (`--native-aot` not required) - .NET 10 template emits `<TargetFramework>net10.0</TargetFramework>` directly (no conditional) - .NET 8 AOT template adds `ILCompiler.LLVM 8.0.0-*` package refs gated on `EXPERIMENTAL_WASM_AOT=1` - Emits correct `global.json` per SDK version (`8.0.100` or `10.0.100`) - **Added `_initialize` call in `wasmtime_module.rs`**: NativeAOT-LLVM modules are WASI reactors that export `_initialize` to bootstrap the native runtime. This is called before preinit functions. Traditional .NET 8 WASI modules export `_start` instead and are unaffected. - **Updated `FFI.cs` `WasmImportLinkageAttribute` guard** to `#if EXPERIMENTAL_WASM_AOT && NET10_0_OR_GREATER`, ensuring the real attribute is only used when both the AOT flag and .NET 10+ TFM are active. The dummy shim is used for all other builds. - **Updated `csharp_aot_module.rs` smoketest**: - Auto-detects .NET SDK version and adjusts TFM accordingly - .NET 8 AOT: Windows-only (ILCompiler.LLVM 8.0.0-* not published for Linux) - .NET 10 AOT: Windows and Linux - Removed emscripten dependency (NativeAOT-LLVM uses WASI SDK, not emscripten) - **Updated CLI reference docs** to reflect `--native-aot` behavior change (not needed for .NET 10) and new `--dotnet-version` arg. This PR addresses several issues from clockworklabs#4741 review: 1. **NativeAOT-LLVM build logic was duplicated** in every server `.csproj` (4+ files had identical `PropertyGroup` and `Target` blocks). This is now centralized in the shipped `.props`/`.targets`. 2. **NuGet packages only shipped a single TFM**. The env var controlled which TFM was built, meaning you needed `EXPERIMENTAL_WASM_AOT=1` at pack time to get the `net10.0` DLL. Now both TFMs are always included. 3. **`spacetime init` emitted unnecessary conditional wrappers** in the generated `.csproj` for .NET 10 projects. Since the project is definitively targeting .NET 10, the TFM should be unconditional. 4. **`spacetime publish` passed MSBuild properties via `/p:` args** that are now handled by centralized `.targets`, simplifying the CLI code. # API and ABI breaking changes - `global.json` now requires .NET 10 SDK for repo development. All developers and CI agents need .NET SDK 10.0+ installed. - End user module projects are **not affected**. They continue to use whichever SDK their local `global.json` specifies. - .NET 8 JIT and .NET 8 AOT paths remain functional for end users until .NET 8 EOL. # Expected complexity level and risk 2 - The core risk is the same as clockworklabs#4741: future changes in the upstream NativeAOT-LLVM/WASI pipeline. Centralizing the build logic in `.props`/`.targets` reduces the surface area for breaking changes (one place to update vs. N consumer projects). The `.wit`-stripping workaround remains necessary until [dotnet/runtimelab#3144](dotnet/runtimelab#3144) is resolved. # Testing - Ran C# module publish + client connection tests for all three build paths: - .NET 8 JIT (`spacetime publish` without `--native-aot`) - .NET 8 AOT (`spacetime publish` with `EXPERIMENTAL_WASM_AOT=1` and .NET 8 SDK) - .NET 10 AOT (`spacetime publish` with .NET 10 SDK, auto-detected) - Verified `spacetime init --lang csharp` generates correct project structure for both .NET 8 and .NET 10 - Verified `spacetime init --lang csharp --dotnet-version 10` emits `net10.0` TFM and `10.0.100` in `global.json` - Verified C# regression tests complete successfully when ran under .NET 8 JIT, .NET 8 AOT, .NET 10 AOT - This required making local customizations to the repo versions of the regression tests, in order to conform to the configuration output by `spacetime init`, but the tests themselves remained identical. - Verified NuGet packages contain both `lib/net8.0/` and `lib/net10.0/` DLLs - Updated smoketest `test_build_csharp_module_aot` to work with both .NET 8 and .NET 10 SDK --------- Signed-off-by: Ryan <r.ekhoff@clockworklabs.io> Co-authored-by: Jason Larabie <jason@clockworklabs.io> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Co-authored-by: Lisandro Crespo <lisandroct@gmail.com> Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
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.
Description of changes
.NET 8.NET 10behindEXPERIMENTAL_WASM_AOT=1SpacetimeDB.Runtimedefaults tonet8.0and switches tonet10.0for NativeAOTSpacetimeDB.BSATN.Runtimedefaults tonetstandard2.1;net8.0and switches tonetstandard2.1;net10.0.NET 8-safe shim forWasmImportLinkageAttributeand annotated the NativeAOT host imports that require itMicrosoft.DotNet.ILCompiler.LLVMruntime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVMnet8.0net10.0whenEXPERIMENTAL_WASM_AOT=1.witfiles fromIlcFrameworkNativePathIlcSdkPathspacetime init --native-aotfor C# so it injects the full known-good.NET 10NativeAOT project shape intoStdbModule.csprojspacetime publish --native-aotso the C# build path:.NET 10NativeAOT output layoutwasm32-unknown-wasip1Why the heavy changes?
NativeAOT-LLVM currently does not cleanly support the exact output shape we need for SpacetimeDB: a WASI Preview 1/core wasm module that can be hosted by SpacetimeDB/wasmtime today.
In practice, the LLVM/WASI toolchain wants to flow component-model metadata (
*.wit) into the link step, which causes linker arguments like--component-typeand breaks production of the core module we actually need. The important upstream issue is:[NativeAOT-LLVM] Output non componentdotnet/runtimelab#3144The current workaround is to overlay
IlcFrameworkNativePathand exclude*.witfiles beforeLinkNativeLlvm, while preservingIlcSdkPath. Through testing, this was the only project-level workaround that proved necessary; the earlierIlcLlvmTargetproperty andWasmComponentTypeWit Remove=...item removal were not required once the overlay was in place.In order to strip out the
*.witfiles it was necessary to expand the module.csprojto include the following, which will copy out the outputs without the.witfiles allowing the build to continue in a core module format:Why
.NET 10And Not.NET 9I also investigated
.NET 9NativeAOT-LLVM as part of this work, but it is blocked by an upstreamMemoryMarshaltype-load failure in the NativeAOT-LLVM toolchain:browser-wasm - System.TypeLoadException: Could not load type 'System.Runtime.InteropServices.MemoryMarshal'browser-wasm - System.TypeLoadException: Could not load type 'System.Runtime.InteropServices.MemoryMarshal' dotnet/runtimelab#2731That issue reproduced in our C# module runtime path as well, so this PR keeps the stable path on
.NET 8and targets the experimental NativeAOT path on.NET 10.API and ABI breaking changes
.csprojto disable the .NET 10 specific requirements.Expected complexity level and risk
2 - The biggest risk is not the stable
.NET 8path, but future changes in the upstream NativeAOT-LLVM/WASI pipeline. This PR intentionally keeps the experimental logic explicit in the generated module project rather than hiding it in transitive package magic, because that proved more reliable during restore/publish testing. We also discussed if we run into troubles due to future changes from theNativeAOT-LLVMbranch to pin our version which can be done through thePackageReferences.Testing
.NET 8path.NET 10NativeAOT pathspacetime init --lang csharp --native-aotgenerates a workingStdbModule.csprojspacetime publish --native-aotsuccessfully builds and publishes a generated C# project