Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ rust-version = "1.85"
tokio-rustls = { version = "0.26.6", default-features = false, features = ["ring", "tls12"], optional = true }
openssl = { version = "0.10.30", optional = true }
tokio-openssl = { version = "0.6.5", optional = true }
tokio = { version = "1.54.1", features = ["net", "io-util", "rt", "fs", "sync", "time"] }
socket2 = "0.6.5"
tokio = { version = "1.54.1", features = ["net", "io-util", "rt", "fs", "sync", "time"], optional = true }
socket2 = { version = "0.6.5", optional = true }
serde = { version = "1.0.123", optional = true }
serde_json = { version = "1.0.59", optional = true }

Expand All @@ -25,21 +25,30 @@ version = "0.1.2"
[dependencies.octane_http]
path = "octane_http"
version = "0.1"
default-features = false

[features]
default = ["faithful", "query_strings", "cookies", "url_variables", "raw_headers"]
faithful = []
test_crate = []
raw_headers = []
query_strings = []
extended_queries = []
cookies = []
json = ["serde", "serde_json"]
default = ["std", "faithful", "query_strings", "cookies", "url_variables", "raw_headers"]
# The tokio server and everything built on it. Without it the crate is
# `#![no_std]` and only exposes the allocation-free core, `octane::portable`.
std = ["tokio", "socket2", "octane_http/std"]
faithful = ["std"]
test_crate = ["std"]
raw_headers = ["std"]
query_strings = ["std"]
extended_queries = ["std"]
cookies = ["std"]
json = ["std", "serde", "serde_json"]
# url variables like `/users/:id`, for both the tokio and the WASIp3 router
url_variables = []
rustls = ["tokio-rustls"]
openSSL = ["openssl", "tokio-openssl"]
rustls = ["std", "tokio-rustls"]
openSSL = ["std", "openssl", "tokio-openssl"]
# `octane::wasip3`: serve from a WASIp3 component over wasi:sockets. Only has
# an effect on wasm32-wasip3, and doesn't need `std` (use it with
# `default-features = false`)
wasm = ["octane_http/wasm"]
# exposes the parsers to the fuzz targets in octane_http/fuzz, not a stable API
fuzzing = []
fuzzing = ["std"]

[dev-dependencies]
tokio = { version = "1.54.1", features = ["macros", "rt-multi-thread"] }
Expand Down
139 changes: 139 additions & 0 deletions guests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# WASI guests

Programs that run octane as WebAssembly components inside wasmtime, using
`octane` with `default-features = false` (no tokio) and the `wasm` feature,
which adds `octane::wasip3` on `wasm32-wasip3`.

| Guest | What it is |
| --- | --- |
| [`wasip3-server`](wasip3-server) | HTTP/1.1 server as a WASIp3 component: `octane::wasip3::serve` with an async handler |

Each guest is its own Cargo workspace, so its target and nightly settings
don't affect the main octane build.

## Setup

You need three things: Rust nightly with `rust-src`, wasi-sdk 34 or newer
and wasmtime with WASIp3 support. The steps below work on macOS and Linux
(arm64 and x86_64).

### 1. Rust nightly

`wasm32-wasip3` is a tier 3 target with no prebuilt standard library, so it
is built from source with `-Zbuild-std`. That needs nightly and `rust-src`:

```sh
rustup toolchain install nightly --component rust-src
```

Each guest has a `rust-toolchain.toml` that selects nightly, so nothing has
to be set as the default.

### 2. wasi-sdk

wasi-sdk provides the WASIp3 libc, `crt1-reactor.o`, `wasm-ld` and
`wasm-component-ld`. The `wasm-component-ld` that ships with rustc is too
old for WASIp3, so the guests use wasi-sdk's.

Download the latest release from
<https://github.com/WebAssembly/wasi-sdk/releases> into `~/wasi-sdk`:

```sh
# latest release number, e.g. 34
WASI_SDK_VERSION=$(curl -fsSL https://api.github.com/repos/WebAssembly/wasi-sdk/releases/latest \
| sed -n 's/.*"tag_name": *"wasi-sdk-\([0-9.]*\)".*/\1/p')

case "$(uname -m)" in arm64|aarch64) ARCH=arm64;; x86_64|amd64) ARCH=x86_64;; esac
case "$(uname -s)" in Darwin) OS=macos;; Linux) OS=linux;; esac
NAME=wasi-sdk-$WASI_SDK_VERSION.0-$ARCH-$OS

mkdir -p ~/wasi-sdk
curl -fL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$WASI_SDK_VERSION/$NAME.tar.gz" \
| tar -xz -C ~/wasi-sdk
echo "installed ~/wasi-sdk/$NAME"
```

