Skip to content

Commit 5893f67

Browse files
rekhoffjdetter
andauthored
Enable --native-aot flag during spacetime init and spacetime publish for NativeAOT-LLVM support (#4672)
# Description of Changes Adds `--native-aot` flag to `spacetime init` command for creating NativeAOT-LLVM enabled C# projects. This automatically adds the required LLVM package references to the generated `.csproj` file and sets `"native-aot": true` in `spacetime.json`. This also adds `--native-aot` flag to `spacetime publish`. This is optional because the developer will still need to add the required LLVM package references to their `.csproj` file, and the provided instructions also include adding `"native-aot": true` to their `spacetime.json` which will already apply the flag. # API and ABI breaking changes None. # Expected complexity level and risk 1 (Low). Adds new optional flag to init command and post-processing of generated .csproj files. # Testing - [X] Verified `spacetime init --lang csharp --native-aot` creates projects with required LLVM package references - [X] Confirmed `spacetime.json` is generated with `"native-aot": true` - [X] Tested publish command works with both `--native-aot` flag and `spacetime.json` configuration --------- Signed-off-by: Ryan <r.ekhoff@clockworklabs.io> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
1 parent 793684b commit 5893f67

35 files changed

Lines changed: 2288 additions & 102 deletions
Lines changed: 133 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,143 @@
1-
# Converting a SpacetimeDB 2.0.x project to use NativeAOT-LLVM
1+
# Using NativeAOT-LLVM with SpacetimeDB C# Modules
22

3-
This guide provides instructions on taking an existing C# module that targets the public-released SpacetimeDB CLI, and guides you through the necessary steps to enable `NativeAOT-LLVM` use.
3+
This guide provides instructions for enabling NativeAOT-LLVM compilation for C# SpacetimeDB modules, which can provide performance improvements.
44

55
## Overview
6-
In order to use `NativeAOT-LLVM` on a C# module, we'll need to set the `EXPERIMENTAL_WASM_AOT` environment variable to `1` which SpacetimeDB will check during publishing of a module.
7-
For the module to work, we'll also need the `NuGet.Config` and `.csproj` files with the required package sources and references.
86

9-
### Prerequisites:
7+
NativeAOT-LLVM compiles C# modules to native WebAssembly (WASM) instead of using the Mono runtime.
8+
9+
> [!WARNING]
10+
> This is currently only supported for Windows server modules and is experimental.
11+
12+
## Prerequisites
13+
1014
- **.NET SDK 8.x** (same version used by SpacetimeDB)
1115
- **Emscripten SDK (EMSDK)** installed (must contain `upstream/emscripten/emcc.bat`)
1216
- **(Optional) Binaryen (wasm-opt)** installed and on `PATH` (recommended: `version_116`)
17+
- **Windows** - NativeAOT-LLVM is currently only supported for Windows server modules
1318

14-
## Steps
19+
## Prerequisites Installation
1520

16-
1. **Install EMSDK**
17-
- Download and extract the `https://github.com/emscripten-core/emsdk` release.
18-
- Example path: `D:\Tools\emsdk`
21+
### Install Emscripten SDK (EMSDK)
22+
23+
The Emscripten SDK is required for NativeAOT-LLVM compilation:
1924

20-
2. **Set environment variables**
25+
1. **Download and extract** the Emscripten SDK from `https://github.com/emscripten-core/emsdk`
26+
- Example path: `D:\Tools\emsdk`
2127

22-
```powershell
23-
$env:EXPERIMENTAL_WASM_AOT=1
28+
2. **Set environment variable** (optional - the CLI will detect it automatically):
29+
```
2430
$env:EMSDK="D:\Tools\emsdk"
2531
```
2632

27-
3. **Ensure NuGet feed is configured**
28-
NativeAOT-LLVM packages currently come from **dotnet-experimental**:
29-
- Add the `dotnet-experimental` feed to a project-local `NuGet.Config`
30-
```xml
31-
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
32-
```
33-
This should be a `NuGet.Config` placed in the root directory of your module folder (next to the `.csproj`). You can simply add the above line to the `packageSources` of your existing file, or if you need to create a minimal one, you can use:
34-
35-
```xml
36-
<?xml version="1.0" encoding="utf-8"?>
37-
<configuration>
38-
<packageSources>
39-
<clear />
40-
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
41-
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
42-
</packageSources>
43-
</configuration>
44-
```
45-
46-
4. **Ensure NativeAOT emits a `.wasm` output**
47-
- For LLVM AOT builds, the CLI currently accepts `dotnet.wasm` under `bin/Release/net8.0/wasi-wasm/publish/`.
48-
- In the module `.csproj`, ensure the AOT package references include:
49-
50-
```xml
51-
<ItemGroup Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1'">
52-
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0-*" Condition="'$(ILLinkTargetsPath)' == ''" />
53-
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
54-
<PackageReference Include="runtime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
55-
</ItemGroup>
56-
```
57-
58-
The contents of your `.csproj` should look something like this:
59-
60-
```xml
61-
<Project Sdk="Microsoft.NET.Sdk">
62-
<PropertyGroup>
63-
<TargetFramework>net8.0</TargetFramework>
64-
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
65-
<ImplicitUsings>enable</ImplicitUsings>
66-
<Nullable>enable</Nullable>
67-
</PropertyGroup>
68-
<ItemGroup>
69-
<PackageReference Include="SpacetimeDB.Runtime" Version="2.0.*" />
70-
</ItemGroup>
71-
<ItemGroup Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1'">
72-
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0-*" Condition="'$(ILLinkTargetsPath)' == ''" />
73-
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
74-
<PackageReference Include="runtime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
75-
</ItemGroup>
76-
</Project>
77-
```
78-
79-
- **NU1504 warning**: Because the runtime targets also add these LLVM packages, you may see a duplicate PackageReference warning. It is non-blocking.
80-
81-
5. **(Optional) Install wasm-opt (Binaryen)**
82-
This step is optional, but provides performance improvements, and therefore is recommended.
83-
- Download Binaryen `https://github.com/WebAssembly/binaryen/releases/tag/version_116` for Windows and extract it, e.g. `D:\Tools\binaryen`.
84-
- Add `D:\Tools\binaryen\bin` to `PATH`.
85-
- Verify:
86-
87-
```powershell
88-
wasm-opt --version
89-
```
90-
91-
6. **Publish module**
92-
- Use the SpacetimeDB CLI to publish from the module directory.
93-
- With `EXPERIMENTAL_WASM_AOT=1`, publish should attempt LLVM AOT.
94-
- Ensure the local server is running if publishing to `local`.
33+
### Install Binaryen (Optional)
34+
35+
Binaryen provides `wasm-opt` for WASM optimization (recommended for performance):
36+
37+
1. Download Binaryen https://github.com/WebAssembly/binaryen/releases/tag/version_116 for Windows
38+
2. Extract to e.g. `D:\Tools\binaryen`
39+
3. Add `D:\Tools\binaryen\bin` to `PATH`
40+
41+
To temporarily add to your current PowerShell session:
42+
```
43+
$env:PATH += ";D:\Tools\binaryen\bin"
44+
```
45+
4. Verify:
46+
```
47+
wasm-opt --version
48+
```
49+
50+
## Creating a New NativeAOT Project
51+
52+
When creating a new C# project, use the `--native-aot` flag:
53+
54+
```
55+
spacetime init --lang csharp --native-aot my-native-aot-project
56+
```
57+
58+
This automatically:
59+
- Creates a C# project with the required package references
60+
- Generates a `spacetime.json` with `"native-aot": true`
61+
- Configures the project for NativeAOT-LLVM compilation
62+
63+
## Converting an Existing Project
64+
65+
1. **Update spacetime.json**
66+
Add `"native-aot": true` to your `spacetime.json`:
67+
```json
68+
{
69+
"module": "your-module-name",
70+
"native-aot": true
71+
}
72+
```
73+
74+
**Note:** Once `spacetime.json` has `"native-aot": true`, you can simply run `spacetime publish` without the `--native-aot` flag. The CLI will automatically detect the configuration and use NativeAOT compilation.
75+
76+
2. **Ensure NuGet feed is configured**
77+
NativeAOT-LLVM packages come from **dotnet-experimental**. Add to `NuGet.Config`:
78+
```xml
79+
<?xml version="1.0" encoding="utf-8"?>
80+
<configuration>
81+
<packageSources>
82+
<clear />
83+
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
84+
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
85+
</packageSources>
86+
</configuration>
87+
```
88+
89+
3. **Add NativeAOT package references**
90+
Add this `ItemGroup` to your `.csproj`:
91+
```xml
92+
<ItemGroup Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1'">
93+
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0-*" Condition="'$(ILLinkTargetsPath)' == ''" />
94+
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
95+
<PackageReference Include="runtime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
96+
</ItemGroup>
97+
```
98+
99+
Your complete `.csproj` should look like:
100+
```xml
101+
<Project Sdk="Microsoft.NET.Sdk">
102+
<PropertyGroup>
103+
<TargetFramework>net8.0</TargetFramework>
104+
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
105+
<ImplicitUsings>enable</ImplicitUsings>
106+
<Nullable>enable</Nullable>
107+
</PropertyGroup>
108+
<ItemGroup>
109+
<PackageReference Include="SpacetimeDB.Runtime" Version="2.0.*" />
110+
</ItemGroup>
111+
<ItemGroup Condition="'$(EXPERIMENTAL_WASM_AOT)' == '1'">
112+
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0-*" Condition="'$(ILLinkTargetsPath)' == ''" />
113+
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
114+
<PackageReference Include="runtime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
115+
</ItemGroup>
116+
</Project>
117+
```
118+
119+
## Publishing Your NativeAOT Module
120+
121+
After completing either the **Creating a New NativeAOT Project** or **Converting an Existing Project** steps above, you can publish your module normally:
122+
123+
```
124+
# From your project directory
125+
spacetime publish your-database-name
126+
```
127+
128+
If you have `"native-aot": true` in your `spacetime.json`, the CLI will automatically detect this and use NativeAOT compilation. Alternatively, you can use:
129+
130+
```
131+
spacetime publish --native-aot your-database-name
132+
```
133+
134+
The CLI will display "Using NativeAOT-LLVM compilation (experimental)" when NativeAOT is enabled.
95135

96136
## Troubleshooting
97137

98138
### Package source mapping enabled
99-
If you have **package source mapping** enabled in `NuGet.Config`, you must add mappings for the LLVM packages or restores will fail.
100-
Place the following in `NuGet.Config` inside the `configuration` section:
139+
If you have **package source mapping** enabled in `NuGet.Config`, add mappings for the LLVM packages:
140+
101141
```xml
102142
<packageSourceMapping>
103143
<packageSource key="bsatn-runtime">
@@ -119,6 +159,16 @@ Place the following in `NuGet.Config` inside the `configuration` section:
119159
### wasi-experimental workload install fails
120160
If the CLI cannot install the `wasi-experimental` workload automatically, install it manually:
121161

122-
```powershell
162+
```
123163
dotnet workload install wasi-experimental
124164
```
165+
166+
### Duplicate PackageReference warning
167+
You may see a `NU1504` warning about duplicate `PackageReference` items. This is expected and non-blocking.
168+
169+
### Code generation failed
170+
If you see errors like "Code generation failed for method", ensure:
171+
1. You're using `SpacetimeDB.Runtime` version 2.0.4 or newer
172+
2. All required package references are in your `.csproj`
173+
3. The `dotnet-experimental` feed is configured in `NuGet.Config`
174+

crates/bindings-csharp/Runtime/Internal/Module.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,36 @@ internal RawModuleDefV10 BuildModuleDefinition()
206206

207207
public static class Module
208208
{
209+
// Workaround for NativeAOT-LLVM IL scanner bug:
210+
// The scanner fails to compute vtables for TaggedEnum<T> base types when
211+
// concrete subtypes are only encountered indirectly (e.g., through Equals
212+
// calls on types containing TaggedEnum fields). This occurs when no user
213+
// table has indexes/primary keys, so RawIndexAlgorithm is never directly
214+
// constructed in user code.
215+
// By referencing concrete TaggedEnum subtypes here, we ensure the IL scanner
216+
// always processes their vtables. One variant per TaggedEnum is sufficient.
217+
[System.Runtime.CompilerServices.MethodImpl(
218+
System.Runtime.CompilerServices.MethodImplOptions.NoInlining
219+
)]
220+
private static void EnsureNativeAotTypeRoots()
221+
{
222+
// These constructions are never executed at runtime — they exist solely
223+
// to make the IL scanner compute vtables for TaggedEnum subtypes.
224+
// The condition is always false but the scanner must assume it could be true.
225+
if (Environment.TickCount < 0 && Environment.TickCount > 0)
226+
{
227+
_ = new RawIndexAlgorithm.BTree(null!);
228+
_ = new RawConstraintDataV9.Unique(null!);
229+
_ = new RawModuleDef.V10(null!);
230+
_ = new RawModuleDefV10Section.Typespace(null!);
231+
_ = new ExplicitNameEntry.Table(null!);
232+
_ = new MiscModuleExport.TypeAlias(null!);
233+
_ = new RawMiscModuleExportV9.ColumnDefaultValue(null!);
234+
_ = new SpacetimeDB.Filter.Sql(null!);
235+
_ = new ViewResultHeader.RowData(default);
236+
}
237+
}
238+
209239
private static readonly RawModuleDefV10 moduleDef = new();
210240

211241
private static readonly List<IReducer> reducers = [];
@@ -419,6 +449,7 @@ private static void Write(this BytesSink sink, byte[] bytes)
419449

420450
public static void __describe_module__(BytesSink description)
421451
{
452+
EnsureNativeAotTypeRoots();
422453
try
423454
{
424455
var module = moduleDef.BuildModuleDefinition();

0 commit comments

Comments
 (0)