mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-07-20 17:17:58 -04:00
master
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
484f3b579e |
Add .NET 10 support via NativeAOT-LLVM along side .NET 8 support (#4915)
# Description of changes Builds on #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 #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 #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](https://github.com/dotnet/runtimelab/issues/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> |
||
|
|
5c04860649 |
Implement HTTP handlers / webhooks in Rust modules (#4636)
# Description of Changes
Adds support to Rust modules and the SpacetimeDB host for defining HTTP
handlers and registering them to routes.
## User-facing API
In a Rust module, users can annotate functions with the new macro
`#[spacetimedb::http::handler]`. A function annotated this way must
accept exactly two arguments, of types `&mut
spacetimedb::http::HandlerContext` and `spacetimedb::http::Request`
(which is a type alias for `http::Request<spacetimedb::http::Body>`. It
must also return `spacetimedb::http::Response` (which is a type alias
for `http::Response<spacetimedb::http::Body>`).
Once the user has defined an HTTP handler, they can register it to a
route by annotating a function with `#[spacetimedb::http::router]`. Such
a function must take no arguments and return a
`spacetimedb::http::Router`. (The original design put this annotation on
a `static` variable rather than a function, but that turned out to be
undesirable because it required that constructing a `Router` be
`const`.) `Router` exposes various methods for registering handlers to
routes.
All of a database's user-defined routes are exposed under
`/v1/database/:name_or_identity/route/{*path}`.
## Example
See [the new
smoketest](https://github.com/clockworklabs/SpacetimeDB/blob/phoebe/http-handlers-webhooks/crates/smoketests/tests/smoketests/http_routes.rs)
for a more exhaustive example.
A simpler example, which stores arbitrary byte data in a table via a
`POST` request, returns an ID, and then retrieves that same data via a
`GET` request with a query parameter:
```rust
#[spacetimedb::table(accessor = data)]
struct Data {
#[primary_key]
#[auto_inc]
id: u64,
body: Vec<u8>,
}
#[spacetimedb::http::handler]
fn insert(ctx: &mut HandlerContext, request: Request) -> Response {
let body: Vec<u8> = request.into_body().into_bytes().into();
let id = ctx.with_tx(|tx| tx.db.data().insert(Data { id: 0, body: body.clone() }).id);
Response::new(Body::from_bytes(format!("{id}")))
}
#[spacetimedb::http::handler]
fn retrieve(ctx: &mut HandlerContext, request: Request) -> Response {
let id = request
.uri()
.query()
.and_then(|query| query.strip_prefix("id="))
.and_then(|id| u64::from_str(id).ok())
.unwrap();
let body = ctx.with_tx(|tx| tx.db.data().id().find(id).map(|data| data.body));
if let Some(body) = body {
Response::new(Body::from_bytes(body))
} else {
Response::builder().status(404).body(Body::empty()).unwrap()
}
}
#[spacetimedb::http::router]
fn router() -> Router {
Router::new().post("/insert", insert).get("/retrieve", retrieve)
}
```
## Design and implementation notes
- As mentioned above, the router is registered via a function, not a
`static` or `const` item. This is because `static` or `const`
initializers must be `const`, and it turns out to be a pain to make all
of the `Router` constructors be `const fn`s.
- The `#[handler]` macro clobbers the original function name with a
`const` variable of type `HttpHandler`. This is unfortunate, but AFAICT
necessary, 'cause we need to pass the string identifier for the handler
to the `Router`, not the function pointer, and Rust allows no (stable
and reliable) way to get a unique string identifier out of a function
item/value, nor to attach data or implement traits for function
items/values. The alternative(s) would involve changing the signature of
the `Router` methods to have uglier and more complex callsites, e.g.
like `.get("/retrieve", retrieve::handler())`, `.get("/retrieve",
handler!(retrieve))` or `.get::<retrieve>("/retrieve")`. I believe that
registering handlers will be much more common than calling their
functions, so I've chosen to make it so that registering them gets the
convenient syntax, even though the inability to call them directly will
be somewhat surprising.
- I haven't wired up energy handling or timing metrics for handler
execution to anywhere. Procedures are still in the same boat.
- HTTP requests to user-defined routes bypass the usual SpacetimeDB auth
middleware, meaning that the host does not validate (or inspect in any
way) `Authorization` headers in requests before invoking the
user-defined handler. This is required to allow arbitrary
user-programmable handling of `Authorization` headers, including those
in formations which SpacetimeDB would reject. As a result of this,
`HandlerContext` doesn't expose a `sender` or `sender_connection_id`.
- HTTP route paths may consist only of a very restrictive set of
characters. I've chosen this set to keep our options open in the future
to add additional syntax to routes, like for registering wildcard
segments and path parameters:
- ASCII digits.
- ASCII letters.
- `-_~/`.
- The internal data structure that represents a `Router` is currently a
`Vec<Route>`, meaning that resolving a request to a route is
`O(num_routes)`. Registering a route checks against each previous route
for uniqueness, meaning that constructing a router is `O(num_routes ^
2)`. There are TODO comments to use a trie, but I think this can wait,
as I expect most databases to register few routes.
- Commit
|