Then point `WASI_SDK_PATH` at it. Add this to `~/.zshrc` (or `~/.bashrc`)
so new shells have it:

```sh
export WASI_SDK_PATH=~/wasi-sdk/wasi-sdk-34.0-arm64-macos # the folder printed above
```

Check it works:

```sh
"$WASI_SDK_PATH/bin/wasm-ld" --version # LLD 23 or newer
ls "$WASI_SDK_PATH/share/wasi-sysroot/lib/wasm32-wasip3" # the WASIp3 libc
```

**macOS:** a wasi-sdk downloaded with a browser is quarantined by
Gatekeeper. Its tools then fail, often silently or with an
"Apple could not verify" dialog. `curl` doesn't quarantine files, so the
steps above aren't affected. If you downloaded it some other way, clear the
quarantine once:

```sh
xattr -dr com.apple.quarantine "$WASI_SDK_PATH"
find "$WASI_SDK_PATH" -type l -exec xattr -s -d com.apple.quarantine {} \;
```

### 3. wasmtime

Install it with the official script, or update an existing install to the
latest version:

```sh
curl https://wasmtime.dev/install.sh -sSf | bash
wasmtime --version
```

WASIp3 needs a release that has the `-S p3` option (tested with 47.0.3):

```sh
wasmtime run -S help | grep p3
```

## Build and run

```sh
cd guests/wasip3-server
cargo build --release
wasmtime run -S p3 -S inherit-network -W component-model-async \
target/wasm32-wasip3/release/wasip3_server.wasm
```

The flags:

- `-S p3` enables the WASIp3 interfaces
- `-S inherit-network` lets the guest bind and accept on the host's network
- `-W component-model-async` enables async lifting, `stream<T>` and
`future<T>`

The first build takes a minute while the standard library is compiled.
Later builds only rebuild the guest.

In another terminal:

```sh
curl localhost:8080/
curl -X POST -d 'hello' localhost:8080/echo
curl -N 'localhost:8080/stream?chunks=5&ms=500' # body arrives in chunks

# 10 requests that each wait 1 s finish in about 1 s, not 10
time (for i in $(seq 10); do curl -s 'localhost:8080/sleep?ms=1000' & done; wait)
```

See the [wasip3-server README](wasip3-server/README.md) for its routes and
how it is put together.

## Troubleshooting

| Error | Cause |
| --- | --- |
| `set WASI_SDK_PATH to the wasi-sdk install` | `WASI_SDK_PATH` isn't exported in this shell |
| `.../lib/wasm32-wasip3 is missing` | wasi-sdk older than 34, it has no WASIp3 libc |
| `can't find crate for core` | `rust-src` isn't installed for nightly, see step 1 |
| `undefined symbol: __wasm_get_stack_pointer` | `crt1-reactor.o` isn't linked, check `build.rs` ran with `WASI_SDK_PATH` set |
| `failed to resolve import env::__wasm_set_tls_base` | rustc's bundled `wasm-component-ld` ran instead of wasi-sdk's, check `.cargo/config.toml` uses `tools/wasm-component-ld` |
| wasmtime: `unknown option p3` | wasmtime too old, update it (step 3) |
15 changes: 15 additions & 0 deletions guests/wasip3-server/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[build]
target = "wasm32-wasip3"

[unstable]
# tier 3 target, no prebuilt std
build-std = ["std", "panic_abort"]

[target.wasm32-wasip3]
# runs wasi-sdk's wasm-component-ld and wasm-ld, see the script
linker = "tools/wasm-component-ld"
rustflags = [
# libc and crt1-reactor.o come from wasi-sdk (see build.rs), rustc has
# no self-contained copies for this target
"-C", "link-self-contained=no",
]
1 change: 1 addition & 0 deletions guests/wasip3-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
28 changes: 28 additions & 0 deletions guests/wasip3-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "wasip3-server"
version = "0.0.0"
edition = "2021"
publish = false

# A WASIp3 guest serving HTTP/1.1 over wasi:sockets with octane's
# allocation-free core. See README.md for building and running it.

# the `wasi:cli/run` export comes from `export!` below, not a `main`
[lib]
crate-type = ["cdylib"]

[dependencies]
# without tokio: `octane::wasip3` serves over wasi:sockets with octane's
# router, `url_variables` enables routes like `/hello/:name`
octane = { path = "../..", default-features = false, features = ["wasm", "url_variables"] }
# the `wasi:cli/run` export and the clock used by `/sleep` and `/stream`
wasip3 = { version = "0.9.0", default-features = false }

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

# not part of the octane workspace, it builds for its own target
[workspace]
64 changes: 64 additions & 0 deletions guests/wasip3-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# wasip3-server

An HTTP/1.1 server built as a WASIp3 component with octane's `wasm`
feature:

```toml
octane = { path = "../..", default-features = false, features = ["wasm", "url_variables"] }
```

Routes are added to an `octane::wasip3::Router` through octane's `Route`
trait (`get`, `post`, `add` for middleware, `:name` url variables), the
same as on the tokio server, with async handlers made by `handler(...)`.
`octane::wasip3::serve` creates the listening socket over
`wasi:sockets@0.3.0` and runs each connection as its own task, so
`src/lib.rs` only defines routes. Connections are served concurrently on
one thread while they wait on sockets, timers or streamed bodies.

## Build and run

See [the guests README](../README.md) for installing nightly, wasi-sdk and
wasmtime. Then:

```sh
export WASI_SDK_PATH=~/wasi-sdk/wasi-sdk-34.0-arm64-macos
cargo build --release
wasmtime run -S p3 -S inherit-network -W component-model-async \
target/wasm32-wasip3/release/wasip3_server.wasm
```

## How it builds

- `rust-toolchain.toml` selects nightly, and `.cargo/config.toml` builds
`std` for `wasm32-wasip3` with `-Zbuild-std`.
- `build.rs` links wasi-sdk's WASIp3 libc and `crt1-reactor.o`. rustc has
no copies of these for a tier 3 target.
- `tools/wasm-component-ld` is the linker. It runs wasi-sdk's
`wasm-component-ld` and `wasm-ld`, because the ones bundled with rustc
are too old for cooperative threading.

## Routes

| Route | Response |
| --- | --- |
| every request | logged by a middleware (`router.add`), then routing continues |
| `GET /` | a greeting, at once |
| `GET /hello/:name` | greets `name`, a url variable |
| `GET /sleep?ms=N` | waits N ms on the host clock, then replies |
| `GET /stream?chunks=N&ms=M` | writes the body in N chunks, M ms apart |
| `POST /echo` | the request body |
| anything else | `404`, sent by the server when no route answered |

Like on the tokio server, a route only answers the method it was added
with: `HEAD /` is a `404` unless it's added with `router.head`.

10 parallel `curl localhost:8080/sleep?ms=1000` finish in about 1 s
in total, not 10, and requests made while `/stream` is writing are
answered between its chunks.

## Atomics

`-C target-feature=+atomics,+bulk-memory` builds and runs, but there's
nothing to gain: WASIp3 tasks are cooperative and share one thread, and
wasm-ld refuses `--cooperative-threading` together with `--shared-memory`,
so the atomic instructions operate on unshared memory.
28 changes: 28 additions & 0 deletions guests/wasip3-server/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Link against wasi-sdk's WASIp3 libc and its `crt1-reactor.o`. rustc has
// no self-contained sysroot for this tier 3 target, and for a cdylib it would
// otherwise take crt1-reactor.o from there. That object sets up the per-task
// context: without it wasm-ld doesn't generate the
// `__wasm_get_stack_pointer`/`__wasm_set_stack_pointer` imports that every
// object built for cooperative threading needs.
use std::env;
use std::path::PathBuf;

fn main() {
println!("cargo:rerun-if-env-changed=WASI_SDK_PATH");
if env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("p3") {
return;
}
let sdk = env::var("WASI_SDK_PATH")
.expect("set WASI_SDK_PATH to the wasi-sdk install (34 or newer has WASIp3)");
let libc = PathBuf::from(sdk).join("share/wasi-sysroot/lib/wasm32-wasip3");
assert!(
libc.is_dir(),
"{} is missing, is wasi-sdk 34+?",
libc.display()
);
println!("cargo:rustc-link-search=native={}", libc.display());

let crt = libc.join("crt1-reactor.o");
assert!(crt.is_file(), "{} is missing", crt.display());
println!("cargo:rustc-link-arg={}", crt.display());
}
4 changes: 4 additions & 0 deletions guests/wasip3-server/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# wasm32-wasip3 is tier 3, its std is built from source with -Zbuild-std
[toolchain]
channel = "nightly"
components = ["rust-src"]
Loading
Loading