Skip to content

Add .NET 10 support via NativeAOT-LLVM - #4741

Closed
JasonAtClockwork wants to merge 22 commits into
masterfrom
jlarabie/nativeaot-net10
Closed

Add .NET 10 support via NativeAOT-LLVM#4741
JasonAtClockwork wants to merge 22 commits into
masterfrom
jlarabie/nativeaot-net10

Conversation

@JasonAtClockwork

Copy link
Copy Markdown
Contributor

Description of changes

  • Added a dual-path C# module workflow:
    • Stable/default path remains .NET 8
    • Experimental NativeAOT-LLVM path targets .NET 10 behind EXPERIMENTAL_WASM_AOT=1
  • Updated the C# runtime packages to switch targets conditionally for the experimental path:
    • SpacetimeDB.Runtime defaults to net8.0 and switches to net10.0 for NativeAOT
    • SpacetimeDB.BSATN.Runtime defaults to netstandard2.1;net8.0 and switches to netstandard2.1;net10.0
  • Added a .NET 8-safe shim for WasmImportLinkageAttribute and annotated the NativeAOT host imports that require it
  • Reduced the explicit experimental package set to the minimum required:
    • Microsoft.DotNet.ILCompiler.LLVM
    • runtime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVM
  • Updated the regression-test C# projects to support both paths:
    • default net8.0
    • net10.0 when EXPERIMENTAL_WASM_AOT=1
  • Kept the required WASI overlay workaround for NativeAOT-LLVM core-module output:
    • strips .wit files from IlcFrameworkNativePath
    • preserves IlcSdkPath
  • Updated spacetime init --native-aot for C# so it injects the full known-good .NET 10 NativeAOT project shape into StdbModule.csproj
  • Updated spacetime publish --native-aot so the C# build path:
    • uses the .NET 10 NativeAOT output layout
    • passes the required MSBuild properties for wasm32-unknown-wasip1

Why 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-type and breaks production of the core module we actually need. The important upstream issue is: [NativeAOT-LLVM] Output non component dotnet/runtimelab#3144

The current workaround is to overlay IlcFrameworkNativePath and exclude *.wit files before LinkNativeLlvm, while preserving IlcSdkPath. Through testing, this was the only project-level workaround that proved necessary; the earlier IlcLlvmTarget property and WasmComponentTypeWit Remove=... item removal were not required once the overlay was in place.

In order to strip out the *.wit files it was necessary to expand the module .csproj to include the following, which will copy out the outputs without the .wit files allowing the build to continue in a core module format:

  <Target Name="UseWasiRuntimeOverlayWithoutComponentWit"
          Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1'"
          AfterTargets="SetupProperties"
          BeforeTargets="LinkNativeLlvm">
    <PropertyGroup>
      <_OriginalIlcSdkPath>$(IlcSdkPath)</_OriginalIlcSdkPath>
      <_WasiRuntimeOverlayDir>$(IntermediateOutputPath)native-hidden-no-wit\</_WasiRuntimeOverlayDir>
    </PropertyGroup>

    <ItemGroup>
      <_WasiRuntimeOverlaySource Include="$(IlcFrameworkNativePath)**\*" Exclude="$(IlcFrameworkNativePath)**\*.wit" />
    </ItemGroup>

    <RemoveDir Directories="$(_WasiRuntimeOverlayDir)" />
    <MakeDir Directories="$(_WasiRuntimeOverlayDir)" />
    <Copy SourceFiles="@(_WasiRuntimeOverlaySource)"
          DestinationFiles="@(_WasiRuntimeOverlaySource->'$(_WasiRuntimeOverlayDir)%(RecursiveDir)%(Filename)%(Extension)')" />

    <PropertyGroup>
      <IlcFrameworkNativePath>$(_WasiRuntimeOverlayDir)</IlcFrameworkNativePath>
      <IlcSdkPath>$(_OriginalIlcSdkPath)</IlcSdkPath>
    </PropertyGroup>
  </Target>

Why .NET 10 And Not .NET 9

I also investigated .NET 9 NativeAOT-LLVM as part of this work, but it is blocked by an upstream MemoryMarshal type-load failure in the NativeAOT-LLVM toolchain:

That issue reproduced in our C# module runtime path as well, so this PR keeps the stable path on .NET 8 and targets the experimental NativeAOT path on .NET 10.

API and ABI breaking changes

  • Currently breaks .NET 8 NativeAOT-LLVM support by updating references to .NET 10, it might be possible to target both but we'd need more control in the .csproj to disable the .NET 10 specific requirements.

Expected complexity level and risk

2 - The biggest risk is not the stable .NET 8 path, 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 the NativeAOT-LLVM branch to pin our version which can be done through the PackageReferences.

Testing

  • Ran the C# regression tests on the stable .NET 8 path
  • Ran the C# regression tests on the experimental .NET 10 NativeAOT path
  • Verified spacetime init --lang csharp --native-aot generates a working StdbModule.csproj
  • Verified spacetime publish --native-aot successfully builds and publishes a generated C# project
  • Validated the NativeAOT path on both Windows and Linux

cloutiertyler and others added 21 commits March 1, 2026 22:20
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>
@JasonAtClockwork JasonAtClockwork self-assigned this Apr 2, 2026
@JasonAtClockwork
JasonAtClockwork requested a review from rekhoff April 2, 2026 20:41
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants