Add WASM host-runtime to TrailBase and rebrand relevant command-line flags.

This commit is contained in:
Sebastian Jeltsch
2025-09-09 15:22:21 +02:00
parent 797423fa91
commit 55149a1734
87 changed files with 6907 additions and 60 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Build artifacts
target/
node_modules/
dist/
# Dart workspace artifacts
.dart_tool

1160
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@ members = [
"crates/schema",
"crates/sqlean",
"crates/sqlite",
"crates/wasi-keyvalue",
"crates/wasm-runtime-host",
"crates/wasm-runtime-common",
"docs/examples/record_api_rs",
"examples/custom-binary",
]
@@ -25,8 +28,13 @@ default-members = [
"crates/extension",
"crates/js-runtime",
"crates/qs",
"crates/refinery",
"crates/schema",
"crates/sqlean",
"crates/sqlite",
"crates/wasi-keyvalue",
"crates/wasm-runtime-host",
"crates/wasm-runtime-common",
]
# https://doc.rust-lang.org/cargo/reference/profiles.html
@@ -66,15 +74,24 @@ rust-embed = { version = "8.4.0", default-features = false, features = ["mime-gu
tokio = { version = "^1.38.0", default-features = false, features = ["macros", "net", "rt-multi-thread", "fs", "signal", "time", "sync"] }
tracing = { version = "0.1.40", default-features = false }
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["smallvec", "std", "fmt", "json"] }
trailbase = { path = "crates/core", version = "0.2.0" }
trailbase-assets = { path = "crates/assets", version = "0.2.0" }
trailbase-build = { path = "crates/build", version = "0.1.1" }
trailbase-client = { path = "crates/client", version = "0.5.0" }
trailbase-sqlean = { path = "crates/sqlean", version = "0.0.3" }
trailbase-extension = { path = "crates/extension", version = "0.3.0" }
trailbase-js = { path = "crates/js-runtime", version = "0.2.0" }
trailbase-qs = { path = "crates/qs", version = "0.1.0" }
trailbase-refinery = { path = "crates/refinery", version = "0.1.0" }
trailbase-schema = { path = "crates/schema", version = "0.1.0" }
trailbase-sqlean = { path = "crates/sqlean", version = "0.0.3" }
trailbase-sqlite = { path = "crates/sqlite", version = "0.3.0" }
trailbase = { path = "crates/core", version = "0.2.0" }
trailbase-wasi-keyvalue = { path = "crates/wasi-keyvalue", version = "0.1.0" }
trailbase-wasm = { path = "crates/wasm-runtime-guest", version = "0.1.0" }
trailbase-wasm-common = { path = "crates/wasm-runtime-common", version = "0.1.0" }
trailbase-wasm-runtime-host = { path = "crates/wasm-runtime-host", version = "0.1.0" }
ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] }
uuid = { version = "1", default-features = false, features = ["std", "v4", "v7", "serde"] }
wasmtime = "36.0.1"
wasmtime-wasi = { version = "36.0.1", default-features = false, features = [] }
wasmtime-wasi-http = "36.0.1"
wasmtime-wasi-io = "36.0.1"

View File

@@ -174,7 +174,7 @@ Future<Process> initTrailBase() async {
'run',
'--address=${address}',
// We want at least some parallelism to experience isolate-local state.
'--js-runtime-threads=2',
'--runtime-threads=2',
]);
final dio = Dio();

View File

@@ -58,7 +58,7 @@ public class ClientTestFixture : IDisposable {
process = new Process();
process.StartInfo.WorkingDirectory = projectDirectory;
process.StartInfo.FileName = "cargo";
process.StartInfo.Arguments = $"run -- --data-dir ../../testfixture run -a {address} --js-runtime-threads 2";
process.StartInfo.Arguments = $"run -- --data-dir ../../testfixture run -a {address} --runtime-threads 2";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();

View File

@@ -52,7 +52,7 @@ func startTrailBase() (*exec.Cmd, error) {
fmt.Sprint("--data-dir=", traildepot),
"run",
fmt.Sprintf("--address=127.0.0.1:%d", PORT),
"--js-runtime-threads=2",
"--runtime-threads=2",
}
cmd := buildCommand("cargo", cwd, args...)
cmd.Start()

View File

@@ -38,7 +38,7 @@ class TrailBaseFixture:
"run",
"-a",
address,
"--js-runtime-threads",
"--runtime-threads",
"1",
]
)

View File

@@ -56,7 +56,7 @@ func startTrailBase() async throws -> ProcessIdentifier {
"--data-dir=\(depotPath)",
"run",
"--address=127.0.0.1:\(PORT)",
"--js-runtime-threads=2",
"--runtime-threads=2",
]
let process = try Subprocess.runDetached(

View File

@@ -117,7 +117,18 @@ addRoute(
return await response.text();
}
throw new HttpError(StatusCodes.BAD_REQUEST, "Missing ?url param");
throw new HttpError(StatusCodes.BAD_REQUEST, `Missing ?url param: ${req.params}`);
}),
);
addRoute(
"GET",
"/fibonacci",
stringHandler((req: StringRequestType) => {
const uri: ParsedPath = parsePath(req.uri);
const n = uri.query.get("n");
return fibonacci(n ? parseInt(n) : 40).toString();
}),
);
@@ -160,17 +171,28 @@ class Completer<T> {
const completer = new Completer<string>();
addCronCallback("JS-registered Job", "@hourly", async () => {
console.info("JS-registered cron job reporting for duty 🚀");
});
addPeriodicCallback(100, (cancel) => {
completer.complete("resolved");
cancel();
});
addCronCallback("JS-registered Job", "@hourly", async () => {
console.info("JS-registered cron job reporting for duty 🚀");
});
addRoute(
"GET",
"/await",
stringHandler(async (_req) => await completer.promise),
);
function fibonacci(num: number): number {
switch (num) {
case 0:
return 0;
case 1:
return 1;
default:
return fibonacci(num - 1) + fibonacci(num - 2);
}
}

View File

@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpContextKind } from "./HttpContextKind";
import type { HttpContextUser } from "./HttpContextUser";
export type HttpContext = { kind: HttpContextKind, registered_path: string, path_params: Array<[string, string]>, user: HttpContextUser | null, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HttpContextKind = "Http" | "Job";

View File

@@ -0,0 +1,15 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HttpContextUser = {
/**
* Url-safe Base64 encoded id of the current user.
*/
id: string,
/**
* E-mail of the current user.
*/
email: string,
/**
* The "expected" CSRF token as included in the auth token claims [User] was constructed from.
*/
csrf_token: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "./serde_json/JsonValue";
export type SqliteRequest = { query: string, params: Array<JsonValue>, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "./serde_json/JsonValue";
export type SqliteResponse = { "Query": { rows: Array<Array<JsonValue>>, } } | { "Execute": { rows_affected: number, } } | { "Error": string } | "TxBegin" | "TxCommit" | "TxRollback";

View File

@@ -28,7 +28,7 @@ async function initTrailBase(): Promise<{ subprocess: Subprocess }> {
cwd: root,
stdout: process.stdout,
stderr: process.stdout,
})`cargo run -- --data-dir client/testfixture --public-url http://${ADDRESS} run -a ${ADDRESS} --js-runtime-threads 1`;
})`cargo run -- --data-dir client/testfixture --public-url http://${ADDRESS} run -a ${ADDRESS} --runtime-threads 1`;
for (let i = 0; i < 100; ++i) {
if ((subprocess.exitCode ?? 0) > 0) {

View File

@@ -93,6 +93,10 @@ pub struct ServerArgs {
#[arg(long, env)]
pub public_dir: Option<String>,
/// Optional path to sandboxed FS root for WASM runtime.
#[arg(long, env)]
pub runtime_root_fs: Option<String>,
/// Optional path to MaxmindDB geoip database. Can be used to map logged IPs to a geo location.
#[arg(long, env)]
pub geoip_db_path: Option<String>,
@@ -119,7 +123,7 @@ pub struct ServerArgs {
/// Number of JavaScript isolates/workers to start (Default: #cpus).
#[arg(long, env)]
pub js_runtime_threads: Option<usize>,
pub runtime_threads: Option<usize>,
}
#[derive(Args, Clone, Debug)]

View File

@@ -73,13 +73,14 @@ async fn async_main() -> Result<(), BoxError> {
address: cmd.address,
admin_address: cmd.admin_address,
public_dir: cmd.public_dir.map(|p| p.into()),
runtime_root_fs: cmd.runtime_root_fs.map(|p| p.into()),
geoip_db_path: cmd.geoip_db_path.map(|p| p.into()),
log_responses: cmd.dev || cmd.stderr_logging,
dev: cmd.dev,
demo: cmd.demo,
disable_auth_ui: cmd.disable_auth_ui,
cors_allowed_origins: cmd.cors_allowed_origins,
js_runtime_threads: cmd.js_runtime_threads,
runtime_threads: cmd.runtime_threads,
tls_key: None,
tls_cert: None,
})

View File

@@ -35,7 +35,7 @@ fn start_server() -> Result<Server, std::io::Error> {
format!("--data-dir={depot_path}"),
"run".to_string(),
format!("--address=127.0.0.1:{PORT}"),
"--js-runtime-threads=2".to_string(),
"--runtime-threads=2".to_string(),
];
let child = std::process::Command::new("cargo")
.args(&args)

View File

@@ -7,7 +7,7 @@ rust-version = "1.86"
description = "Package to use TrailBase as a framework"
homepage = "https://trailbase.io"
repository = "https://github.com/trailbaseio/trailbase"
readme = "../README.md"
readme = "../../README.md"
exclude = [
"benches/",
"tests/",
@@ -89,7 +89,9 @@ trailbase-qs = { workspace = true }
trailbase-refinery = { workspace = true }
trailbase-schema = { workspace = true }
trailbase-sqlite = { workspace = true }
ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] }
trailbase-wasm-runtime-host = { workspace = true }
trailbase-wasm-common = { workspace = true }
ts-rs = { workspace = true }
url = { version = "^2.4.1", default-features = false }
utoipa = { version = "5.0.0-beta.0", features = ["axum_extras"] }
uuid = { workspace = true }

View File

@@ -4,6 +4,7 @@ use reactivate::{Merge, Reactive};
use std::path::PathBuf;
use std::sync::Arc;
use trailbase_schema::QualifiedName;
use trailbase_wasm_runtime_host::Runtime;
use crate::auth::jwt::JwtHelper;
use crate::auth::options::AuthOptions;
@@ -43,6 +44,8 @@ struct InternalState {
runtime: RuntimeHandle,
wasm_runtimes: Vec<Arc<Runtime>>,
#[cfg(test)]
#[allow(unused)]
cleanup: Vec<Box<dyn std::any::Any + Send + Sync>>,
@@ -52,6 +55,7 @@ pub(crate) struct AppStateArgs {
pub data_dir: DataDir,
pub public_url: Option<url::Url>,
pub public_dir: Option<PathBuf>,
pub runtime_root_fs: Option<PathBuf>,
pub dev: bool,
pub demo: bool,
pub schema_metadata: SchemaMetadataCache,
@@ -60,7 +64,7 @@ pub(crate) struct AppStateArgs {
pub logs_conn: trailbase_sqlite::Connection,
pub jwt: JwtHelper,
pub object_store: Box<dyn ObjectStore + Send + Sync>,
pub js_runtime_threads: Option<usize>,
pub runtime_threads: Option<usize>,
}
#[derive(Clone)]
@@ -124,7 +128,14 @@ impl AppState {
object_store.clone(),
);
let runtime = build_js_runtime(args.conn.clone(), args.js_runtime_threads);
let runtime = build_js_runtime(args.conn.clone(), args.runtime_threads);
let wasm_runtimes = crate::wasm::build_wasm_runtimes_for_components(
args.runtime_threads,
args.conn.clone(),
args.data_dir.root().join("wasm"),
args.runtime_root_fs,
)
.expect("startup");
AppState {
state: Arc::new(InternalState {
@@ -163,6 +174,7 @@ impl AppState {
schema_metadata,
object_store,
runtime,
wasm_runtimes,
#[cfg(test)]
cleanup: vec![],
}),
@@ -311,6 +323,10 @@ impl AppState {
pub(crate) fn script_runtime(&self) -> RuntimeHandle {
return self.state.runtime.clone();
}
pub(crate) fn wasm_runtimes(&self) -> &[Arc<Runtime>] {
return &self.state.wasm_runtimes;
}
}
#[cfg(test)]
@@ -458,6 +474,7 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
schema_metadata,
object_store,
runtime: build_js_runtime(conn, None),
wasm_runtimes: vec![],
cleanup: vec![Box::new(temp_dir)],
}),
});

View File

@@ -82,7 +82,7 @@ impl DbUser {
/// Representing an authenticated and *valid* user, as opposed to DbUser, which is merely an entry
/// for any user including users that haven't been validated.
#[derive(Debug, Clone)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct User {
/// Url-safe Base64 encoded id of the current user.
pub id: String,

View File

@@ -67,6 +67,7 @@ impl DataDir {
self.migrations_path(),
self.uploads_path(),
self.key_path(),
self.root().join("wasm/"),
];
}
@@ -101,4 +102,13 @@ backups/
data/
secrets/
uploads/
wasm/
scripts/
# Runtime files, will be overriden by `trail`.
trailbase.d.ts
trailbase.js
# Any potential MaxMind GeoIP dbs.
*.mmdb
"#;

View File

@@ -7,6 +7,7 @@ use axum::response::{IntoResponse, Response};
use futures_util::FutureExt;
use log::*;
use serde::Deserialize;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;
use tokio::sync::oneshot;
@@ -324,6 +325,7 @@ async fn install_routes_and_jobs(
pub(crate) async fn load_routes_and_jobs_from_js_modules(
state: &AppState,
scripts_dir: PathBuf,
) -> Result<Option<Router<AppState>>, AnyError> {
let runtime_handle = state.script_runtime();
if runtime_handle.num_threads() == 0 {
@@ -331,7 +333,6 @@ pub(crate) async fn load_routes_and_jobs_from_js_modules(
return Ok(None);
}
let scripts_dir = state.data_dir().root().join("scripts");
let modules = match Module::load_dir(scripts_dir.clone()) {
Ok(modules) => modules,
Err(err) => {

View File

@@ -22,6 +22,7 @@ mod scheduler;
mod schema_metadata;
mod server;
mod transaction;
mod wasm;
#[cfg(test)]
mod test;

View File

@@ -61,8 +61,9 @@ mod tests {
pub fn to_message(v: serde_json::Value) -> Message {
return match v {
serde_json::Value::Object(ref obj) => {
let keys: Vec<&str> = obj.keys().map(|s| s.as_str()).collect();
assert_eq!(keys, vec!["mid", "room", "data", "table"], "Got: {keys:?}");
let mut keys: Vec<&str> = obj.keys().map(|s| s.as_str()).collect();
keys.sort();
assert_eq!(keys, ["data", "mid", "room", "table"], "Got: {keys:?}");
serde_json::from_value::<Message>(v).unwrap()
}
_ => panic!("expected object, got {v:?}"),

View File

@@ -43,12 +43,13 @@ pub struct InitArgs {
pub data_dir: DataDir,
pub public_url: Option<url::Url>,
pub public_dir: Option<PathBuf>,
pub runtime_root_fs: Option<PathBuf>,
pub geoip_db_path: Option<PathBuf>,
pub address: String,
pub dev: bool,
pub demo: bool,
pub js_runtime_threads: Option<usize>,
pub runtime_threads: Option<usize>,
}
pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitError> {
@@ -138,6 +139,7 @@ pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitErro
data_dir: args.data_dir.clone(),
public_url: args.public_url,
public_dir: args.public_dir,
runtime_root_fs: args.runtime_root_fs,
dev: args.dev,
demo: args.demo,
schema_metadata,
@@ -146,7 +148,7 @@ pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitErro
logs_conn,
jwt,
object_store,
js_runtime_threads: args.js_runtime_threads,
runtime_threads: args.runtime_threads,
});
if new_db {

View File

@@ -54,6 +54,9 @@ pub struct ServerOptions {
/// Optional path to static assets that will be served at the HTTP root.
pub public_dir: Option<PathBuf>,
/// Optional path to sandboxed FS root for WASM runtime.
pub runtime_root_fs: Option<PathBuf>,
/// Optional path to MaxmindDB geoip database. Can be used to map logged IPs to a geo location.
pub geoip_db_path: Option<PathBuf>,
@@ -75,7 +78,7 @@ pub struct ServerOptions {
pub cors_allowed_origins: Vec<String>,
/// Number of V8 worker threads. If set to None, default of num available cores will be used.
pub js_runtime_threads: Option<usize>,
pub runtime_threads: Option<usize>,
/// TLS certificate path.
pub tls_cert: Option<CertificateDer<'static>>,
@@ -120,15 +123,20 @@ impl Server {
date = version_info.commit_date.unwrap_or_default(),
);
validate_path(opts.public_dir.as_ref())?;
validate_path(opts.runtime_root_fs.as_ref())?;
validate_path(opts.geoip_db_path.as_ref())?;
let (new_data_dir, state) = init::init_app_state(InitArgs {
data_dir: opts.data_dir.clone(),
public_url: opts.public_url.clone(),
public_dir: opts.public_dir.clone(),
runtime_root_fs: opts.runtime_root_fs.clone(),
geoip_db_path: opts.geoip_db_path.clone(),
address: opts.address.clone(),
dev: opts.dev,
demo: opts.demo,
js_runtime_threads: opts.js_runtime_threads,
runtime_threads: opts.runtime_threads,
})
.await?;
@@ -170,18 +178,31 @@ impl Server {
.map_err(|err| InitError::CustomInit(err.to_string()))?;
}
#[cfg(feature = "v8")]
let js_routes: Option<Router<AppState>> =
crate::js::runtime::load_routes_and_jobs_from_js_modules(&state)
.await
.map_err(|err| InitError::ScriptError(err.to_string()))?;
let mut custom_routers: Vec<Router<AppState>> = vec![];
#[cfg(not(feature = "v8"))]
let js_routes: Option<Router<AppState>> = None;
#[cfg(feature = "v8")]
if let Some(js_router) = crate::js::runtime::load_routes_and_jobs_from_js_modules(
&state,
state.data_dir().root().join("scripts"),
)
.await
.map_err(|err| InitError::ScriptError(err.to_string()))?
{
custom_routers.push(js_router);
}
for rt in state.wasm_runtimes() {
if let Some(wasm_router) = crate::wasm::install_routes_and_jobs(&state, rt.clone())
.await
.map_err(|err| InitError::ScriptError(err.to_string()))?
{
custom_routers.push(wasm_router);
}
}
Ok(Self {
state: state.clone(),
main_router: Self::build_main_router(&state, &opts, js_routes).await,
main_router: Self::build_main_router(&state, &opts, custom_routers).await,
admin_router: Self::build_independent_admin_router(&state, &opts),
tls: Self::load_tls(&opts),
})
@@ -200,6 +221,7 @@ impl Server {
stream.recv().await;
// TODO: Re-load JS/TS.
// TODO: Re-load WASM.
info!("Received SIGHUP: re-apply migations then re-load config.");
// Re-apply migrations. This needs to happen before reloading the config, which is
@@ -333,7 +355,7 @@ impl Server {
async fn build_main_router(
state: &AppState,
opts: &ServerOptions,
custom_router: Option<Router<AppState>>,
custom_routers: Vec<Router<AppState>>,
) -> (String, Router<()>) {
let enable_transactions =
state.access_config(|conn| conn.server.enable_record_transactions.unwrap_or(false));
@@ -352,7 +374,7 @@ impl Server {
router = router.merge(auth::auth_ui_router());
}
if let Some(custom_router) = custom_router {
for custom_router in custom_routers {
router = router.merge(custom_router);
}
@@ -579,6 +601,11 @@ async fn start_listen(
}
};
#[cfg(not(feature = "v8"))]
tokio_rustls::rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install TLS provider");
let server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert], key)
@@ -616,3 +643,12 @@ async fn start_listen(
}
};
}
fn validate_path(path: Option<&PathBuf>) -> Result<(), InitError> {
if let Some(path) = path {
if !std::fs::exists(path)? {
return Err(InitError::CustomInit(format!("Path not found: {path:?}")));
};
}
return Ok(());
}

230
crates/core/src/wasm/mod.rs Normal file
View File

@@ -0,0 +1,230 @@
use axum::Router;
use axum::extract::{RawPathParams, Request};
use bytes::Bytes;
use http_body_util::{BodyExt, combinators::BoxBody};
use hyper::StatusCode;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use trailbase_wasm_common::{HttpContext, HttpContextKind, HttpContextUser};
use trailbase_wasm_runtime_host::exports::trailbase::runtime::init_endpoint::MethodType;
use trailbase_wasm_runtime_host::{Error as WasmError, KvStore, Runtime};
use crate::AppState;
use crate::User;
type AnyError = Box<dyn std::error::Error + Send + Sync>;
pub(crate) fn build_wasm_runtimes_for_components(
n_threads: Option<usize>,
conn: trailbase_sqlite::Connection,
components_path: PathBuf,
fs_root_path: Option<PathBuf>,
) -> Result<Vec<Arc<Runtime>>, AnyError> {
let shared_kv_store = KvStore::new();
let mut runtimes: Vec<Arc<Runtime>> = vec![];
if let Ok(entries) = std::fs::read_dir(components_path) {
for entry in entries {
let Ok(entry) = entry else {
continue;
};
let Ok(metadata) = entry.metadata() else {
continue;
};
if !metadata.is_file() {
continue;
}
let path = entry.path();
let Some(extension) = path.extension().and_then(|e| e.to_str()) else {
continue;
};
if extension == "wasm" {
let n_threads = n_threads
.or(std::thread::available_parallelism().ok().map(|n| n.get()))
.unwrap_or(1);
runtimes.push(Arc::new(Runtime::new(
n_threads,
path,
conn.clone(),
shared_kv_store.clone(),
fs_root_path.clone(),
)?));
}
}
}
if runtimes.is_empty() {
log::debug!("No WASM component found");
}
return Ok(runtimes);
}
pub(crate) async fn install_routes_and_jobs(
state: &AppState,
runtime: Arc<Runtime>,
) -> Result<Option<Router<AppState>>, AnyError> {
let init_result = runtime
.call(async |instance| {
return instance.call_init().await;
})
.await??;
for (name, spec) in &init_result.job_handlers {
let schedule = cron::Schedule::from_str(spec)?;
let runtime = runtime.clone();
let name_clone = name.to_string();
let Some(job) = state.jobs().new_job(
None,
name,
schedule,
crate::scheduler::build_callback(move || {
let name = name_clone.clone();
let runtime = runtime.clone();
return async move {
runtime
.call(async move |instance| -> Result<(), WasmError> {
let request = hyper::Request::builder()
// NOTE: We cannot use a custom-scheme, since the wasi http
// implementation rejects everything but http and https.
.uri(format!("http://__job/?name={name}"))
.header(
"__context",
to_header_value(&HttpContext {
kind: HttpContextKind::Job,
registered_path: name.clone(),
path_params: vec![],
user: None,
})?,
)
.body(empty())
.unwrap_or_default();
instance.call_incoming_http_handler(request).await?;
return Ok(());
})
.await??;
Ok::<_, AnyError>(())
};
}),
) else {
return Err("Failed to add job".into());
};
job.start();
}
log::debug!("Got {} WASM routes", init_result.http_handlers.len());
let mut router = Router::<AppState>::new();
for (method, path) in &init_result.http_handlers {
let runtime = runtime.clone();
log::debug!("Installing WASM route: {method:?}: {path}");
let handler = {
let path = path.clone();
move |params: RawPathParams, user: Option<User>, req: Request| async move {
log::debug!(
"Host received WASM HTTP request: {params:?}, {user:?}, {}",
req.uri()
);
let result = runtime
.call(
async move |instance| -> Result<axum::response::Response, WasmError> {
let (mut parts, body) = req.into_parts();
let bytes = body
.collect()
.await
.map_err(|_err| WasmError::ChannelClosed)?
.to_bytes();
parts.headers.insert(
"__context",
to_header_value(&HttpContext {
kind: HttpContextKind::Http,
registered_path: path.clone(),
path_params: params
.iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect(),
user: user.map(|u| HttpContextUser {
id: u.id,
email: u.email,
csrf_token: u.csrf_token,
}),
})?,
);
let request = hyper::Request::from_parts(
parts,
BoxBody::new(http_body_util::Full::new(bytes).map_err(|_| unreachable!())),
);
let response = instance.call_incoming_http_handler(request).await?;
let (parts, body) = response.into_parts();
let bytes = body
.collect()
.await
.map_err(|_err| WasmError::ChannelClosed)?
.to_bytes();
return Ok(axum::response::Response::from_parts(parts, bytes.into()));
},
)
.await;
return match result {
Ok(Ok(r)) => r,
Ok(Err(err)) => internal_error_response(err),
Err(err) => internal_error_response(err),
};
}
};
router = router.route(
path,
match method {
MethodType::Delete => axum::routing::delete(handler),
MethodType::Get => axum::routing::get(handler),
MethodType::Head => axum::routing::head(handler),
MethodType::Options => axum::routing::options(handler),
MethodType::Patch => axum::routing::patch(handler),
MethodType::Post => axum::routing::post(handler),
MethodType::Put => axum::routing::put(handler),
MethodType::Trace => axum::routing::trace(handler),
MethodType::Connect => axum::routing::connect(handler),
},
);
}
return Ok(Some(router));
}
fn empty() -> BoxBody<Bytes, hyper::Error> {
return BoxBody::new(http_body_util::Empty::new().map_err(|_| unreachable!()));
}
fn to_header_value(context: &HttpContext) -> Result<hyper::http::HeaderValue, WasmError> {
return hyper::http::HeaderValue::from_bytes(&serde_json::to_vec(&context).unwrap_or_default())
.map_err(|_err| WasmError::Encoding);
}
fn internal_error_response(err: impl std::string::ToString) -> axum::response::Response {
return axum::response::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(err.to_string().into())
.unwrap_or_default();
}

View File

@@ -26,7 +26,7 @@ fn test_admin_permissions() {
dev: false,
disable_auth_ui: false,
cors_allowed_origins: vec![],
js_runtime_threads: None,
runtime_threads: None,
..Default::default()
})
.await

View File

@@ -48,7 +48,7 @@ async fn test_record_apis() {
dev: false,
disable_auth_ui: false,
cors_allowed_origins: vec![],
js_runtime_threads: None,
runtime_threads: None,
..Default::default()
})
.await

View File

@@ -32,7 +32,7 @@ fn test_https_serving() {
dev: false,
disable_auth_ui: false,
cors_allowed_origins: vec![],
js_runtime_threads: None,
runtime_threads: None,
tls_key: Some(tls_key),
tls_cert: Some(cert.der().clone()),
..Default::default()

View File

@@ -26,7 +26,7 @@ sqlite3-parser = "0.15.0"
thiserror = "2.0.12"
tokio = { workspace = true }
trailbase-extension = { workspace = true }
ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] }
ts-rs = { workspace = true }
uuid = { workspace = true }
[dev-dependencies]

View File

@@ -0,0 +1,17 @@
# Fork of wasmtime/crates/wasi-keyvalue.
[package]
name = "trailbase-wasi-keyvalue"
version = "0.1.0"
edition = "2024"
license = "OSL-3.0"
description = "Sync WASI KV store - fork of upstream"
homepage = "https://trailbase.io"
[dependencies]
anyhow = "1.0.99"
parking_lot = { workspace = true }
wasmtime = { workspace = true }
[dev-dependencies]
wasmtime-wasi = { workspace = true }
tokio = { workspace = true }

View File

@@ -0,0 +1,233 @@
//! # Wasmtime's [wasi-keyvalue] Implementation
//!
//! This crate provides a Wasmtime host implementation of the [wasi-keyvalue]
//! API. With this crate, the runtime can run components that call APIs in
//! [wasi-keyvalue] and provide components with access to key-value storages.
//!
//! Currently supported storage backends:
//! * In-Memory (empty identifier)
#![allow(clippy::needless_return)]
#![deny(missing_docs)]
#![forbid(clippy::unwrap_used)]
#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
mod generated {
wasmtime::component::bindgen!({
path: "wit",
world: "wasi:keyvalue/imports",
imports: { default: trappable },
with: {
"wasi:keyvalue/store/bucket": crate::Bucket,
},
trappable_error_type: {
"wasi:keyvalue/store/error" => crate::Error,
},
});
}
use self::generated::wasi::keyvalue;
use anyhow::Result;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use wasmtime::component::{HasData, Resource, ResourceTable, ResourceTableError};
#[doc(hidden)]
pub enum Error {
NoSuchStore,
AccessDenied,
Other(String),
}
impl From<ResourceTableError> for Error {
fn from(err: ResourceTableError) -> Self {
Self::Other(err.to_string())
}
}
type InternalStore = Arc<RwLock<HashMap<String, Vec<u8>>>>;
/// The practical type for the inmemory Store.
#[derive(Clone, Default)]
pub struct Store {
store: Arc<RwLock<HashMap<String, Vec<u8>>>>,
}
impl Store {
/// New shared storage for WASI KV implementation.
pub fn new() -> Self {
return Store {
store: Arc::new(RwLock::new(HashMap::new())),
};
}
}
#[doc(hidden)]
pub struct Bucket {
in_memory_data: InternalStore,
}
/// Capture the state necessary for use in the `wasi-keyvalue` API implementation.
pub struct WasiKeyValueCtx {
in_memory_data: InternalStore,
}
impl WasiKeyValueCtx {
/// Inject shared data.
pub fn new(data: Store) -> Self {
return Self {
in_memory_data: data.store,
};
}
}
/// A wrapper capturing the needed internal `wasi-keyvalue` state.
pub struct WasiKeyValue<'a> {
ctx: &'a WasiKeyValueCtx,
table: &'a mut ResourceTable,
}
impl<'a> WasiKeyValue<'a> {
/// Create a new view into the `wasi-keyvalue` state.
pub fn new(ctx: &'a WasiKeyValueCtx, table: &'a mut ResourceTable) -> Self {
Self { ctx, table }
}
}
impl keyvalue::store::Host for WasiKeyValue<'_> {
fn open(&mut self, identifier: String) -> Result<Resource<Bucket>, Error> {
match identifier.as_str() {
"" => Ok(self.table.push(Bucket {
in_memory_data: self.ctx.in_memory_data.clone(),
})?),
_ => Err(Error::NoSuchStore),
}
}
fn convert_error(&mut self, err: Error) -> Result<keyvalue::store::Error> {
match err {
Error::NoSuchStore => Ok(keyvalue::store::Error::NoSuchStore),
Error::AccessDenied => Ok(keyvalue::store::Error::AccessDenied),
Error::Other(e) => Ok(keyvalue::store::Error::Other(e)),
}
}
}
impl keyvalue::store::HostBucket for WasiKeyValue<'_> {
fn get(&mut self, bucket: Resource<Bucket>, key: String) -> Result<Option<Vec<u8>>, Error> {
let bucket = self.table.get_mut(&bucket)?;
Ok(bucket.in_memory_data.read().get(&key).cloned())
}
fn set(&mut self, bucket: Resource<Bucket>, key: String, value: Vec<u8>) -> Result<(), Error> {
let bucket = self.table.get_mut(&bucket)?;
bucket.in_memory_data.write().insert(key, value);
Ok(())
}
fn delete(&mut self, bucket: Resource<Bucket>, key: String) -> Result<(), Error> {
let bucket = self.table.get_mut(&bucket)?;
bucket.in_memory_data.write().remove(&key);
Ok(())
}
fn exists(&mut self, bucket: Resource<Bucket>, key: String) -> Result<bool, Error> {
let bucket = self.table.get_mut(&bucket)?;
Ok(bucket.in_memory_data.read().contains_key(&key))
}
fn list_keys(
&mut self,
bucket: Resource<Bucket>,
cursor: Option<u64>,
) -> Result<keyvalue::store::KeyResponse, Error> {
let bucket = self.table.get_mut(&bucket)?;
let keys: Vec<String> = bucket.in_memory_data.read().keys().cloned().collect();
let cursor = cursor.unwrap_or(0) as usize;
let keys_slice = &keys[cursor..];
Ok(keyvalue::store::KeyResponse {
keys: keys_slice.to_vec(),
cursor: None,
})
}
fn drop(&mut self, bucket: Resource<Bucket>) -> Result<()> {
self.table.delete(bucket)?;
Ok(())
}
}
impl keyvalue::atomics::Host for WasiKeyValue<'_> {
fn increment(&mut self, bucket: Resource<Bucket>, key: String, delta: u64) -> Result<u64, Error> {
let bucket = self.table.get_mut(&bucket)?;
let mut data = bucket.in_memory_data.write();
let value = data.entry(key.clone()).or_insert(b"0".to_vec());
let current_value = String::from_utf8(value.clone())
.map_err(|e| Error::Other(e.to_string()))?
.parse::<u64>()
.map_err(|e| Error::Other(e.to_string()))?;
let new_value = current_value + delta;
*value = new_value.to_string().into_bytes();
Ok(new_value)
}
}
impl keyvalue::batch::Host for WasiKeyValue<'_> {
fn get_many(
&mut self,
bucket: Resource<Bucket>,
keys: Vec<String>,
) -> Result<Vec<Option<(String, Vec<u8>)>>, Error> {
let bucket = self.table.get_mut(&bucket)?;
let lock = bucket.in_memory_data.read();
Ok(
keys
.into_iter()
.map(|key| lock.get(&key).map(|value| (key.clone(), value.clone())))
.collect(),
)
}
fn set_many(
&mut self,
bucket: Resource<Bucket>,
key_values: Vec<(String, Vec<u8>)>,
) -> Result<(), Error> {
let bucket = self.table.get_mut(&bucket)?;
let mut lock = bucket.in_memory_data.write();
for (key, value) in key_values {
lock.insert(key, value);
}
Ok(())
}
fn delete_many(&mut self, bucket: Resource<Bucket>, keys: Vec<String>) -> Result<(), Error> {
let bucket = self.table.get_mut(&bucket)?;
let mut lock = bucket.in_memory_data.write();
for key in keys {
lock.remove(&key);
}
Ok(())
}
}
/// Add all the `wasi-keyvalue` world's interfaces to a [`wasmtime::component::Linker`].
pub fn add_to_linker<T: Send + 'static>(
l: &mut wasmtime::component::Linker<T>,
f: fn(&mut T) -> WasiKeyValue<'_>,
) -> Result<()> {
keyvalue::store::add_to_linker::<_, HasWasiKeyValue>(l, f)?;
keyvalue::atomics::add_to_linker::<_, HasWasiKeyValue>(l, f)?;
keyvalue::batch::add_to_linker::<_, HasWasiKeyValue>(l, f)?;
Ok(())
}
struct HasWasiKeyValue;
impl HasData for HasWasiKeyValue {
type Data<'a> = WasiKeyValue<'a>;
}

View File

@@ -0,0 +1,22 @@
/// A keyvalue interface that provides atomic operations.
///
/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to
/// fail, it will appear to the invoker of the atomic operation that the action either completed
/// successfully or did nothing at all.
///
/// Please note that this interface is bare functions that take a reference to a bucket. This is to
/// get around the current lack of a way to "extend" a resource with additional methods inside of
/// wit. Future version of the interface will instead extend these methods on the base `bucket`
/// resource.
interface atomics {
use store.{bucket, error};
/// Atomically increment the value associated with the key in the store by the given delta. It
/// returns the new value.
///
/// If the key does not exist in the store, it creates a new key-value pair with the value set
/// to the given delta.
///
/// If any other error occurs, it returns an `Err(error)`.
increment: func(bucket: borrow<bucket>, key: string, delta: u64) -> result<u64, error>;
}

View File

@@ -0,0 +1,63 @@
/// A keyvalue interface that provides batch operations.
///
/// A batch operation is an operation that operates on multiple keys at once.
///
/// Batch operations are useful for reducing network round-trip time. For example, if you want to
/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1
/// batch get operation. The batch operation is faster because it only needs to make 1 network call
/// instead of 100.
///
/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some
/// of the keys may have been modified and some may not.
///
/// This interface does has the same consistency guarantees as the `store` interface, meaning that
/// you should be able to "read your writes."
///
/// Please note that this interface is bare functions that take a reference to a bucket. This is to
/// get around the current lack of a way to "extend" a resource with additional methods inside of
/// wit. Future version of the interface will instead extend these methods on the base `bucket`
/// resource.
interface batch {
use store.{bucket, error};
/// Get the key-value pairs associated with the keys in the store. It returns a list of
/// key-value pairs.
///
/// If any of the keys do not exist in the store, it returns a `none` value for that pair in the
/// list.
///
/// MAY show an out-of-date value if there are concurrent writes to the store.
///
/// If any other error occurs, it returns an `Err(error)`.
get-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<list<option<tuple<string, list<u8>>>>, error>;
/// Set the values associated with the keys in the store. If the key already exists in the
/// store, it overwrites the value.
///
/// Note that the key-value pairs are not guaranteed to be set in the order they are provided.
///
/// If any of the keys do not exist in the store, it creates a new key-value pair.
///
/// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
/// rollback the key-value pairs that were already set. Thus, this batch operation does not
/// guarantee atomicity, implying that some key-value pairs could be set while others might
/// fail.
///
/// Other concurrent operations may also be able to see the partial results.
set-many: func(bucket: borrow<bucket>, key-values: list<tuple<string, list<u8>>>) -> result<_, error>;
/// Delete the key-value pairs associated with the keys in the store.
///
/// Note that the key-value pairs are not guaranteed to be deleted in the order they are
/// provided.
///
/// If any of the keys do not exist in the store, it skips the key.
///
/// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
/// rollback the key-value pairs that were already deleted. Thus, this batch operation does not
/// guarantee atomicity, implying that some key-value pairs could be deleted while others might
/// fail.
///
/// Other concurrent operations may also be able to see the partial results.
delete-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<_, error>;
}

View File

@@ -0,0 +1,122 @@
/// A keyvalue interface that provides eventually consistent key-value operations.
///
/// Each of these operations acts on a single key-value pair.
///
/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is
/// the common denominator for all data types defined by different key-value stores to handle data,
/// ensuring compatibility between different key-value stores. Note: the clients will be expecting
/// serialization/deserialization overhead to be handled by the key-value store. The value could be
/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects.
///
/// Data consistency in a key value store refers to the guarantee that once a write operation
/// completes, all subsequent read operations will return the value that was written.
///
/// Any implementation of this interface must have enough consistency to guarantee "reading your
/// writes." In particular, this means that the client should never get a value that is older than
/// the one it wrote, but it MAY get a newer value if one was written around the same time. These
/// guarantees only apply to the same client (which will likely be provided by the host or an
/// external capability of some kind). In this context a "client" is referring to the caller or
/// guest that is consuming this interface. Once a write request is committed by a specific client,
/// all subsequent read requests by the same client will reflect that write or any subsequent
/// writes. Another client running in a different context may or may not immediately see the result
/// due to the replication lag. As an example of all of this, if a value at a given key is A, and
/// the client writes B, then immediately reads, it should get B. If something else writes C in
/// quick succession, then the client may get C. However, a client running in a separate context may
/// still see A or B
interface store {
/// The set of errors which may be raised by functions in this package
variant error {
/// The host does not recognize the store identifier requested.
no-such-store,
/// The requesting component does not have access to the specified store
/// (which may or may not exist).
access-denied,
/// Some implementation-specific error has occurred (e.g. I/O)
other(string)
}
/// A response to a `list-keys` operation.
record key-response {
/// The list of keys returned by the query.
keys: list<string>,
/// The continuation token to use to fetch the next page of keys. If this is `null`, then
/// there are no more keys to fetch.
cursor: option<u64>
}
/// Get the bucket with the specified identifier.
///
/// `identifier` must refer to a bucket provided by the host.
///
/// `error::no-such-store` will be raised if the `identifier` is not recognized.
open: func(identifier: string) -> result<bucket, error>;
/// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the
/// bucket, and the bucket itself acts as a collection of all these entries.
///
/// It is worth noting that the exact terminology for bucket in key-value stores can very
/// depending on the specific implementation. For example:
///
/// 1. Amazon DynamoDB calls a collection of key-value pairs a table
/// 2. Redis has hashes, sets, and sorted sets as different types of collections
/// 3. Cassandra calls a collection of key-value pairs a column family
/// 4. MongoDB calls a collection of key-value pairs a collection
/// 5. Riak calls a collection of key-value pairs a bucket
/// 6. Memcached calls a collection of key-value pairs a slab
/// 7. Azure Cosmos DB calls a collection of key-value pairs a container
///
/// In this interface, we use the term `bucket` to refer to a collection of key-value pairs
resource bucket {
/// Get the value associated with the specified `key`
///
/// The value is returned as an option. If the key-value pair exists in the
/// store, it returns `Ok(value)`. If the key does not exist in the
/// store, it returns `Ok(none)`.
///
/// If any other error occurs, it returns an `Err(error)`.
get: func(key: string) -> result<option<list<u8>>, error>;
/// Set the value associated with the key in the store. If the key already
/// exists in the store, it overwrites the value.
///
/// If the key does not exist in the store, it creates a new key-value pair.
///
/// If any other error occurs, it returns an `Err(error)`.
set: func(key: string, value: list<u8>) -> result<_, error>;
/// Delete the key-value pair associated with the key in the store.
///
/// If the key does not exist in the store, it does nothing.
///
/// If any other error occurs, it returns an `Err(error)`.
delete: func(key: string) -> result<_, error>;
/// Check if the key exists in the store.
///
/// If the key exists in the store, it returns `Ok(true)`. If the key does
/// not exist in the store, it returns `Ok(false)`.
///
/// If any other error occurs, it returns an `Err(error)`.
exists: func(key: string) -> result<bool, error>;
/// Get all the keys in the store with an optional cursor (for use in pagination). It
/// returns a list of keys. Please note that for most KeyValue implementations, this is a
/// can be a very expensive operation and so it should be used judiciously. Implementations
/// can return any number of keys in a single response, but they should never attempt to
/// send more data than is reasonable (i.e. on a small edge device, this may only be a few
/// KB, while on a large machine this could be several MB). Any response should also return
/// a cursor that can be used to fetch the next page of keys. See the `key-response` record
/// for more information.
///
/// Note that the keys are not guaranteed to be returned in any particular order.
///
/// If the store is empty, it returns an empty list.
///
/// MAY show an out-of-date list of keys if there are concurrent writes to the store.
///
/// If any error occurs, it returns an `Err(error)`.
list-keys: func(cursor: option<u64>) -> result<key-response, error>;
}
}

View File

@@ -0,0 +1,16 @@
/// A keyvalue interface that provides watch operations.
///
/// This interface is used to provide event-driven mechanisms to handle
/// keyvalue changes.
interface watcher {
/// A keyvalue interface that provides handle-watch operations.
use store.{bucket};
/// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket`
/// that can be used to interact with the store.
on-set: func(bucket: bucket, key: string, value: list<u8>);
/// Handle the `delete` event for the given bucket and key. It includes a reference to the
/// `bucket` that can be used to interact with the store.
on-delete: func(bucket: bucket, key: string);
}

View File

@@ -0,0 +1,26 @@
package wasi:keyvalue@0.2.0-draft;
/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores.
/// Components targeting this world will be able to do:
///
/// 1. CRUD (create, read, update, delete) operations on key-value stores.
/// 2. Atomic `increment` and CAS (compare-and-swap) operations.
/// 3. Batch operations that can reduce the number of round trips to the network.
world imports {
/// The `store` capability allows the component to perform eventually consistent operations on
/// the key-value store.
import store;
/// The `atomic` capability allows the component to perform atomic / `increment` and CAS
/// (compare-and-swap) operations.
import atomics;
/// The `batch` capability allows the component to perform eventually consistent batch
/// operations that can reduce the number of round trips to the network.
import batch;
}
world watch-service {
include imports;
export watcher;
}

View File

@@ -0,0 +1,6 @@
// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps.
package wasmtime:wasi-keyvalue;
world bindings {
include wasi:keyvalue/imports@0.2.0-draft;
}

View File

@@ -0,0 +1,16 @@
[package]
name = "trailbase-wasm-common"
version = "0.1.0"
edition = "2024"
license = "OSL-3.0"
description = "WASM runtime for the TrailBase framework"
homepage = "https://trailbase.io"
exclude = [
"**/node_modules/",
"**/dist/",
]
[dependencies]
serde = { version = "^1.0.203", features = ["derive"] }
serde_json = "^1.0.117"
ts-rs = { workspace = true }

View File

@@ -0,0 +1,51 @@
#![forbid(unsafe_code, clippy::unwrap_used)]
#![allow(clippy::needless_return)]
#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct SqliteRequest {
pub query: String,
pub params: Vec<serde_json::Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub enum SqliteResponse {
Query { rows: Vec<Vec<serde_json::Value>> },
Execute { rows_affected: usize },
Error(String),
TxBegin,
TxCommit,
TxRollback,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub enum HttpContextKind {
/// An incoming http request.
Http,
/// An incoming job request.
Job,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct HttpContextUser {
/// Url-safe Base64 encoded id of the current user.
pub id: String,
/// E-mail of the current user.
pub email: String,
/// The "expected" CSRF token as included in the auth token claims [User] was constructed from.
pub csrf_token: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct HttpContext {
pub kind: HttpContextKind,
pub registered_path: String,
pub path_params: Vec<(String, String)>,
pub user: Option<HttpContextUser>,
}

View File

@@ -0,0 +1,10 @@
package wasi:cli@0.2.6;
@since(version = 0.2.0)
world command {
@since(version = 0.2.0)
include imports;
@since(version = 0.2.0)
export run;
}

View File

@@ -0,0 +1,22 @@
@since(version = 0.2.0)
interface environment {
/// Get the POSIX-style environment variables.
///
/// Each environment variable is provided as a pair of string variable names
/// and string value.
///
/// Morally, these are a value import, but until value imports are available
/// in the component model, this import function should return the same
/// values each time it is called.
@since(version = 0.2.0)
get-environment: func() -> list<tuple<string, string>>;
/// Get the POSIX-style arguments to the program.
@since(version = 0.2.0)
get-arguments: func() -> list<string>;
/// Return a path that programs should use as their initial current working
/// directory, interpreting `.` as shorthand for this.
@since(version = 0.2.0)
initial-cwd: func() -> option<string>;
}

View File

@@ -0,0 +1,17 @@
@since(version = 0.2.0)
interface exit {
/// Exit the current instance and any linked instances.
@since(version = 0.2.0)
exit: func(status: result);
/// Exit the current instance and any linked instances, reporting the
/// specified status code to the host.
///
/// The meaning of the code depends on the context, with 0 usually meaning
/// "success", and other values indicating various types of failure.
///
/// This function does not return; the effect is analogous to a trap, but
/// without the connotation that something bad has happened.
@unstable(feature = cli-exit-with-code)
exit-with-code: func(status-code: u8);
}

View File

@@ -0,0 +1,36 @@
package wasi:cli@0.2.6;
@since(version = 0.2.0)
world imports {
@since(version = 0.2.0)
include wasi:clocks/imports@0.2.6;
@since(version = 0.2.0)
include wasi:filesystem/imports@0.2.6;
@since(version = 0.2.0)
include wasi:sockets/imports@0.2.6;
@since(version = 0.2.0)
include wasi:random/imports@0.2.6;
@since(version = 0.2.0)
include wasi:io/imports@0.2.6;
@since(version = 0.2.0)
import environment;
@since(version = 0.2.0)
import exit;
@since(version = 0.2.0)
import stdin;
@since(version = 0.2.0)
import stdout;
@since(version = 0.2.0)
import stderr;
@since(version = 0.2.0)
import terminal-input;
@since(version = 0.2.0)
import terminal-output;
@since(version = 0.2.0)
import terminal-stdin;
@since(version = 0.2.0)
import terminal-stdout;
@since(version = 0.2.0)
import terminal-stderr;
}

View File

@@ -0,0 +1,6 @@
@since(version = 0.2.0)
interface run {
/// Run the program.
@since(version = 0.2.0)
run: func() -> result;
}

View File

@@ -0,0 +1,26 @@
@since(version = 0.2.0)
interface stdin {
@since(version = 0.2.0)
use wasi:io/streams@0.2.6.{input-stream};
@since(version = 0.2.0)
get-stdin: func() -> input-stream;
}
@since(version = 0.2.0)
interface stdout {
@since(version = 0.2.0)
use wasi:io/streams@0.2.6.{output-stream};
@since(version = 0.2.0)
get-stdout: func() -> output-stream;
}
@since(version = 0.2.0)
interface stderr {
@since(version = 0.2.0)
use wasi:io/streams@0.2.6.{output-stream};
@since(version = 0.2.0)
get-stderr: func() -> output-stream;
}

View File

@@ -0,0 +1,62 @@
/// Terminal input.
///
/// In the future, this may include functions for disabling echoing,
/// disabling input buffering so that keyboard events are sent through
/// immediately, querying supported features, and so on.
@since(version = 0.2.0)
interface terminal-input {
/// The input side of a terminal.
@since(version = 0.2.0)
resource terminal-input;
}
/// Terminal output.
///
/// In the future, this may include functions for querying the terminal
/// size, being notified of terminal size changes, querying supported
/// features, and so on.
@since(version = 0.2.0)
interface terminal-output {
/// The output side of a terminal.
@since(version = 0.2.0)
resource terminal-output;
}
/// An interface providing an optional `terminal-input` for stdin as a
/// link-time authority.
@since(version = 0.2.0)
interface terminal-stdin {
@since(version = 0.2.0)
use terminal-input.{terminal-input};
/// If stdin is connected to a terminal, return a `terminal-input` handle
/// allowing further interaction with it.
@since(version = 0.2.0)
get-terminal-stdin: func() -> option<terminal-input>;
}
/// An interface providing an optional `terminal-output` for stdout as a
/// link-time authority.
@since(version = 0.2.0)
interface terminal-stdout {
@since(version = 0.2.0)
use terminal-output.{terminal-output};
/// If stdout is connected to a terminal, return a `terminal-output` handle
/// allowing further interaction with it.
@since(version = 0.2.0)
get-terminal-stdout: func() -> option<terminal-output>;
}
/// An interface providing an optional `terminal-output` for stderr as a
/// link-time authority.
@since(version = 0.2.0)
interface terminal-stderr {
@since(version = 0.2.0)
use terminal-output.{terminal-output};
/// If stderr is connected to a terminal, return a `terminal-output` handle
/// allowing further interaction with it.
@since(version = 0.2.0)
get-terminal-stderr: func() -> option<terminal-output>;
}

View File

@@ -0,0 +1,50 @@
package wasi:clocks@0.2.6;
/// WASI Monotonic Clock is a clock API intended to let users measure elapsed
/// time.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
///
/// A monotonic clock is a clock which has an unspecified initial value, and
/// successive reads of the clock will produce non-decreasing values.
@since(version = 0.2.0)
interface monotonic-clock {
@since(version = 0.2.0)
use wasi:io/poll@0.2.6.{pollable};
/// An instant in time, in nanoseconds. An instant is relative to an
/// unspecified initial value, and can only be compared to instances from
/// the same monotonic-clock.
@since(version = 0.2.0)
type instant = u64;
/// A duration of time, in nanoseconds.
@since(version = 0.2.0)
type duration = u64;
/// Read the current value of the clock.
///
/// The clock is monotonic, therefore calling this function repeatedly will
/// produce a sequence of non-decreasing values.
@since(version = 0.2.0)
now: func() -> instant;
/// Query the resolution of the clock. Returns the duration of time
/// corresponding to a clock tick.
@since(version = 0.2.0)
resolution: func() -> duration;
/// Create a `pollable` which will resolve once the specified instant
/// has occurred.
@since(version = 0.2.0)
subscribe-instant: func(
when: instant,
) -> pollable;
/// Create a `pollable` that will resolve after the specified duration has
/// elapsed from the time this function is invoked.
@since(version = 0.2.0)
subscribe-duration: func(
when: duration,
) -> pollable;
}

View File

@@ -0,0 +1,55 @@
package wasi:clocks@0.2.6;
@unstable(feature = clocks-timezone)
interface timezone {
@unstable(feature = clocks-timezone)
use wall-clock.{datetime};
/// Return information needed to display the given `datetime`. This includes
/// the UTC offset, the time zone name, and a flag indicating whether
/// daylight saving time is active.
///
/// If the timezone cannot be determined for the given `datetime`, return a
/// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight
/// saving time.
@unstable(feature = clocks-timezone)
display: func(when: datetime) -> timezone-display;
/// The same as `display`, but only return the UTC offset.
@unstable(feature = clocks-timezone)
utc-offset: func(when: datetime) -> s32;
/// Information useful for displaying the timezone of a specific `datetime`.
///
/// This information may vary within a single `timezone` to reflect daylight
/// saving time adjustments.
@unstable(feature = clocks-timezone)
record timezone-display {
/// The number of seconds difference between UTC time and the local
/// time of the timezone.
///
/// The returned value will always be less than 86400 which is the
/// number of seconds in a day (24*60*60).
///
/// In implementations that do not expose an actual time zone, this
/// should return 0.
utc-offset: s32,
/// The abbreviated name of the timezone to display to a user. The name
/// `UTC` indicates Coordinated Universal Time. Otherwise, this should
/// reference local standards for the name of the time zone.
///
/// In implementations that do not expose an actual time zone, this
/// should be the string `UTC`.
///
/// In time zones that do not have an applicable name, a formatted
/// representation of the UTC offset may be returned, such as `-04:00`.
name: string,
/// Whether daylight saving time is active.
///
/// In implementations that do not expose an actual time zone, this
/// should return false.
in-daylight-saving-time: bool,
}
}

View File

@@ -0,0 +1,46 @@
package wasi:clocks@0.2.6;
/// WASI Wall Clock is a clock API intended to let users query the current
/// time. The name "wall" makes an analogy to a "clock on the wall", which
/// is not necessarily monotonic as it may be reset.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
///
/// A wall clock is a clock which measures the date and time according to
/// some external reference.
///
/// External references may be reset, so this clock is not necessarily
/// monotonic, making it unsuitable for measuring elapsed time.
///
/// It is intended for reporting the current date and time for humans.
@since(version = 0.2.0)
interface wall-clock {
/// A time and date in seconds plus nanoseconds.
@since(version = 0.2.0)
record datetime {
seconds: u64,
nanoseconds: u32,
}
/// Read the current value of the clock.
///
/// This clock is not monotonic, therefore calling this function repeatedly
/// will not necessarily produce a sequence of non-decreasing values.
///
/// The returned timestamps represent the number of seconds since
/// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch],
/// also known as [Unix Time].
///
/// The nanoseconds field of the output is always less than 1000000000.
///
/// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16
/// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time
@since(version = 0.2.0)
now: func() -> datetime;
/// Query the resolution of the clock.
///
/// The nanoseconds field of the output is always less than 1000000000.
@since(version = 0.2.0)
resolution: func() -> datetime;
}

View File

@@ -0,0 +1,11 @@
package wasi:clocks@0.2.6;
@since(version = 0.2.0)
world imports {
@since(version = 0.2.0)
import monotonic-clock;
@since(version = 0.2.0)
import wall-clock;
@unstable(feature = clocks-timezone)
import timezone;
}

View File

@@ -0,0 +1,11 @@
package wasi:filesystem@0.2.6;
@since(version = 0.2.0)
interface preopens {
@since(version = 0.2.0)
use types.{descriptor};
/// Return the set of preopened directories, and their paths.
@since(version = 0.2.0)
get-directories: func() -> list<tuple<descriptor, string>>;
}

View File

@@ -0,0 +1,676 @@
package wasi:filesystem@0.2.6;
/// WASI filesystem is a filesystem API primarily intended to let users run WASI
/// programs that access their files on their existing filesystems, without
/// significant overhead.
///
/// It is intended to be roughly portable between Unix-family platforms and
/// Windows, though it does not hide many of the major differences.
///
/// Paths are passed as interface-type `string`s, meaning they must consist of
/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain
/// paths which are not accessible by this API.
///
/// The directory separator in WASI is always the forward-slash (`/`).
///
/// All paths in WASI are relative paths, and are interpreted relative to a
/// `descriptor` referring to a base directory. If a `path` argument to any WASI
/// function starts with `/`, or if any step of resolving a `path`, including
/// `..` and symbolic link steps, reaches a directory outside of the base
/// directory, or reaches a symlink to an absolute or rooted path in the
/// underlying filesystem, the function fails with `error-code::not-permitted`.
///
/// For more information about WASI path resolution and sandboxing, see
/// [WASI filesystem path resolution].
///
/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md
@since(version = 0.2.0)
interface types {
@since(version = 0.2.0)
use wasi:io/streams@0.2.6.{input-stream, output-stream, error};
@since(version = 0.2.0)
use wasi:clocks/wall-clock@0.2.6.{datetime};
/// File size or length of a region within a file.
@since(version = 0.2.0)
type filesize = u64;
/// The type of a filesystem object referenced by a descriptor.
///
/// Note: This was called `filetype` in earlier versions of WASI.
@since(version = 0.2.0)
enum descriptor-type {
/// The type of the descriptor or file is unknown or is different from
/// any of the other types specified.
unknown,
/// The descriptor refers to a block device inode.
block-device,
/// The descriptor refers to a character device inode.
character-device,
/// The descriptor refers to a directory inode.
directory,
/// The descriptor refers to a named pipe.
fifo,
/// The file refers to a symbolic link inode.
symbolic-link,
/// The descriptor refers to a regular file inode.
regular-file,
/// The descriptor refers to a socket.
socket,
}
/// Descriptor flags.
///
/// Note: This was called `fdflags` in earlier versions of WASI.
@since(version = 0.2.0)
flags descriptor-flags {
/// Read mode: Data can be read.
read,
/// Write mode: Data can be written to.
write,
/// Request that writes be performed according to synchronized I/O file
/// integrity completion. The data stored in the file and the file's
/// metadata are synchronized. This is similar to `O_SYNC` in POSIX.
///
/// The precise semantics of this operation have not yet been defined for
/// WASI. At this time, it should be interpreted as a request, and not a
/// requirement.
file-integrity-sync,
/// Request that writes be performed according to synchronized I/O data
/// integrity completion. Only the data stored in the file is
/// synchronized. This is similar to `O_DSYNC` in POSIX.
///
/// The precise semantics of this operation have not yet been defined for
/// WASI. At this time, it should be interpreted as a request, and not a
/// requirement.
data-integrity-sync,
/// Requests that reads be performed at the same level of integrity
/// requested for writes. This is similar to `O_RSYNC` in POSIX.
///
/// The precise semantics of this operation have not yet been defined for
/// WASI. At this time, it should be interpreted as a request, and not a
/// requirement.
requested-write-sync,
/// Mutating directories mode: Directory contents may be mutated.
///
/// When this flag is unset on a descriptor, operations using the
/// descriptor which would create, rename, delete, modify the data or
/// metadata of filesystem objects, or obtain another handle which
/// would permit any of those, shall fail with `error-code::read-only` if
/// they would otherwise succeed.
///
/// This may only be set on directories.
mutate-directory,
}
/// File attributes.
///
/// Note: This was called `filestat` in earlier versions of WASI.
@since(version = 0.2.0)
record descriptor-stat {
/// File type.
%type: descriptor-type,
/// Number of hard links to the file.
link-count: link-count,
/// For regular files, the file size in bytes. For symbolic links, the
/// length in bytes of the pathname contained in the symbolic link.
size: filesize,
/// Last data access timestamp.
///
/// If the `option` is none, the platform doesn't maintain an access
/// timestamp for this file.
data-access-timestamp: option<datetime>,
/// Last data modification timestamp.
///
/// If the `option` is none, the platform doesn't maintain a
/// modification timestamp for this file.
data-modification-timestamp: option<datetime>,
/// Last file status-change timestamp.
///
/// If the `option` is none, the platform doesn't maintain a
/// status-change timestamp for this file.
status-change-timestamp: option<datetime>,
}
/// Flags determining the method of how paths are resolved.
@since(version = 0.2.0)
flags path-flags {
/// As long as the resolved path corresponds to a symbolic link, it is
/// expanded.
symlink-follow,
}
/// Open flags used by `open-at`.
@since(version = 0.2.0)
flags open-flags {
/// Create file if it does not exist, similar to `O_CREAT` in POSIX.
create,
/// Fail if not a directory, similar to `O_DIRECTORY` in POSIX.
directory,
/// Fail if file already exists, similar to `O_EXCL` in POSIX.
exclusive,
/// Truncate file to size 0, similar to `O_TRUNC` in POSIX.
truncate,
}
/// Number of hard links to an inode.
@since(version = 0.2.0)
type link-count = u64;
/// When setting a timestamp, this gives the value to set it to.
@since(version = 0.2.0)
variant new-timestamp {
/// Leave the timestamp set to its previous value.
no-change,
/// Set the timestamp to the current time of the system clock associated
/// with the filesystem.
now,
/// Set the timestamp to the given value.
timestamp(datetime),
}
/// A directory entry.
record directory-entry {
/// The type of the file referred to by this directory entry.
%type: descriptor-type,
/// The name of the object.
name: string,
}
/// Error codes returned by functions, similar to `errno` in POSIX.
/// Not all of these error codes are returned by the functions provided by this
/// API; some are used in higher-level library layers, and others are provided
/// merely for alignment with POSIX.
enum error-code {
/// Permission denied, similar to `EACCES` in POSIX.
access,
/// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX.
would-block,
/// Connection already in progress, similar to `EALREADY` in POSIX.
already,
/// Bad descriptor, similar to `EBADF` in POSIX.
bad-descriptor,
/// Device or resource busy, similar to `EBUSY` in POSIX.
busy,
/// Resource deadlock would occur, similar to `EDEADLK` in POSIX.
deadlock,
/// Storage quota exceeded, similar to `EDQUOT` in POSIX.
quota,
/// File exists, similar to `EEXIST` in POSIX.
exist,
/// File too large, similar to `EFBIG` in POSIX.
file-too-large,
/// Illegal byte sequence, similar to `EILSEQ` in POSIX.
illegal-byte-sequence,
/// Operation in progress, similar to `EINPROGRESS` in POSIX.
in-progress,
/// Interrupted function, similar to `EINTR` in POSIX.
interrupted,
/// Invalid argument, similar to `EINVAL` in POSIX.
invalid,
/// I/O error, similar to `EIO` in POSIX.
io,
/// Is a directory, similar to `EISDIR` in POSIX.
is-directory,
/// Too many levels of symbolic links, similar to `ELOOP` in POSIX.
loop,
/// Too many links, similar to `EMLINK` in POSIX.
too-many-links,
/// Message too large, similar to `EMSGSIZE` in POSIX.
message-size,
/// Filename too long, similar to `ENAMETOOLONG` in POSIX.
name-too-long,
/// No such device, similar to `ENODEV` in POSIX.
no-device,
/// No such file or directory, similar to `ENOENT` in POSIX.
no-entry,
/// No locks available, similar to `ENOLCK` in POSIX.
no-lock,
/// Not enough space, similar to `ENOMEM` in POSIX.
insufficient-memory,
/// No space left on device, similar to `ENOSPC` in POSIX.
insufficient-space,
/// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX.
not-directory,
/// Directory not empty, similar to `ENOTEMPTY` in POSIX.
not-empty,
/// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX.
not-recoverable,
/// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX.
unsupported,
/// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX.
no-tty,
/// No such device or address, similar to `ENXIO` in POSIX.
no-such-device,
/// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX.
overflow,
/// Operation not permitted, similar to `EPERM` in POSIX.
not-permitted,
/// Broken pipe, similar to `EPIPE` in POSIX.
pipe,
/// Read-only file system, similar to `EROFS` in POSIX.
read-only,
/// Invalid seek, similar to `ESPIPE` in POSIX.
invalid-seek,
/// Text file busy, similar to `ETXTBSY` in POSIX.
text-file-busy,
/// Cross-device link, similar to `EXDEV` in POSIX.
cross-device,
}
/// File or memory access pattern advisory information.
@since(version = 0.2.0)
enum advice {
/// The application has no advice to give on its behavior with respect
/// to the specified data.
normal,
/// The application expects to access the specified data sequentially
/// from lower offsets to higher offsets.
sequential,
/// The application expects to access the specified data in a random
/// order.
random,
/// The application expects to access the specified data in the near
/// future.
will-need,
/// The application expects that it will not access the specified data
/// in the near future.
dont-need,
/// The application expects to access the specified data once and then
/// not reuse it thereafter.
no-reuse,
}
/// A 128-bit hash value, split into parts because wasm doesn't have a
/// 128-bit integer type.
@since(version = 0.2.0)
record metadata-hash-value {
/// 64 bits of a 128-bit hash value.
lower: u64,
/// Another 64 bits of a 128-bit hash value.
upper: u64,
}
/// A descriptor is a reference to a filesystem object, which may be a file,
/// directory, named pipe, special file, or other object on which filesystem
/// calls may be made.
@since(version = 0.2.0)
resource descriptor {
/// Return a stream for reading from a file, if available.
///
/// May fail with an error-code describing why the file cannot be read.
///
/// Multiple read, write, and append streams may be active on the same open
/// file and they do not interfere with each other.
///
/// Note: This allows using `read-stream`, which is similar to `read` in POSIX.
@since(version = 0.2.0)
read-via-stream: func(
/// The offset within the file at which to start reading.
offset: filesize,
) -> result<input-stream, error-code>;
/// Return a stream for writing to a file, if available.
///
/// May fail with an error-code describing why the file cannot be written.
///
/// Note: This allows using `write-stream`, which is similar to `write` in
/// POSIX.
@since(version = 0.2.0)
write-via-stream: func(
/// The offset within the file at which to start writing.
offset: filesize,
) -> result<output-stream, error-code>;
/// Return a stream for appending to a file, if available.
///
/// May fail with an error-code describing why the file cannot be appended.
///
/// Note: This allows using `write-stream`, which is similar to `write` with
/// `O_APPEND` in POSIX.
@since(version = 0.2.0)
append-via-stream: func() -> result<output-stream, error-code>;
/// Provide file advisory information on a descriptor.
///
/// This is similar to `posix_fadvise` in POSIX.
@since(version = 0.2.0)
advise: func(
/// The offset within the file to which the advisory applies.
offset: filesize,
/// The length of the region to which the advisory applies.
length: filesize,
/// The advice.
advice: advice
) -> result<_, error-code>;
/// Synchronize the data of a file to disk.
///
/// This function succeeds with no effect if the file descriptor is not
/// opened for writing.
///
/// Note: This is similar to `fdatasync` in POSIX.
@since(version = 0.2.0)
sync-data: func() -> result<_, error-code>;
/// Get flags associated with a descriptor.
///
/// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX.
///
/// Note: This returns the value that was the `fs_flags` value returned
/// from `fdstat_get` in earlier versions of WASI.
@since(version = 0.2.0)
get-flags: func() -> result<descriptor-flags, error-code>;
/// Get the dynamic type of a descriptor.
///
/// Note: This returns the same value as the `type` field of the `fd-stat`
/// returned by `stat`, `stat-at` and similar.
///
/// Note: This returns similar flags to the `st_mode & S_IFMT` value provided
/// by `fstat` in POSIX.
///
/// Note: This returns the value that was the `fs_filetype` value returned
/// from `fdstat_get` in earlier versions of WASI.
@since(version = 0.2.0)
get-type: func() -> result<descriptor-type, error-code>;
/// Adjust the size of an open file. If this increases the file's size, the
/// extra bytes are filled with zeros.
///
/// Note: This was called `fd_filestat_set_size` in earlier versions of WASI.
@since(version = 0.2.0)
set-size: func(size: filesize) -> result<_, error-code>;
/// Adjust the timestamps of an open file or directory.
///
/// Note: This is similar to `futimens` in POSIX.
///
/// Note: This was called `fd_filestat_set_times` in earlier versions of WASI.
@since(version = 0.2.0)
set-times: func(
/// The desired values of the data access timestamp.
data-access-timestamp: new-timestamp,
/// The desired values of the data modification timestamp.
data-modification-timestamp: new-timestamp,
) -> result<_, error-code>;
/// Read from a descriptor, without using and updating the descriptor's offset.
///
/// This function returns a list of bytes containing the data that was
/// read, along with a bool which, when true, indicates that the end of the
/// file was reached. The returned list will contain up to `length` bytes; it
/// may return fewer than requested, if the end of the file is reached or
/// if the I/O operation is interrupted.
///
/// In the future, this may change to return a `stream<u8, error-code>`.
///
/// Note: This is similar to `pread` in POSIX.
@since(version = 0.2.0)
read: func(
/// The maximum number of bytes to read.
length: filesize,
/// The offset within the file at which to read.
offset: filesize,
) -> result<tuple<list<u8>, bool>, error-code>;
/// Write to a descriptor, without using and updating the descriptor's offset.
///
/// It is valid to write past the end of a file; the file is extended to the
/// extent of the write, with bytes between the previous end and the start of
/// the write set to zero.
///
/// In the future, this may change to take a `stream<u8, error-code>`.
///
/// Note: This is similar to `pwrite` in POSIX.
@since(version = 0.2.0)
write: func(
/// Data to write
buffer: list<u8>,
/// The offset within the file at which to write.
offset: filesize,
) -> result<filesize, error-code>;
/// Read directory entries from a directory.
///
/// On filesystems where directories contain entries referring to themselves
/// and their parents, often named `.` and `..` respectively, these entries
/// are omitted.
///
/// This always returns a new stream which starts at the beginning of the
/// directory. Multiple streams may be active on the same directory, and they
/// do not interfere with each other.
@since(version = 0.2.0)
read-directory: func() -> result<directory-entry-stream, error-code>;
/// Synchronize the data and metadata of a file to disk.
///
/// This function succeeds with no effect if the file descriptor is not
/// opened for writing.
///
/// Note: This is similar to `fsync` in POSIX.
@since(version = 0.2.0)
sync: func() -> result<_, error-code>;
/// Create a directory.
///
/// Note: This is similar to `mkdirat` in POSIX.
@since(version = 0.2.0)
create-directory-at: func(
/// The relative path at which to create the directory.
path: string,
) -> result<_, error-code>;
/// Return the attributes of an open file or directory.
///
/// Note: This is similar to `fstat` in POSIX, except that it does not return
/// device and inode information. For testing whether two descriptors refer to
/// the same underlying filesystem object, use `is-same-object`. To obtain
/// additional data that can be used do determine whether a file has been
/// modified, use `metadata-hash`.
///
/// Note: This was called `fd_filestat_get` in earlier versions of WASI.
@since(version = 0.2.0)
stat: func() -> result<descriptor-stat, error-code>;
/// Return the attributes of a file or directory.
///
/// Note: This is similar to `fstatat` in POSIX, except that it does not
/// return device and inode information. See the `stat` description for a
/// discussion of alternatives.
///
/// Note: This was called `path_filestat_get` in earlier versions of WASI.
@since(version = 0.2.0)
stat-at: func(
/// Flags determining the method of how the path is resolved.
path-flags: path-flags,
/// The relative path of the file or directory to inspect.
path: string,
) -> result<descriptor-stat, error-code>;
/// Adjust the timestamps of a file or directory.
///
/// Note: This is similar to `utimensat` in POSIX.
///
/// Note: This was called `path_filestat_set_times` in earlier versions of
/// WASI.
@since(version = 0.2.0)
set-times-at: func(
/// Flags determining the method of how the path is resolved.
path-flags: path-flags,
/// The relative path of the file or directory to operate on.
path: string,
/// The desired values of the data access timestamp.
data-access-timestamp: new-timestamp,
/// The desired values of the data modification timestamp.
data-modification-timestamp: new-timestamp,
) -> result<_, error-code>;
/// Create a hard link.
///
/// Fails with `error-code::no-entry` if the old path does not exist,
/// with `error-code::exist` if the new path already exists, and
/// `error-code::not-permitted` if the old path is not a file.
///
/// Note: This is similar to `linkat` in POSIX.
@since(version = 0.2.0)
link-at: func(
/// Flags determining the method of how the path is resolved.
old-path-flags: path-flags,
/// The relative source path from which to link.
old-path: string,
/// The base directory for `new-path`.
new-descriptor: borrow<descriptor>,
/// The relative destination path at which to create the hard link.
new-path: string,
) -> result<_, error-code>;
/// Open a file or directory.
///
/// If `flags` contains `descriptor-flags::mutate-directory`, and the base
/// descriptor doesn't have `descriptor-flags::mutate-directory` set,
/// `open-at` fails with `error-code::read-only`.
///
/// If `flags` contains `write` or `mutate-directory`, or `open-flags`
/// contains `truncate` or `create`, and the base descriptor doesn't have
/// `descriptor-flags::mutate-directory` set, `open-at` fails with
/// `error-code::read-only`.
///
/// Note: This is similar to `openat` in POSIX.
@since(version = 0.2.0)
open-at: func(
/// Flags determining the method of how the path is resolved.
path-flags: path-flags,
/// The relative path of the object to open.
path: string,
/// The method by which to open the file.
open-flags: open-flags,
/// Flags to use for the resulting descriptor.
%flags: descriptor-flags,
) -> result<descriptor, error-code>;
/// Read the contents of a symbolic link.
///
/// If the contents contain an absolute or rooted path in the underlying
/// filesystem, this function fails with `error-code::not-permitted`.
///
/// Note: This is similar to `readlinkat` in POSIX.
@since(version = 0.2.0)
readlink-at: func(
/// The relative path of the symbolic link from which to read.
path: string,
) -> result<string, error-code>;
/// Remove a directory.
///
/// Return `error-code::not-empty` if the directory is not empty.
///
/// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX.
@since(version = 0.2.0)
remove-directory-at: func(
/// The relative path to a directory to remove.
path: string,
) -> result<_, error-code>;
/// Rename a filesystem object.
///
/// Note: This is similar to `renameat` in POSIX.
@since(version = 0.2.0)
rename-at: func(
/// The relative source path of the file or directory to rename.
old-path: string,
/// The base directory for `new-path`.
new-descriptor: borrow<descriptor>,
/// The relative destination path to which to rename the file or directory.
new-path: string,
) -> result<_, error-code>;
/// Create a symbolic link (also known as a "symlink").
///
/// If `old-path` starts with `/`, the function fails with
/// `error-code::not-permitted`.
///
/// Note: This is similar to `symlinkat` in POSIX.
@since(version = 0.2.0)
symlink-at: func(
/// The contents of the symbolic link.
old-path: string,
/// The relative destination path at which to create the symbolic link.
new-path: string,
) -> result<_, error-code>;
/// Unlink a filesystem object that is not a directory.
///
/// Return `error-code::is-directory` if the path refers to a directory.
/// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX.
@since(version = 0.2.0)
unlink-file-at: func(
/// The relative path to a file to unlink.
path: string,
) -> result<_, error-code>;
/// Test whether two descriptors refer to the same filesystem object.
///
/// In POSIX, this corresponds to testing whether the two descriptors have the
/// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers.
/// wasi-filesystem does not expose device and inode numbers, so this function
/// may be used instead.
@since(version = 0.2.0)
is-same-object: func(other: borrow<descriptor>) -> bool;
/// Return a hash of the metadata associated with a filesystem object referred
/// to by a descriptor.
///
/// This returns a hash of the last-modification timestamp and file size, and
/// may also include the inode number, device number, birth timestamp, and
/// other metadata fields that may change when the file is modified or
/// replaced. It may also include a secret value chosen by the
/// implementation and not otherwise exposed.
///
/// Implementations are encouraged to provide the following properties:
///
/// - If the file is not modified or replaced, the computed hash value should
/// usually not change.
/// - If the object is modified or replaced, the computed hash value should
/// usually change.
/// - The inputs to the hash should not be easily computable from the
/// computed hash.
///
/// However, none of these is required.
@since(version = 0.2.0)
metadata-hash: func() -> result<metadata-hash-value, error-code>;
/// Return a hash of the metadata associated with a filesystem object referred
/// to by a directory descriptor and a relative path.
///
/// This performs the same hash computation as `metadata-hash`.
@since(version = 0.2.0)
metadata-hash-at: func(
/// Flags determining the method of how the path is resolved.
path-flags: path-flags,
/// The relative path of the file or directory to inspect.
path: string,
) -> result<metadata-hash-value, error-code>;
}
/// A stream of directory entries.
@since(version = 0.2.0)
resource directory-entry-stream {
/// Read a single directory entry from a `directory-entry-stream`.
@since(version = 0.2.0)
read-directory-entry: func() -> result<option<directory-entry>, error-code>;
}
/// Attempts to extract a filesystem-related `error-code` from the stream
/// `error` provided.
///
/// Stream operations which return `stream-error::last-operation-failed`
/// have a payload with more information about the operation that failed.
/// This payload can be passed through to this function to see if there's
/// filesystem-related information about the error to return.
///
/// Note that this function is fallible because not all stream-related
/// errors are filesystem-related errors.
@since(version = 0.2.0)
filesystem-error-code: func(err: borrow<error>) -> option<error-code>;
}

View File

@@ -0,0 +1,9 @@
package wasi:filesystem@0.2.6;
@since(version = 0.2.0)
world imports {
@since(version = 0.2.0)
import types;
@since(version = 0.2.0)
import preopens;
}

View File

@@ -0,0 +1,49 @@
/// This interface defines a handler of incoming HTTP Requests. It should
/// be exported by components which can respond to HTTP Requests.
@since(version = 0.2.0)
interface incoming-handler {
@since(version = 0.2.0)
use types.{incoming-request, response-outparam};
/// This function is invoked with an incoming HTTP Request, and a resource
/// `response-outparam` which provides the capability to reply with an HTTP
/// Response. The response is sent by calling the `response-outparam.set`
/// method, which allows execution to continue after the response has been
/// sent. This enables both streaming to the response body, and performing other
/// work.
///
/// The implementor of this function must write a response to the
/// `response-outparam` before returning, or else the caller will respond
/// with an error on its behalf.
@since(version = 0.2.0)
handle: func(
request: incoming-request,
response-out: response-outparam
);
}
/// This interface defines a handler of outgoing HTTP Requests. It should be
/// imported by components which wish to make HTTP Requests.
@since(version = 0.2.0)
interface outgoing-handler {
@since(version = 0.2.0)
use types.{
outgoing-request, request-options, future-incoming-response, error-code
};
/// This function is invoked with an outgoing HTTP Request, and it returns
/// a resource `future-incoming-response` which represents an HTTP Response
/// which may arrive in the future.
///
/// The `options` argument accepts optional parameters for the HTTP
/// protocol's transport layer.
///
/// This function may return an error if the `outgoing-request` is invalid
/// or not allowed to be made. Otherwise, protocol errors are reported
/// through the `future-incoming-response`.
@since(version = 0.2.0)
handle: func(
request: outgoing-request,
options: option<request-options>
) -> result<future-incoming-response, error-code>;
}

View File

@@ -0,0 +1,50 @@
package wasi:http@0.2.6;
/// The `wasi:http/imports` world imports all the APIs for HTTP proxies.
/// It is intended to be `include`d in other worlds.
@since(version = 0.2.0)
world imports {
/// HTTP proxies have access to time and randomness.
@since(version = 0.2.0)
import wasi:clocks/monotonic-clock@0.2.6;
@since(version = 0.2.0)
import wasi:clocks/wall-clock@0.2.6;
@since(version = 0.2.0)
import wasi:random/random@0.2.6;
/// Proxies have standard output and error streams which are expected to
/// terminate in a developer-facing console provided by the host.
@since(version = 0.2.0)
import wasi:cli/stdout@0.2.6;
@since(version = 0.2.0)
import wasi:cli/stderr@0.2.6;
/// TODO: this is a temporary workaround until component tooling is able to
/// gracefully handle the absence of stdin. Hosts must return an eof stream
/// for this import, which is what wasi-libc + tooling will do automatically
/// when this import is properly removed.
@since(version = 0.2.0)
import wasi:cli/stdin@0.2.6;
/// This is the default handler to use when user code simply wants to make an
/// HTTP request (e.g., via `fetch()`).
@since(version = 0.2.0)
import outgoing-handler;
}
/// The `wasi:http/proxy` world captures a widely-implementable intersection of
/// hosts that includes HTTP forward and reverse proxies. Components targeting
/// this world may concurrently stream in and out any number of incoming and
/// outgoing HTTP requests.
@since(version = 0.2.0)
world proxy {
@since(version = 0.2.0)
include imports;
/// The host delivers incoming HTTP requests to a component by calling the
/// `handle` function of this exported interface. A host may arbitrarily reuse
/// or not reuse component instance when delivering incoming HTTP requests and
/// thus a component must be able to handle 0..N calls to `handle`.
@since(version = 0.2.0)
export incoming-handler;
}

View File

@@ -0,0 +1,688 @@
/// This interface defines all of the types and methods for implementing
/// HTTP Requests and Responses, both incoming and outgoing, as well as
/// their headers, trailers, and bodies.
@since(version = 0.2.0)
interface types {
@since(version = 0.2.0)
use wasi:clocks/monotonic-clock@0.2.6.{duration};
@since(version = 0.2.0)
use wasi:io/streams@0.2.6.{input-stream, output-stream};
@since(version = 0.2.0)
use wasi:io/error@0.2.6.{error as io-error};
@since(version = 0.2.0)
use wasi:io/poll@0.2.6.{pollable};
/// This type corresponds to HTTP standard Methods.
@since(version = 0.2.0)
variant method {
get,
head,
post,
put,
delete,
connect,
options,
trace,
patch,
other(string)
}
/// This type corresponds to HTTP standard Related Schemes.
@since(version = 0.2.0)
variant scheme {
HTTP,
HTTPS,
other(string)
}
/// These cases are inspired by the IANA HTTP Proxy Error Types:
/// <https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types>
@since(version = 0.2.0)
variant error-code {
DNS-timeout,
DNS-error(DNS-error-payload),
destination-not-found,
destination-unavailable,
destination-IP-prohibited,
destination-IP-unroutable,
connection-refused,
connection-terminated,
connection-timeout,
connection-read-timeout,
connection-write-timeout,
connection-limit-reached,
TLS-protocol-error,
TLS-certificate-error,
TLS-alert-received(TLS-alert-received-payload),
HTTP-request-denied,
HTTP-request-length-required,
HTTP-request-body-size(option<u64>),
HTTP-request-method-invalid,
HTTP-request-URI-invalid,
HTTP-request-URI-too-long,
HTTP-request-header-section-size(option<u32>),
HTTP-request-header-size(option<field-size-payload>),
HTTP-request-trailer-section-size(option<u32>),
HTTP-request-trailer-size(field-size-payload),
HTTP-response-incomplete,
HTTP-response-header-section-size(option<u32>),
HTTP-response-header-size(field-size-payload),
HTTP-response-body-size(option<u64>),
HTTP-response-trailer-section-size(option<u32>),
HTTP-response-trailer-size(field-size-payload),
HTTP-response-transfer-coding(option<string>),
HTTP-response-content-coding(option<string>),
HTTP-response-timeout,
HTTP-upgrade-failed,
HTTP-protocol-error,
loop-detected,
configuration-error,
/// This is a catch-all error for anything that doesn't fit cleanly into a
/// more specific case. It also includes an optional string for an
/// unstructured description of the error. Users should not depend on the
/// string for diagnosing errors, as it's not required to be consistent
/// between implementations.
internal-error(option<string>)
}
/// Defines the case payload type for `DNS-error` above:
@since(version = 0.2.0)
record DNS-error-payload {
rcode: option<string>,
info-code: option<u16>
}
/// Defines the case payload type for `TLS-alert-received` above:
@since(version = 0.2.0)
record TLS-alert-received-payload {
alert-id: option<u8>,
alert-message: option<string>
}
/// Defines the case payload type for `HTTP-response-{header,trailer}-size` above:
@since(version = 0.2.0)
record field-size-payload {
field-name: option<string>,
field-size: option<u32>
}
/// Attempts to extract a http-related `error` from the wasi:io `error`
/// provided.
///
/// Stream operations which return
/// `wasi:io/stream/stream-error::last-operation-failed` have a payload of
/// type `wasi:io/error/error` with more information about the operation
/// that failed. This payload can be passed through to this function to see
/// if there's http-related information about the error to return.
///
/// Note that this function is fallible because not all io-errors are
/// http-related errors.
@since(version = 0.2.0)
http-error-code: func(err: borrow<io-error>) -> option<error-code>;
/// This type enumerates the different kinds of errors that may occur when
/// setting or appending to a `fields` resource.
@since(version = 0.2.0)
variant header-error {
/// This error indicates that a `field-name` or `field-value` was
/// syntactically invalid when used with an operation that sets headers in a
/// `fields`.
invalid-syntax,
/// This error indicates that a forbidden `field-name` was used when trying
/// to set a header in a `fields`.
forbidden,
/// This error indicates that the operation on the `fields` was not
/// permitted because the fields are immutable.
immutable,
}
/// Field names are always strings.
///
/// Field names should always be treated as case insensitive by the `fields`
/// resource for the purposes of equality checking.
@since(version = 0.2.1)
type field-name = field-key;
/// Field keys are always strings.
///
/// Field keys should always be treated as case insensitive by the `fields`
/// resource for the purposes of equality checking.
///
/// # Deprecation
///
/// This type has been deprecated in favor of the `field-name` type.
@since(version = 0.2.0)
@deprecated(version = 0.2.2)
type field-key = string;
/// Field values should always be ASCII strings. However, in
/// reality, HTTP implementations often have to interpret malformed values,
/// so they are provided as a list of bytes.
@since(version = 0.2.0)
type field-value = list<u8>;
/// This following block defines the `fields` resource which corresponds to
/// HTTP standard Fields. Fields are a common representation used for both
/// Headers and Trailers.
///
/// A `fields` may be mutable or immutable. A `fields` created using the
/// constructor, `from-list`, or `clone` will be mutable, but a `fields`
/// resource given by other means (including, but not limited to,
/// `incoming-request.headers`, `outgoing-request.headers`) might be
/// immutable. In an immutable fields, the `set`, `append`, and `delete`
/// operations will fail with `header-error.immutable`.
@since(version = 0.2.0)
resource fields {
/// Construct an empty HTTP Fields.
///
/// The resulting `fields` is mutable.
@since(version = 0.2.0)
constructor();
/// Construct an HTTP Fields.
///
/// The resulting `fields` is mutable.
///
/// The list represents each name-value pair in the Fields. Names
/// which have multiple values are represented by multiple entries in this
/// list with the same name.
///
/// The tuple is a pair of the field name, represented as a string, and
/// Value, represented as a list of bytes.
///
/// An error result will be returned if any `field-name` or `field-value` is
/// syntactically invalid, or if a field is forbidden.
@since(version = 0.2.0)
from-list: static func(
entries: list<tuple<field-name,field-value>>
) -> result<fields, header-error>;
/// Get all of the values corresponding to a name. If the name is not present
/// in this `fields` or is syntactically invalid, an empty list is returned.
/// However, if the name is present but empty, this is represented by a list
/// with one or more empty field-values present.
@since(version = 0.2.0)
get: func(name: field-name) -> list<field-value>;
/// Returns `true` when the name is present in this `fields`. If the name is
/// syntactically invalid, `false` is returned.
@since(version = 0.2.0)
has: func(name: field-name) -> bool;
/// Set all of the values for a name. Clears any existing values for that
/// name, if they have been set.
///
/// Fails with `header-error.immutable` if the `fields` are immutable.
///
/// Fails with `header-error.invalid-syntax` if the `field-name` or any of
/// the `field-value`s are syntactically invalid.
@since(version = 0.2.0)
set: func(name: field-name, value: list<field-value>) -> result<_, header-error>;
/// Delete all values for a name. Does nothing if no values for the name
/// exist.
///
/// Fails with `header-error.immutable` if the `fields` are immutable.
///
/// Fails with `header-error.invalid-syntax` if the `field-name` is
/// syntactically invalid.
@since(version = 0.2.0)
delete: func(name: field-name) -> result<_, header-error>;
/// Append a value for a name. Does not change or delete any existing
/// values for that name.
///
/// Fails with `header-error.immutable` if the `fields` are immutable.
///
/// Fails with `header-error.invalid-syntax` if the `field-name` or
/// `field-value` are syntactically invalid.
@since(version = 0.2.0)
append: func(name: field-name, value: field-value) -> result<_, header-error>;
/// Retrieve the full set of names and values in the Fields. Like the
/// constructor, the list represents each name-value pair.
///
/// The outer list represents each name-value pair in the Fields. Names
/// which have multiple values are represented by multiple entries in this
/// list with the same name.
///
/// The names and values are always returned in the original casing and in
/// the order in which they will be serialized for transport.
@since(version = 0.2.0)
entries: func() -> list<tuple<field-name,field-value>>;
/// Make a deep copy of the Fields. Equivalent in behavior to calling the
/// `fields` constructor on the return value of `entries`. The resulting
/// `fields` is mutable.
@since(version = 0.2.0)
clone: func() -> fields;
}
/// Headers is an alias for Fields.
@since(version = 0.2.0)
type headers = fields;
/// Trailers is an alias for Fields.
@since(version = 0.2.0)
type trailers = fields;
/// Represents an incoming HTTP Request.
@since(version = 0.2.0)
resource incoming-request {
/// Returns the method of the incoming request.
@since(version = 0.2.0)
method: func() -> method;
/// Returns the path with query parameters from the request, as a string.
@since(version = 0.2.0)
path-with-query: func() -> option<string>;
/// Returns the protocol scheme from the request.
@since(version = 0.2.0)
scheme: func() -> option<scheme>;
/// Returns the authority of the Request's target URI, if present.
@since(version = 0.2.0)
authority: func() -> option<string>;
/// Get the `headers` associated with the request.
///
/// The returned `headers` resource is immutable: `set`, `append`, and
/// `delete` operations will fail with `header-error.immutable`.
///
/// The `headers` returned are a child resource: it must be dropped before
/// the parent `incoming-request` is dropped. Dropping this
/// `incoming-request` before all children are dropped will trap.
@since(version = 0.2.0)
headers: func() -> headers;
/// Gives the `incoming-body` associated with this request. Will only
/// return success at most once, and subsequent calls will return error.
@since(version = 0.2.0)
consume: func() -> result<incoming-body>;
}
/// Represents an outgoing HTTP Request.
@since(version = 0.2.0)
resource outgoing-request {
/// Construct a new `outgoing-request` with a default `method` of `GET`, and
/// `none` values for `path-with-query`, `scheme`, and `authority`.
///
/// * `headers` is the HTTP Headers for the Request.
///
/// It is possible to construct, or manipulate with the accessor functions
/// below, an `outgoing-request` with an invalid combination of `scheme`
/// and `authority`, or `headers` which are not permitted to be sent.
/// It is the obligation of the `outgoing-handler.handle` implementation
/// to reject invalid constructions of `outgoing-request`.
@since(version = 0.2.0)
constructor(
headers: headers
);
/// Returns the resource corresponding to the outgoing Body for this
/// Request.
///
/// Returns success on the first call: the `outgoing-body` resource for
/// this `outgoing-request` can be retrieved at most once. Subsequent
/// calls will return error.
@since(version = 0.2.0)
body: func() -> result<outgoing-body>;
/// Get the Method for the Request.
@since(version = 0.2.0)
method: func() -> method;
/// Set the Method for the Request. Fails if the string present in a
/// `method.other` argument is not a syntactically valid method.
@since(version = 0.2.0)
set-method: func(method: method) -> result;
/// Get the combination of the HTTP Path and Query for the Request.
/// When `none`, this represents an empty Path and empty Query.
@since(version = 0.2.0)
path-with-query: func() -> option<string>;
/// Set the combination of the HTTP Path and Query for the Request.
/// When `none`, this represents an empty Path and empty Query. Fails is the
/// string given is not a syntactically valid path and query uri component.
@since(version = 0.2.0)
set-path-with-query: func(path-with-query: option<string>) -> result;
/// Get the HTTP Related Scheme for the Request. When `none`, the
/// implementation may choose an appropriate default scheme.
@since(version = 0.2.0)
scheme: func() -> option<scheme>;
/// Set the HTTP Related Scheme for the Request. When `none`, the
/// implementation may choose an appropriate default scheme. Fails if the
/// string given is not a syntactically valid uri scheme.
@since(version = 0.2.0)
set-scheme: func(scheme: option<scheme>) -> result;
/// Get the authority of the Request's target URI. A value of `none` may be used
/// with Related Schemes which do not require an authority. The HTTP and
/// HTTPS schemes always require an authority.
@since(version = 0.2.0)
authority: func() -> option<string>;
/// Set the authority of the Request's target URI. A value of `none` may be used
/// with Related Schemes which do not require an authority. The HTTP and
/// HTTPS schemes always require an authority. Fails if the string given is
/// not a syntactically valid URI authority.
@since(version = 0.2.0)
set-authority: func(authority: option<string>) -> result;
/// Get the headers associated with the Request.
///
/// The returned `headers` resource is immutable: `set`, `append`, and
/// `delete` operations will fail with `header-error.immutable`.
///
/// This headers resource is a child: it must be dropped before the parent
/// `outgoing-request` is dropped, or its ownership is transferred to
/// another component by e.g. `outgoing-handler.handle`.
@since(version = 0.2.0)
headers: func() -> headers;
}
/// Parameters for making an HTTP Request. Each of these parameters is
/// currently an optional timeout applicable to the transport layer of the
/// HTTP protocol.
///
/// These timeouts are separate from any the user may use to bound a
/// blocking call to `wasi:io/poll.poll`.
@since(version = 0.2.0)
resource request-options {
/// Construct a default `request-options` value.
@since(version = 0.2.0)
constructor();
/// The timeout for the initial connect to the HTTP Server.
@since(version = 0.2.0)
connect-timeout: func() -> option<duration>;
/// Set the timeout for the initial connect to the HTTP Server. An error
/// return value indicates that this timeout is not supported.
@since(version = 0.2.0)
set-connect-timeout: func(duration: option<duration>) -> result;
/// The timeout for receiving the first byte of the Response body.
@since(version = 0.2.0)
first-byte-timeout: func() -> option<duration>;
/// Set the timeout for receiving the first byte of the Response body. An
/// error return value indicates that this timeout is not supported.
@since(version = 0.2.0)
set-first-byte-timeout: func(duration: option<duration>) -> result;
/// The timeout for receiving subsequent chunks of bytes in the Response
/// body stream.
@since(version = 0.2.0)
between-bytes-timeout: func() -> option<duration>;
/// Set the timeout for receiving subsequent chunks of bytes in the Response
/// body stream. An error return value indicates that this timeout is not
/// supported.
@since(version = 0.2.0)
set-between-bytes-timeout: func(duration: option<duration>) -> result;
}
/// Represents the ability to send an HTTP Response.
///
/// This resource is used by the `wasi:http/incoming-handler` interface to
/// allow a Response to be sent corresponding to the Request provided as the
/// other argument to `incoming-handler.handle`.
@since(version = 0.2.0)
resource response-outparam {
/// Send an HTTP 1xx response.
///
/// Unlike `response-outparam.set`, this does not consume the
/// `response-outparam`, allowing the guest to send an arbitrary number of
/// informational responses before sending the final response using
/// `response-outparam.set`.
///
/// This will return an `HTTP-protocol-error` if `status` is not in the
/// range [100-199], or an `internal-error` if the implementation does not
/// support informational responses.
@unstable(feature = informational-outbound-responses)
send-informational: func(
status: u16,
headers: headers
) -> result<_, error-code>;
/// Set the value of the `response-outparam` to either send a response,
/// or indicate an error.
///
/// This method consumes the `response-outparam` to ensure that it is
/// called at most once. If it is never called, the implementation
/// will respond with an error.
///
/// The user may provide an `error` to `response` to allow the
/// implementation determine how to respond with an HTTP error response.
@since(version = 0.2.0)
set: static func(
param: response-outparam,
response: result<outgoing-response, error-code>,
);
}
/// This type corresponds to the HTTP standard Status Code.
@since(version = 0.2.0)
type status-code = u16;
/// Represents an incoming HTTP Response.
@since(version = 0.2.0)
resource incoming-response {
/// Returns the status code from the incoming response.
@since(version = 0.2.0)
status: func() -> status-code;
/// Returns the headers from the incoming response.
///
/// The returned `headers` resource is immutable: `set`, `append`, and
/// `delete` operations will fail with `header-error.immutable`.
///
/// This headers resource is a child: it must be dropped before the parent
/// `incoming-response` is dropped.
@since(version = 0.2.0)
headers: func() -> headers;
/// Returns the incoming body. May be called at most once. Returns error
/// if called additional times.
@since(version = 0.2.0)
consume: func() -> result<incoming-body>;
}
/// Represents an incoming HTTP Request or Response's Body.
///
/// A body has both its contents - a stream of bytes - and a (possibly
/// empty) set of trailers, indicating that the full contents of the
/// body have been received. This resource represents the contents as
/// an `input-stream` and the delivery of trailers as a `future-trailers`,
/// and ensures that the user of this interface may only be consuming either
/// the body contents or waiting on trailers at any given time.
@since(version = 0.2.0)
resource incoming-body {
/// Returns the contents of the body, as a stream of bytes.
///
/// Returns success on first call: the stream representing the contents
/// can be retrieved at most once. Subsequent calls will return error.
///
/// The returned `input-stream` resource is a child: it must be dropped
/// before the parent `incoming-body` is dropped, or consumed by
/// `incoming-body.finish`.
///
/// This invariant ensures that the implementation can determine whether
/// the user is consuming the contents of the body, waiting on the
/// `future-trailers` to be ready, or neither. This allows for network
/// backpressure is to be applied when the user is consuming the body,
/// and for that backpressure to not inhibit delivery of the trailers if
/// the user does not read the entire body.
@since(version = 0.2.0)
%stream: func() -> result<input-stream>;
/// Takes ownership of `incoming-body`, and returns a `future-trailers`.
/// This function will trap if the `input-stream` child is still alive.
@since(version = 0.2.0)
finish: static func(this: incoming-body) -> future-trailers;
}
/// Represents a future which may eventually return trailers, or an error.
///
/// In the case that the incoming HTTP Request or Response did not have any
/// trailers, this future will resolve to the empty set of trailers once the
/// complete Request or Response body has been received.
@since(version = 0.2.0)
resource future-trailers {
/// Returns a pollable which becomes ready when either the trailers have
/// been received, or an error has occurred. When this pollable is ready,
/// the `get` method will return `some`.
@since(version = 0.2.0)
subscribe: func() -> pollable;
/// Returns the contents of the trailers, or an error which occurred,
/// once the future is ready.
///
/// The outer `option` represents future readiness. Users can wait on this
/// `option` to become `some` using the `subscribe` method.
///
/// The outer `result` is used to retrieve the trailers or error at most
/// once. It will be success on the first call in which the outer option
/// is `some`, and error on subsequent calls.
///
/// The inner `result` represents that either the HTTP Request or Response
/// body, as well as any trailers, were received successfully, or that an
/// error occurred receiving them. The optional `trailers` indicates whether
/// or not trailers were present in the body.
///
/// When some `trailers` are returned by this method, the `trailers`
/// resource is immutable, and a child. Use of the `set`, `append`, or
/// `delete` methods will return an error, and the resource must be
/// dropped before the parent `future-trailers` is dropped.
@since(version = 0.2.0)
get: func() -> option<result<result<option<trailers>, error-code>>>;
}
/// Represents an outgoing HTTP Response.
@since(version = 0.2.0)
resource outgoing-response {
/// Construct an `outgoing-response`, with a default `status-code` of `200`.
/// If a different `status-code` is needed, it must be set via the
/// `set-status-code` method.
///
/// * `headers` is the HTTP Headers for the Response.
@since(version = 0.2.0)
constructor(headers: headers);
/// Get the HTTP Status Code for the Response.
@since(version = 0.2.0)
status-code: func() -> status-code;
/// Set the HTTP Status Code for the Response. Fails if the status-code
/// given is not a valid http status code.
@since(version = 0.2.0)
set-status-code: func(status-code: status-code) -> result;
/// Get the headers associated with the Request.
///
/// The returned `headers` resource is immutable: `set`, `append`, and
/// `delete` operations will fail with `header-error.immutable`.
///
/// This headers resource is a child: it must be dropped before the parent
/// `outgoing-request` is dropped, or its ownership is transferred to
/// another component by e.g. `outgoing-handler.handle`.
@since(version = 0.2.0)
headers: func() -> headers;
/// Returns the resource corresponding to the outgoing Body for this Response.
///
/// Returns success on the first call: the `outgoing-body` resource for
/// this `outgoing-response` can be retrieved at most once. Subsequent
/// calls will return error.
@since(version = 0.2.0)
body: func() -> result<outgoing-body>;
}
/// Represents an outgoing HTTP Request or Response's Body.
///
/// A body has both its contents - a stream of bytes - and a (possibly
/// empty) set of trailers, inducating the full contents of the body
/// have been sent. This resource represents the contents as an
/// `output-stream` child resource, and the completion of the body (with
/// optional trailers) with a static function that consumes the
/// `outgoing-body` resource, and ensures that the user of this interface
/// may not write to the body contents after the body has been finished.
///
/// If the user code drops this resource, as opposed to calling the static
/// method `finish`, the implementation should treat the body as incomplete,
/// and that an error has occurred. The implementation should propagate this
/// error to the HTTP protocol by whatever means it has available,
/// including: corrupting the body on the wire, aborting the associated
/// Request, or sending a late status code for the Response.
@since(version = 0.2.0)
resource outgoing-body {
/// Returns a stream for writing the body contents.
///
/// The returned `output-stream` is a child resource: it must be dropped
/// before the parent `outgoing-body` resource is dropped (or finished),
/// otherwise the `outgoing-body` drop or `finish` will trap.
///
/// Returns success on the first call: the `output-stream` resource for
/// this `outgoing-body` may be retrieved at most once. Subsequent calls
/// will return error.
@since(version = 0.2.0)
write: func() -> result<output-stream>;
/// Finalize an outgoing body, optionally providing trailers. This must be
/// called to signal that the response is complete. If the `outgoing-body`
/// is dropped without calling `outgoing-body.finalize`, the implementation
/// should treat the body as corrupted.
///
/// Fails if the body's `outgoing-request` or `outgoing-response` was
/// constructed with a Content-Length header, and the contents written
/// to the body (via `write`) does not match the value given in the
/// Content-Length.
@since(version = 0.2.0)
finish: static func(
this: outgoing-body,
trailers: option<trailers>
) -> result<_, error-code>;
}
/// Represents a future which may eventually return an incoming HTTP
/// Response, or an error.
///
/// This resource is returned by the `wasi:http/outgoing-handler` interface to
/// provide the HTTP Response corresponding to the sent Request.
@since(version = 0.2.0)
resource future-incoming-response {
/// Returns a pollable which becomes ready when either the Response has
/// been received, or an error has occurred. When this pollable is ready,
/// the `get` method will return `some`.
@since(version = 0.2.0)
subscribe: func() -> pollable;
/// Returns the incoming HTTP Response, or an error, once one is ready.
///
/// The outer `option` represents future readiness. Users can wait on this
/// `option` to become `some` using the `subscribe` method.
///
/// The outer `result` is used to retrieve the response or error at most
/// once. It will be success on the first call in which the outer option
/// is `some`, and error on subsequent calls.
///
/// The inner `result` represents that either the incoming HTTP Response
/// status and headers have received successfully, or that an error
/// occurred. Errors may also occur while consuming the response body,
/// but those will be reported by the `incoming-body` and its
/// `output-stream` child.
@since(version = 0.2.0)
get: func() -> option<result<result<incoming-response, error-code>>>;
}
}

View File

@@ -0,0 +1,34 @@
package wasi:io@0.2.6;
@since(version = 0.2.0)
interface error {
/// A resource which represents some error information.
///
/// The only method provided by this resource is `to-debug-string`,
/// which provides some human-readable information about the error.
///
/// In the `wasi:io` package, this resource is returned through the
/// `wasi:io/streams/stream-error` type.
///
/// To provide more specific error information, other interfaces may
/// offer functions to "downcast" this error into more specific types. For example,
/// errors returned from streams derived from filesystem types can be described using
/// the filesystem's own error-code type. This is done using the function
/// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow<error>`
/// parameter and returns an `option<wasi:filesystem/types/error-code>`.
///
/// The set of functions which can "downcast" an `error` into a more
/// concrete type is open.
@since(version = 0.2.0)
resource error {
/// Returns a string that is suitable to assist humans in debugging
/// this error.
///
/// WARNING: The returned string should not be consumed mechanically!
/// It may change across platforms, hosts, or other implementation
/// details. Parsing this string is a major platform-compatibility
/// hazard.
@since(version = 0.2.0)
to-debug-string: func() -> string;
}
}

View File

@@ -0,0 +1,47 @@
package wasi:io@0.2.6;
/// A poll API intended to let users wait for I/O events on multiple handles
/// at once.
@since(version = 0.2.0)
interface poll {
/// `pollable` represents a single I/O event which may be ready, or not.
@since(version = 0.2.0)
resource pollable {
/// Return the readiness of a pollable. This function never blocks.
///
/// Returns `true` when the pollable is ready, and `false` otherwise.
@since(version = 0.2.0)
ready: func() -> bool;
/// `block` returns immediately if the pollable is ready, and otherwise
/// blocks until ready.
///
/// This function is equivalent to calling `poll.poll` on a list
/// containing only this pollable.
@since(version = 0.2.0)
block: func();
}
/// Poll for completion on a set of pollables.
///
/// This function takes a list of pollables, which identify I/O sources of
/// interest, and waits until one or more of the events is ready for I/O.
///
/// The result `list<u32>` contains one or more indices of handles in the
/// argument list that is ready for I/O.
///
/// This function traps if either:
/// - the list is empty, or:
/// - the list contains more elements than can be indexed with a `u32` value.
///
/// A timeout can be implemented by adding a pollable from the
/// wasi-clocks API to the list.
///
/// This function does not return a `result`; polling in itself does not
/// do any I/O so it doesn't fail. If any of the I/O sources identified by
/// the pollables has an error, it is indicated by marking the source as
/// being ready for I/O.
@since(version = 0.2.0)
poll: func(in: list<borrow<pollable>>) -> list<u32>;
}

View File

@@ -0,0 +1,290 @@
package wasi:io@0.2.6;
/// WASI I/O is an I/O abstraction API which is currently focused on providing
/// stream types.
///
/// In the future, the component model is expected to add built-in stream types;
/// when it does, they are expected to subsume this API.
@since(version = 0.2.0)
interface streams {
@since(version = 0.2.0)
use error.{error};
@since(version = 0.2.0)
use poll.{pollable};
/// An error for input-stream and output-stream operations.
@since(version = 0.2.0)
variant stream-error {
/// The last operation (a write or flush) failed before completion.
///
/// More information is available in the `error` payload.
///
/// After this, the stream will be closed. All future operations return
/// `stream-error::closed`.
last-operation-failed(error),
/// The stream is closed: no more input will be accepted by the
/// stream. A closed output-stream will return this error on all
/// future operations.
closed
}
/// An input bytestream.
///
/// `input-stream`s are *non-blocking* to the extent practical on underlying
/// platforms. I/O operations always return promptly; if fewer bytes are
/// promptly available than requested, they return the number of bytes promptly
/// available, which could even be zero. To wait for data to be available,
/// use the `subscribe` function to obtain a `pollable` which can be polled
/// for using `wasi:io/poll`.
@since(version = 0.2.0)
resource input-stream {
/// Perform a non-blocking read from the stream.
///
/// When the source of a `read` is binary data, the bytes from the source
/// are returned verbatim. When the source of a `read` is known to the
/// implementation to be text, bytes containing the UTF-8 encoding of the
/// text are returned.
///
/// This function returns a list of bytes containing the read data,
/// when successful. The returned list will contain up to `len` bytes;
/// it may return fewer than requested, but not more. The list is
/// empty when no bytes are available for reading at this time. The
/// pollable given by `subscribe` will be ready when more bytes are
/// available.
///
/// This function fails with a `stream-error` when the operation
/// encounters an error, giving `last-operation-failed`, or when the
/// stream is closed, giving `closed`.
///
/// When the caller gives a `len` of 0, it represents a request to
/// read 0 bytes. If the stream is still open, this call should
/// succeed and return an empty list, or otherwise fail with `closed`.
///
/// The `len` parameter is a `u64`, which could represent a list of u8 which
/// is not possible to allocate in wasm32, or not desirable to allocate as
/// as a return value by the callee. The callee may return a list of bytes
/// less than `len` in size while more bytes are available for reading.
@since(version = 0.2.0)
read: func(
/// The maximum number of bytes to read
len: u64
) -> result<list<u8>, stream-error>;
/// Read bytes from a stream, after blocking until at least one byte can
/// be read. Except for blocking, behavior is identical to `read`.
@since(version = 0.2.0)
blocking-read: func(
/// The maximum number of bytes to read
len: u64
) -> result<list<u8>, stream-error>;
/// Skip bytes from a stream. Returns number of bytes skipped.
///
/// Behaves identical to `read`, except instead of returning a list
/// of bytes, returns the number of bytes consumed from the stream.
@since(version = 0.2.0)
skip: func(
/// The maximum number of bytes to skip.
len: u64,
) -> result<u64, stream-error>;
/// Skip bytes from a stream, after blocking until at least one byte
/// can be skipped. Except for blocking behavior, identical to `skip`.
@since(version = 0.2.0)
blocking-skip: func(
/// The maximum number of bytes to skip.
len: u64,
) -> result<u64, stream-error>;
/// Create a `pollable` which will resolve once either the specified stream
/// has bytes available to read or the other end of the stream has been
/// closed.
/// The created `pollable` is a child resource of the `input-stream`.
/// Implementations may trap if the `input-stream` is dropped before
/// all derived `pollable`s created with this function are dropped.
@since(version = 0.2.0)
subscribe: func() -> pollable;
}
/// An output bytestream.
///
/// `output-stream`s are *non-blocking* to the extent practical on
/// underlying platforms. Except where specified otherwise, I/O operations also
/// always return promptly, after the number of bytes that can be written
/// promptly, which could even be zero. To wait for the stream to be ready to
/// accept data, the `subscribe` function to obtain a `pollable` which can be
/// polled for using `wasi:io/poll`.
///
/// Dropping an `output-stream` while there's still an active write in
/// progress may result in the data being lost. Before dropping the stream,
/// be sure to fully flush your writes.
@since(version = 0.2.0)
resource output-stream {
/// Check readiness for writing. This function never blocks.
///
/// Returns the number of bytes permitted for the next call to `write`,
/// or an error. Calling `write` with more bytes than this function has
/// permitted will trap.
///
/// When this function returns 0 bytes, the `subscribe` pollable will
/// become ready when this function will report at least 1 byte, or an
/// error.
@since(version = 0.2.0)
check-write: func() -> result<u64, stream-error>;
/// Perform a write. This function never blocks.
///
/// When the destination of a `write` is binary data, the bytes from
/// `contents` are written verbatim. When the destination of a `write` is
/// known to the implementation to be text, the bytes of `contents` are
/// transcoded from UTF-8 into the encoding of the destination and then
/// written.
///
/// Precondition: check-write gave permit of Ok(n) and contents has a
/// length of less than or equal to n. Otherwise, this function will trap.
///
/// returns Err(closed) without writing if the stream has closed since
/// the last call to check-write provided a permit.
@since(version = 0.2.0)
write: func(
contents: list<u8>
) -> result<_, stream-error>;
/// Perform a write of up to 4096 bytes, and then flush the stream. Block
/// until all of these operations are complete, or an error occurs.
///
/// This is a convenience wrapper around the use of `check-write`,
/// `subscribe`, `write`, and `flush`, and is implemented with the
/// following pseudo-code:
///
/// ```text
/// let pollable = this.subscribe();
/// while !contents.is_empty() {
/// // Wait for the stream to become writable
/// pollable.block();
/// let Ok(n) = this.check-write(); // eliding error handling
/// let len = min(n, contents.len());
/// let (chunk, rest) = contents.split_at(len);
/// this.write(chunk ); // eliding error handling
/// contents = rest;
/// }
/// this.flush();
/// // Wait for completion of `flush`
/// pollable.block();
/// // Check for any errors that arose during `flush`
/// let _ = this.check-write(); // eliding error handling
/// ```
@since(version = 0.2.0)
blocking-write-and-flush: func(
contents: list<u8>
) -> result<_, stream-error>;
/// Request to flush buffered output. This function never blocks.
///
/// This tells the output-stream that the caller intends any buffered
/// output to be flushed. the output which is expected to be flushed
/// is all that has been passed to `write` prior to this call.
///
/// Upon calling this function, the `output-stream` will not accept any
/// writes (`check-write` will return `ok(0)`) until the flush has
/// completed. The `subscribe` pollable will become ready when the
/// flush has completed and the stream can accept more writes.
@since(version = 0.2.0)
flush: func() -> result<_, stream-error>;
/// Request to flush buffered output, and block until flush completes
/// and stream is ready for writing again.
@since(version = 0.2.0)
blocking-flush: func() -> result<_, stream-error>;
/// Create a `pollable` which will resolve once the output-stream
/// is ready for more writing, or an error has occurred. When this
/// pollable is ready, `check-write` will return `ok(n)` with n>0, or an
/// error.
///
/// If the stream is closed, this pollable is always ready immediately.
///
/// The created `pollable` is a child resource of the `output-stream`.
/// Implementations may trap if the `output-stream` is dropped before
/// all derived `pollable`s created with this function are dropped.
@since(version = 0.2.0)
subscribe: func() -> pollable;
/// Write zeroes to a stream.
///
/// This should be used precisely like `write` with the exact same
/// preconditions (must use check-write first), but instead of
/// passing a list of bytes, you simply pass the number of zero-bytes
/// that should be written.
@since(version = 0.2.0)
write-zeroes: func(
/// The number of zero-bytes to write
len: u64
) -> result<_, stream-error>;
/// Perform a write of up to 4096 zeroes, and then flush the stream.
/// Block until all of these operations are complete, or an error
/// occurs.
///
/// This is a convenience wrapper around the use of `check-write`,
/// `subscribe`, `write-zeroes`, and `flush`, and is implemented with
/// the following pseudo-code:
///
/// ```text
/// let pollable = this.subscribe();
/// while num_zeroes != 0 {
/// // Wait for the stream to become writable
/// pollable.block();
/// let Ok(n) = this.check-write(); // eliding error handling
/// let len = min(n, num_zeroes);
/// this.write-zeroes(len); // eliding error handling
/// num_zeroes -= len;
/// }
/// this.flush();
/// // Wait for completion of `flush`
/// pollable.block();
/// // Check for any errors that arose during `flush`
/// let _ = this.check-write(); // eliding error handling
/// ```
@since(version = 0.2.0)
blocking-write-zeroes-and-flush: func(
/// The number of zero-bytes to write
len: u64
) -> result<_, stream-error>;
/// Read from one stream and write to another.
///
/// The behavior of splice is equivalent to:
/// 1. calling `check-write` on the `output-stream`
/// 2. calling `read` on the `input-stream` with the smaller of the
/// `check-write` permitted length and the `len` provided to `splice`
/// 3. calling `write` on the `output-stream` with that read data.
///
/// Any error reported by the call to `check-write`, `read`, or
/// `write` ends the splice and reports that error.
///
/// This function returns the number of bytes transferred; it may be less
/// than `len`.
@since(version = 0.2.0)
splice: func(
/// The stream to read from
src: borrow<input-stream>,
/// The number of bytes to splice
len: u64,
) -> result<u64, stream-error>;
/// Read from one stream and write to another, with blocking.
///
/// This is similar to `splice`, except that it blocks until the
/// `output-stream` is ready for writing, and the `input-stream`
/// is ready for reading, before performing the `splice`.
@since(version = 0.2.0)
blocking-splice: func(
/// The stream to read from
src: borrow<input-stream>,
/// The number of bytes to splice
len: u64,
) -> result<u64, stream-error>;
}
}

View File

@@ -0,0 +1,10 @@
package wasi:io@0.2.6;
@since(version = 0.2.0)
world imports {
@since(version = 0.2.0)
import streams;
@since(version = 0.2.0)
import poll;
}

View File

@@ -0,0 +1,27 @@
package wasi:random@0.2.6;
/// The insecure-seed interface for seeding hash-map DoS resistance.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
@since(version = 0.2.0)
interface insecure-seed {
/// Return a 128-bit value that may contain a pseudo-random value.
///
/// The returned value is not required to be computed from a CSPRNG, and may
/// even be entirely deterministic. Host implementations are encouraged to
/// provide pseudo-random values to any program exposed to
/// attacker-controlled content, to enable DoS protection built into many
/// languages' hash-map implementations.
///
/// This function is intended to only be called once, by a source language
/// to initialize Denial Of Service (DoS) protection in its hash-map
/// implementation.
///
/// # Expected future evolution
///
/// This will likely be changed to a value import, to prevent it from being
/// called multiple times and potentially used for purposes other than DoS
/// protection.
@since(version = 0.2.0)
insecure-seed: func() -> tuple<u64, u64>;
}

View File

@@ -0,0 +1,25 @@
package wasi:random@0.2.6;
/// The insecure interface for insecure pseudo-random numbers.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
@since(version = 0.2.0)
interface insecure {
/// Return `len` insecure pseudo-random bytes.
///
/// This function is not cryptographically secure. Do not use it for
/// anything related to security.
///
/// There are no requirements on the values of the returned bytes, however
/// implementations are encouraged to return evenly distributed values with
/// a long period.
@since(version = 0.2.0)
get-insecure-random-bytes: func(len: u64) -> list<u8>;
/// Return an insecure pseudo-random `u64` value.
///
/// This function returns the same type of pseudo-random data as
/// `get-insecure-random-bytes`, represented as a `u64`.
@since(version = 0.2.0)
get-insecure-random-u64: func() -> u64;
}

View File

@@ -0,0 +1,29 @@
package wasi:random@0.2.6;
/// WASI Random is a random data API.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
@since(version = 0.2.0)
interface random {
/// Return `len` cryptographically-secure random or pseudo-random bytes.
///
/// This function must produce data at least as cryptographically secure and
/// fast as an adequately seeded cryptographically-secure pseudo-random
/// number generator (CSPRNG). It must not block, from the perspective of
/// the calling program, under any circumstances, including on the first
/// request and on requests for numbers of bytes. The returned data must
/// always be unpredictable.
///
/// This function must always return fresh data. Deterministic environments
/// must omit this function, rather than implementing it with deterministic
/// data.
@since(version = 0.2.0)
get-random-bytes: func(len: u64) -> list<u8>;
/// Return a cryptographically-secure random or pseudo-random `u64` value.
///
/// This function returns the same type of data as `get-random-bytes`,
/// represented as a `u64`.
@since(version = 0.2.0)
get-random-u64: func() -> u64;
}

View File

@@ -0,0 +1,13 @@
package wasi:random@0.2.6;
@since(version = 0.2.0)
world imports {
@since(version = 0.2.0)
import random;
@since(version = 0.2.0)
import insecure;
@since(version = 0.2.0)
import insecure-seed;
}

View File

@@ -0,0 +1,11 @@
/// This interface provides a value-export of the default network handle..
@since(version = 0.2.0)
interface instance-network {
@since(version = 0.2.0)
use network.{network};
/// Get a handle to the default network.
@since(version = 0.2.0)
instance-network: func() -> network;
}

View File

@@ -0,0 +1,56 @@
@since(version = 0.2.0)
interface ip-name-lookup {
@since(version = 0.2.0)
use wasi:io/poll@0.2.6.{pollable};
@since(version = 0.2.0)
use network.{network, error-code, ip-address};
/// Resolve an internet host name to a list of IP addresses.
///
/// Unicode domain names are automatically converted to ASCII using IDNA encoding.
/// If the input is an IP address string, the address is parsed and returned
/// as-is without making any external requests.
///
/// See the wasi-socket proposal README.md for a comparison with getaddrinfo.
///
/// This function never blocks. It either immediately fails or immediately
/// returns successfully with a `resolve-address-stream` that can be used
/// to (asynchronously) fetch the results.
///
/// # Typical errors
/// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address.
///
/// # References:
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html>
/// - <https://man7.org/linux/man-pages/man3/getaddrinfo.3.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo>
/// - <https://man.freebsd.org/cgi/man.cgi?query=getaddrinfo&sektion=3>
@since(version = 0.2.0)
resolve-addresses: func(network: borrow<network>, name: string) -> result<resolve-address-stream, error-code>;
@since(version = 0.2.0)
resource resolve-address-stream {
/// Returns the next address from the resolver.
///
/// This function should be called multiple times. On each call, it will
/// return the next address in connection order preference. If all
/// addresses have been exhausted, this function returns `none`.
///
/// This function never returns IPv4-mapped IPv6 addresses.
///
/// # Typical errors
/// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY)
/// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN)
/// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL)
/// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN)
@since(version = 0.2.0)
resolve-next-address: func() -> result<option<ip-address>, error-code>;
/// Create a `pollable` which will resolve once the stream is ready for I/O.
///
/// Note: this function is here for WASI 0.2 only.
/// It's planned to be removed when `future` is natively supported in Preview3.
@since(version = 0.2.0)
subscribe: func() -> pollable;
}
}

View File

@@ -0,0 +1,169 @@
@since(version = 0.2.0)
interface network {
@unstable(feature = network-error-code)
use wasi:io/error@0.2.6.{error};
/// An opaque resource that represents access to (a subset of) the network.
/// This enables context-based security for networking.
/// There is no need for this to map 1:1 to a physical network interface.
@since(version = 0.2.0)
resource network;
/// Error codes.
///
/// In theory, every API can return any error code.
/// In practice, API's typically only return the errors documented per API
/// combined with a couple of errors that are always possible:
/// - `unknown`
/// - `access-denied`
/// - `not-supported`
/// - `out-of-memory`
/// - `concurrency-conflict`
///
/// See each individual API for what the POSIX equivalents are. They sometimes differ per API.
@since(version = 0.2.0)
enum error-code {
/// Unknown error
unknown,
/// Access denied.
///
/// POSIX equivalent: EACCES, EPERM
access-denied,
/// The operation is not supported.
///
/// POSIX equivalent: EOPNOTSUPP
not-supported,
/// One of the arguments is invalid.
///
/// POSIX equivalent: EINVAL
invalid-argument,
/// Not enough memory to complete the operation.
///
/// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY
out-of-memory,
/// The operation timed out before it could finish completely.
timeout,
/// This operation is incompatible with another asynchronous operation that is already in progress.
///
/// POSIX equivalent: EALREADY
concurrency-conflict,
/// Trying to finish an asynchronous operation that:
/// - has not been started yet, or:
/// - was already finished by a previous `finish-*` call.
///
/// Note: this is scheduled to be removed when `future`s are natively supported.
not-in-progress,
/// The operation has been aborted because it could not be completed immediately.
///
/// Note: this is scheduled to be removed when `future`s are natively supported.
would-block,
/// The operation is not valid in the socket's current state.
invalid-state,
/// A new socket resource could not be created because of a system limit.
new-socket-limit,
/// A bind operation failed because the provided address is not an address that the `network` can bind to.
address-not-bindable,
/// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available.
address-in-use,
/// The remote address is not reachable
remote-unreachable,
/// The TCP connection was forcefully rejected
connection-refused,
/// The TCP connection was reset.
connection-reset,
/// A TCP connection was aborted.
connection-aborted,
/// The size of a datagram sent to a UDP socket exceeded the maximum
/// supported size.
datagram-too-large,
/// Name does not exist or has no suitable associated IP addresses.
name-unresolvable,
/// A temporary failure in name resolution occurred.
temporary-resolver-failure,
/// A permanent failure in name resolution occurred.
permanent-resolver-failure,
}
/// Attempts to extract a network-related `error-code` from the stream
/// `error` provided.
///
/// Stream operations which return `stream-error::last-operation-failed`
/// have a payload with more information about the operation that failed.
/// This payload can be passed through to this function to see if there's
/// network-related information about the error to return.
///
/// Note that this function is fallible because not all stream-related
/// errors are network-related errors.
@unstable(feature = network-error-code)
network-error-code: func(err: borrow<error>) -> option<error-code>;
@since(version = 0.2.0)
enum ip-address-family {
/// Similar to `AF_INET` in POSIX.
ipv4,
/// Similar to `AF_INET6` in POSIX.
ipv6,
}
@since(version = 0.2.0)
type ipv4-address = tuple<u8, u8, u8, u8>;
@since(version = 0.2.0)
type ipv6-address = tuple<u16, u16, u16, u16, u16, u16, u16, u16>;
@since(version = 0.2.0)
variant ip-address {
ipv4(ipv4-address),
ipv6(ipv6-address),
}
@since(version = 0.2.0)
record ipv4-socket-address {
/// sin_port
port: u16,
/// sin_addr
address: ipv4-address,
}
@since(version = 0.2.0)
record ipv6-socket-address {
/// sin6_port
port: u16,
/// sin6_flowinfo
flow-info: u32,
/// sin6_addr
address: ipv6-address,
/// sin6_scope_id
scope-id: u32,
}
@since(version = 0.2.0)
variant ip-socket-address {
ipv4(ipv4-socket-address),
ipv6(ipv6-socket-address),
}
}

View File

@@ -0,0 +1,30 @@
@since(version = 0.2.0)
interface tcp-create-socket {
@since(version = 0.2.0)
use network.{network, error-code, ip-address-family};
@since(version = 0.2.0)
use tcp.{tcp-socket};
/// Create a new TCP socket.
///
/// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX.
/// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise.
///
/// This function does not require a network capability handle. This is considered to be safe because
/// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect`
/// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world.
///
/// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations.
///
/// # Typical errors
/// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT)
/// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html>
/// - <https://man7.org/linux/man-pages/man2/socket.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw>
/// - <https://man.freebsd.org/cgi/man.cgi?query=socket&sektion=2>
@since(version = 0.2.0)
create-tcp-socket: func(address-family: ip-address-family) -> result<tcp-socket, error-code>;
}

View File

@@ -0,0 +1,387 @@
@since(version = 0.2.0)
interface tcp {
@since(version = 0.2.0)
use wasi:io/streams@0.2.6.{input-stream, output-stream};
@since(version = 0.2.0)
use wasi:io/poll@0.2.6.{pollable};
@since(version = 0.2.0)
use wasi:clocks/monotonic-clock@0.2.6.{duration};
@since(version = 0.2.0)
use network.{network, error-code, ip-socket-address, ip-address-family};
@since(version = 0.2.0)
enum shutdown-type {
/// Similar to `SHUT_RD` in POSIX.
receive,
/// Similar to `SHUT_WR` in POSIX.
send,
/// Similar to `SHUT_RDWR` in POSIX.
both,
}
/// A TCP socket resource.
///
/// The socket can be in one of the following states:
/// - `unbound`
/// - `bind-in-progress`
/// - `bound` (See note below)
/// - `listen-in-progress`
/// - `listening`
/// - `connect-in-progress`
/// - `connected`
/// - `closed`
/// See <https://github.com/WebAssembly/wasi-sockets/blob/main/TcpSocketOperationalSemantics.md>
/// for more information.
///
/// Note: Except where explicitly mentioned, whenever this documentation uses
/// the term "bound" without backticks it actually means: in the `bound` state *or higher*.
/// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`)
///
/// In addition to the general error codes documented on the
/// `network::error-code` type, TCP socket methods may always return
/// `error(invalid-state)` when in the `closed` state.
@since(version = 0.2.0)
resource tcp-socket {
/// Bind the socket to a specific network on the provided IP address and port.
///
/// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which
/// network interface(s) to bind to.
/// If the TCP/UDP port is zero, the socket will be bound to a random free port.
///
/// Bind can be attempted multiple times on the same socket, even with
/// different arguments on each iteration. But never concurrently and
/// only as long as the previous bind failed. Once a bind succeeds, the
/// binding can't be changed anymore.
///
/// # Typical errors
/// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows)
/// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL)
/// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL)
/// - `invalid-state`: The socket is already bound. (EINVAL)
/// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows)
/// - `address-in-use`: Address is already in use. (EADDRINUSE)
/// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL)
/// - `not-in-progress`: A `bind` operation is not in progress.
/// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
///
/// # Implementors note
/// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT
/// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR
/// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior
/// and SO_REUSEADDR performs something different entirely.
///
/// Unlike in POSIX, in WASI the bind operation is async. This enables
/// interactive WASI hosts to inject permission prompts. Runtimes that
/// don't want to make use of this ability can simply call the native
/// `bind` as part of either `start-bind` or `finish-bind`.
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html>
/// - <https://man7.org/linux/man-pages/man2/bind.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-bind>
/// - <https://man.freebsd.org/cgi/man.cgi?query=bind&sektion=2&format=html>
@since(version = 0.2.0)
start-bind: func(network: borrow<network>, local-address: ip-socket-address) -> result<_, error-code>;
@since(version = 0.2.0)
finish-bind: func() -> result<_, error-code>;
/// Connect to a remote endpoint.
///
/// On success:
/// - the socket is transitioned into the `connected` state.
/// - a pair of streams is returned that can be used to read & write to the connection
///
/// After a failed connection attempt, the socket will be in the `closed`
/// state and the only valid action left is to `drop` the socket. A single
/// socket can not be used to connect more than once.
///
/// # Typical errors
/// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT)
/// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS)
/// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos)
/// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows)
/// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows)
/// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`.
/// - `invalid-state`: The socket is already in the `connected` state. (EISCONN)
/// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows)
/// - `timeout`: Connection timed out. (ETIMEDOUT)
/// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED)
/// - `connection-reset`: The connection was reset. (ECONNRESET)
/// - `connection-aborted`: The connection was aborted. (ECONNABORTED)
/// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
/// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD)
/// - `not-in-progress`: A connect operation is not in progress.
/// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
///
/// # Implementors note
/// The POSIX equivalent of `start-connect` is the regular `connect` syscall.
/// Because all WASI sockets are non-blocking this is expected to return
/// EINPROGRESS, which should be translated to `ok()` in WASI.
///
/// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT`
/// with a timeout of 0 on the socket descriptor. Followed by a check for
/// the `SO_ERROR` socket option, in case the poll signaled readiness.
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html>
/// - <https://man7.org/linux/man-pages/man2/connect.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect>
/// - <https://man.freebsd.org/cgi/man.cgi?connect>
@since(version = 0.2.0)
start-connect: func(network: borrow<network>, remote-address: ip-socket-address) -> result<_, error-code>;
@since(version = 0.2.0)
finish-connect: func() -> result<tuple<input-stream, output-stream>, error-code>;
/// Start listening for new connections.
///
/// Transitions the socket into the `listening` state.
///
/// Unlike POSIX, the socket must already be explicitly bound.
///
/// # Typical errors
/// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ)
/// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD)
/// - `invalid-state`: The socket is already in the `listening` state.
/// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE)
/// - `not-in-progress`: A listen operation is not in progress.
/// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
///
/// # Implementors note
/// Unlike in POSIX, in WASI the listen operation is async. This enables
/// interactive WASI hosts to inject permission prompts. Runtimes that
/// don't want to make use of this ability can simply call the native
/// `listen` as part of either `start-listen` or `finish-listen`.
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/listen.html>
/// - <https://man7.org/linux/man-pages/man2/listen.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-listen>
/// - <https://man.freebsd.org/cgi/man.cgi?query=listen&sektion=2>
@since(version = 0.2.0)
start-listen: func() -> result<_, error-code>;
@since(version = 0.2.0)
finish-listen: func() -> result<_, error-code>;
/// Accept a new client socket.
///
/// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket:
/// - `address-family`
/// - `keep-alive-enabled`
/// - `keep-alive-idle-time`
/// - `keep-alive-interval`
/// - `keep-alive-count`
/// - `hop-limit`
/// - `receive-buffer-size`
/// - `send-buffer-size`
///
/// On success, this function returns the newly accepted client socket along with
/// a pair of streams that can be used to read & write to the connection.
///
/// # Typical errors
/// - `invalid-state`: Socket is not in the `listening` state. (EINVAL)
/// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN)
/// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED)
/// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/accept.html>
/// - <https://man7.org/linux/man-pages/man2/accept.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-accept>
/// - <https://man.freebsd.org/cgi/man.cgi?query=accept&sektion=2>
@since(version = 0.2.0)
accept: func() -> result<tuple<tcp-socket, input-stream, output-stream>, error-code>;
/// Get the bound local address.
///
/// POSIX mentions:
/// > If the socket has not been bound to a local name, the value
/// > stored in the object pointed to by `address` is unspecified.
///
/// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet.
///
/// # Typical errors
/// - `invalid-state`: The socket is not bound to any local address.
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html>
/// - <https://man7.org/linux/man-pages/man2/getsockname.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getsockname>
/// - <https://man.freebsd.org/cgi/man.cgi?getsockname>
@since(version = 0.2.0)
local-address: func() -> result<ip-socket-address, error-code>;
/// Get the remote address.
///
/// # Typical errors
/// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html>
/// - <https://man7.org/linux/man-pages/man2/getpeername.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getpeername>
/// - <https://man.freebsd.org/cgi/man.cgi?query=getpeername&sektion=2&n=1>
@since(version = 0.2.0)
remote-address: func() -> result<ip-socket-address, error-code>;
/// Whether the socket is in the `listening` state.
///
/// Equivalent to the SO_ACCEPTCONN socket option.
@since(version = 0.2.0)
is-listening: func() -> bool;
/// Whether this is a IPv4 or IPv6 socket.
///
/// Equivalent to the SO_DOMAIN socket option.
@since(version = 0.2.0)
address-family: func() -> ip-address-family;
/// Hints the desired listen queue size. Implementations are free to ignore this.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
/// Any other value will never cause an error, but it might be silently clamped and/or rounded.
///
/// # Typical errors
/// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen.
/// - `invalid-argument`: (set) The provided value was 0.
/// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state.
@since(version = 0.2.0)
set-listen-backlog-size: func(value: u64) -> result<_, error-code>;
/// Enables or disables keepalive.
///
/// The keepalive behavior can be adjusted using:
/// - `keep-alive-idle-time`
/// - `keep-alive-interval`
/// - `keep-alive-count`
/// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true.
///
/// Equivalent to the SO_KEEPALIVE socket option.
@since(version = 0.2.0)
keep-alive-enabled: func() -> result<bool, error-code>;
@since(version = 0.2.0)
set-keep-alive-enabled: func(value: bool) -> result<_, error-code>;
/// Amount of time the connection has to be idle before TCP starts sending keepalive packets.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
/// Any other value will never cause an error, but it might be silently clamped and/or rounded.
/// I.e. after setting a value, reading the same setting back may return a different value.
///
/// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS)
///
/// # Typical errors
/// - `invalid-argument`: (set) The provided value was 0.
@since(version = 0.2.0)
keep-alive-idle-time: func() -> result<duration, error-code>;
@since(version = 0.2.0)
set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>;
/// The time between keepalive packets.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
/// Any other value will never cause an error, but it might be silently clamped and/or rounded.
/// I.e. after setting a value, reading the same setting back may return a different value.
///
/// Equivalent to the TCP_KEEPINTVL socket option.
///
/// # Typical errors
/// - `invalid-argument`: (set) The provided value was 0.
@since(version = 0.2.0)
keep-alive-interval: func() -> result<duration, error-code>;
@since(version = 0.2.0)
set-keep-alive-interval: func(value: duration) -> result<_, error-code>;
/// The maximum amount of keepalive packets TCP should send before aborting the connection.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
/// Any other value will never cause an error, but it might be silently clamped and/or rounded.
/// I.e. after setting a value, reading the same setting back may return a different value.
///
/// Equivalent to the TCP_KEEPCNT socket option.
///
/// # Typical errors
/// - `invalid-argument`: (set) The provided value was 0.
@since(version = 0.2.0)
keep-alive-count: func() -> result<u32, error-code>;
@since(version = 0.2.0)
set-keep-alive-count: func(value: u32) -> result<_, error-code>;
/// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
///
/// # Typical errors
/// - `invalid-argument`: (set) The TTL value must be 1 or higher.
@since(version = 0.2.0)
hop-limit: func() -> result<u8, error-code>;
@since(version = 0.2.0)
set-hop-limit: func(value: u8) -> result<_, error-code>;
/// The kernel buffer space reserved for sends/receives on this socket.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
/// Any other value will never cause an error, but it might be silently clamped and/or rounded.
/// I.e. after setting a value, reading the same setting back may return a different value.
///
/// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options.
///
/// # Typical errors
/// - `invalid-argument`: (set) The provided value was 0.
@since(version = 0.2.0)
receive-buffer-size: func() -> result<u64, error-code>;
@since(version = 0.2.0)
set-receive-buffer-size: func(value: u64) -> result<_, error-code>;
@since(version = 0.2.0)
send-buffer-size: func() -> result<u64, error-code>;
@since(version = 0.2.0)
set-send-buffer-size: func(value: u64) -> result<_, error-code>;
/// Create a `pollable` which can be used to poll for, or block on,
/// completion of any of the asynchronous operations of this socket.
///
/// When `finish-bind`, `finish-listen`, `finish-connect` or `accept`
/// return `error(would-block)`, this pollable can be used to wait for
/// their success or failure, after which the method can be retried.
///
/// The pollable is not limited to the async operation that happens to be
/// in progress at the time of calling `subscribe` (if any). Theoretically,
/// `subscribe` only has to be called once per socket and can then be
/// (re)used for the remainder of the socket's lifetime.
///
/// See <https://github.com/WebAssembly/wasi-sockets/blob/main/TcpSocketOperationalSemantics.md#pollable-readiness>
/// for more information.
///
/// Note: this function is here for WASI 0.2 only.
/// It's planned to be removed when `future` is natively supported in Preview3.
@since(version = 0.2.0)
subscribe: func() -> pollable;
/// Initiate a graceful shutdown.
///
/// - `receive`: The socket is not expecting to receive any data from
/// the peer. The `input-stream` associated with this socket will be
/// closed. Any data still in the receive queue at time of calling
/// this method will be discarded.
/// - `send`: The socket has no more data to send to the peer. The `output-stream`
/// associated with this socket will be closed and a FIN packet will be sent.
/// - `both`: Same effect as `receive` & `send` combined.
///
/// This function is idempotent; shutting down a direction more than once
/// has no effect and returns `ok`.
///
/// The shutdown function does not close (drop) the socket.
///
/// # Typical errors
/// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/shutdown.html>
/// - <https://man7.org/linux/man-pages/man2/shutdown.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-shutdown>
/// - <https://man.freebsd.org/cgi/man.cgi?query=shutdown&sektion=2>
@since(version = 0.2.0)
shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>;
}
}

View File

@@ -0,0 +1,30 @@
@since(version = 0.2.0)
interface udp-create-socket {
@since(version = 0.2.0)
use network.{network, error-code, ip-address-family};
@since(version = 0.2.0)
use udp.{udp-socket};
/// Create a new UDP socket.
///
/// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX.
/// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise.
///
/// This function does not require a network capability handle. This is considered to be safe because
/// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called,
/// the socket is effectively an in-memory configuration object, unable to communicate with the outside world.
///
/// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations.
///
/// # Typical errors
/// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT)
/// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE)
///
/// # References:
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html>
/// - <https://man7.org/linux/man-pages/man2/socket.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw>
/// - <https://man.freebsd.org/cgi/man.cgi?query=socket&sektion=2>
@since(version = 0.2.0)
create-udp-socket: func(address-family: ip-address-family) -> result<udp-socket, error-code>;
}

View File

@@ -0,0 +1,288 @@
@since(version = 0.2.0)
interface udp {
@since(version = 0.2.0)
use wasi:io/poll@0.2.6.{pollable};
@since(version = 0.2.0)
use network.{network, error-code, ip-socket-address, ip-address-family};
/// A received datagram.
@since(version = 0.2.0)
record incoming-datagram {
/// The payload.
///
/// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes.
data: list<u8>,
/// The source address.
///
/// This field is guaranteed to match the remote address the stream was initialized with, if any.
///
/// Equivalent to the `src_addr` out parameter of `recvfrom`.
remote-address: ip-socket-address,
}
/// A datagram to be sent out.
@since(version = 0.2.0)
record outgoing-datagram {
/// The payload.
data: list<u8>,
/// The destination address.
///
/// The requirements on this field depend on how the stream was initialized:
/// - with a remote address: this field must be None or match the stream's remote address exactly.
/// - without a remote address: this field is required.
///
/// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`.
remote-address: option<ip-socket-address>,
}
/// A UDP socket handle.
@since(version = 0.2.0)
resource udp-socket {
/// Bind the socket to a specific network on the provided IP address and port.
///
/// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which
/// network interface(s) to bind to.
/// If the port is zero, the socket will be bound to a random free port.
///
/// # Typical errors
/// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows)
/// - `invalid-state`: The socket is already bound. (EINVAL)
/// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows)
/// - `address-in-use`: Address is already in use. (EADDRINUSE)
/// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL)
/// - `not-in-progress`: A `bind` operation is not in progress.
/// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
///
/// # Implementors note
/// Unlike in POSIX, in WASI the bind operation is async. This enables
/// interactive WASI hosts to inject permission prompts. Runtimes that
/// don't want to make use of this ability can simply call the native
/// `bind` as part of either `start-bind` or `finish-bind`.
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html>
/// - <https://man7.org/linux/man-pages/man2/bind.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-bind>
/// - <https://man.freebsd.org/cgi/man.cgi?query=bind&sektion=2&format=html>
@since(version = 0.2.0)
start-bind: func(network: borrow<network>, local-address: ip-socket-address) -> result<_, error-code>;
@since(version = 0.2.0)
finish-bind: func() -> result<_, error-code>;
/// Set up inbound & outbound communication channels, optionally to a specific peer.
///
/// This function only changes the local socket configuration and does not generate any network traffic.
/// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well,
/// based on the best network path to `remote-address`.
///
/// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer:
/// - `send` can only be used to send to this destination.
/// - `receive` will only return datagrams sent from the provided `remote-address`.
///
/// This method may be called multiple times on the same socket to change its association, but
/// only the most recently returned pair of streams will be operational. Implementations may trap if
/// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again.
///
/// The POSIX equivalent in pseudo-code is:
/// ```text
/// if (was previously connected) {
/// connect(s, AF_UNSPEC)
/// }
/// if (remote_address is Some) {
/// connect(s, remote_address)
/// }
/// ```
///
/// Unlike in POSIX, the socket must already be explicitly bound.
///
/// # Typical errors
/// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT)
/// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL)
/// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL)
/// - `invalid-state`: The socket is not bound.
/// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD)
/// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
/// - `connection-refused`: The connection was refused. (ECONNREFUSED)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html>
/// - <https://man7.org/linux/man-pages/man2/connect.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect>
/// - <https://man.freebsd.org/cgi/man.cgi?connect>
@since(version = 0.2.0)
%stream: func(remote-address: option<ip-socket-address>) -> result<tuple<incoming-datagram-stream, outgoing-datagram-stream>, error-code>;
/// Get the current bound address.
///
/// POSIX mentions:
/// > If the socket has not been bound to a local name, the value
/// > stored in the object pointed to by `address` is unspecified.
///
/// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet.
///
/// # Typical errors
/// - `invalid-state`: The socket is not bound to any local address.
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html>
/// - <https://man7.org/linux/man-pages/man2/getsockname.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getsockname>
/// - <https://man.freebsd.org/cgi/man.cgi?getsockname>
@since(version = 0.2.0)
local-address: func() -> result<ip-socket-address, error-code>;
/// Get the address the socket is currently streaming to.
///
/// # Typical errors
/// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html>
/// - <https://man7.org/linux/man-pages/man2/getpeername.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getpeername>
/// - <https://man.freebsd.org/cgi/man.cgi?query=getpeername&sektion=2&n=1>
@since(version = 0.2.0)
remote-address: func() -> result<ip-socket-address, error-code>;
/// Whether this is a IPv4 or IPv6 socket.
///
/// Equivalent to the SO_DOMAIN socket option.
@since(version = 0.2.0)
address-family: func() -> ip-address-family;
/// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
///
/// # Typical errors
/// - `invalid-argument`: (set) The TTL value must be 1 or higher.
@since(version = 0.2.0)
unicast-hop-limit: func() -> result<u8, error-code>;
@since(version = 0.2.0)
set-unicast-hop-limit: func(value: u8) -> result<_, error-code>;
/// The kernel buffer space reserved for sends/receives on this socket.
///
/// If the provided value is 0, an `invalid-argument` error is returned.
/// Any other value will never cause an error, but it might be silently clamped and/or rounded.
/// I.e. after setting a value, reading the same setting back may return a different value.
///
/// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options.
///
/// # Typical errors
/// - `invalid-argument`: (set) The provided value was 0.
@since(version = 0.2.0)
receive-buffer-size: func() -> result<u64, error-code>;
@since(version = 0.2.0)
set-receive-buffer-size: func(value: u64) -> result<_, error-code>;
@since(version = 0.2.0)
send-buffer-size: func() -> result<u64, error-code>;
@since(version = 0.2.0)
set-send-buffer-size: func(value: u64) -> result<_, error-code>;
/// Create a `pollable` which will resolve once the socket is ready for I/O.
///
/// Note: this function is here for WASI 0.2 only.
/// It's planned to be removed when `future` is natively supported in Preview3.
@since(version = 0.2.0)
subscribe: func() -> pollable;
}
@since(version = 0.2.0)
resource incoming-datagram-stream {
/// Receive messages on the socket.
///
/// This function attempts to receive up to `max-results` datagrams on the socket without blocking.
/// The returned list may contain fewer elements than requested, but never more.
///
/// This function returns successfully with an empty list when either:
/// - `max-results` is 0, or:
/// - `max-results` is greater than 0, but no results are immediately available.
/// This function never returns `error(would-block)`.
///
/// # Typical errors
/// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
/// - `connection-refused`: The connection was refused. (ECONNREFUSED)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/recvfrom.html>
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/recvmsg.html>
/// - <https://man7.org/linux/man-pages/man2/recv.2.html>
/// - <https://man7.org/linux/man-pages/man2/recvmmsg.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recvfrom>
/// - <https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms741687(v=vs.85)>
/// - <https://man.freebsd.org/cgi/man.cgi?query=recv&sektion=2>
@since(version = 0.2.0)
receive: func(max-results: u64) -> result<list<incoming-datagram>, error-code>;
/// Create a `pollable` which will resolve once the stream is ready to receive again.
///
/// Note: this function is here for WASI 0.2 only.
/// It's planned to be removed when `future` is natively supported in Preview3.
@since(version = 0.2.0)
subscribe: func() -> pollable;
}
@since(version = 0.2.0)
resource outgoing-datagram-stream {
/// Check readiness for sending. This function never blocks.
///
/// Returns the number of datagrams permitted for the next call to `send`,
/// or an error. Calling `send` with more datagrams than this function has
/// permitted will trap.
///
/// When this function returns ok(0), the `subscribe` pollable will
/// become ready when this function will report at least ok(1), or an
/// error.
///
/// Never returns `would-block`.
check-send: func() -> result<u64, error-code>;
/// Send messages on the socket.
///
/// This function attempts to send all provided `datagrams` on the socket without blocking and
/// returns how many messages were actually sent (or queued for sending). This function never
/// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned.
///
/// This function semantically behaves the same as iterating the `datagrams` list and sequentially
/// sending each individual datagram until either the end of the list has been reached or the first error occurred.
/// If at least one datagram has been sent successfully, this function never returns an error.
///
/// If the input list is empty, the function returns `ok(0)`.
///
/// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if
/// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted.
///
/// # Typical errors
/// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT)
/// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL)
/// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL)
/// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN)
/// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ)
/// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
/// - `connection-refused`: The connection was refused. (ECONNREFUSED)
/// - `datagram-too-large`: The datagram is too large. (EMSGSIZE)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/sendto.html>
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/sendmsg.html>
/// - <https://man7.org/linux/man-pages/man2/send.2.html>
/// - <https://man7.org/linux/man-pages/man2/sendmmsg.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-send>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-sendto>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasendmsg>
/// - <https://man.freebsd.org/cgi/man.cgi?query=send&sektion=2>
@since(version = 0.2.0)
send: func(datagrams: list<outgoing-datagram>) -> result<u64, error-code>;
/// Create a `pollable` which will resolve once the stream is ready to send again.
///
/// Note: this function is here for WASI 0.2 only.
/// It's planned to be removed when `future` is natively supported in Preview3.
@since(version = 0.2.0)
subscribe: func() -> pollable;
}
}

View File

@@ -0,0 +1,19 @@
package wasi:sockets@0.2.6;
@since(version = 0.2.0)
world imports {
@since(version = 0.2.0)
import instance-network;
@since(version = 0.2.0)
import network;
@since(version = 0.2.0)
import udp;
@since(version = 0.2.0)
import udp-create-socket;
@since(version = 0.2.0)
import tcp;
@since(version = 0.2.0)
import tcp-create-socket;
@since(version = 0.2.0)
import ip-name-lookup;
}

View File

@@ -0,0 +1,22 @@
/// A keyvalue interface that provides atomic operations.
///
/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to
/// fail, it will appear to the invoker of the atomic operation that the action either completed
/// successfully or did nothing at all.
///
/// Please note that this interface is bare functions that take a reference to a bucket. This is to
/// get around the current lack of a way to "extend" a resource with additional methods inside of
/// wit. Future version of the interface will instead extend these methods on the base `bucket`
/// resource.
interface atomics {
use store.{bucket, error};
/// Atomically increment the value associated with the key in the store by the given delta. It
/// returns the new value.
///
/// If the key does not exist in the store, it creates a new key-value pair with the value set
/// to the given delta.
///
/// If any other error occurs, it returns an `Err(error)`.
increment: func(bucket: borrow<bucket>, key: string, delta: u64) -> result<u64, error>;
}

View File

@@ -0,0 +1,63 @@
/// A keyvalue interface that provides batch operations.
///
/// A batch operation is an operation that operates on multiple keys at once.
///
/// Batch operations are useful for reducing network round-trip time. For example, if you want to
/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1
/// batch get operation. The batch operation is faster because it only needs to make 1 network call
/// instead of 100.
///
/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some
/// of the keys may have been modified and some may not.
///
/// This interface does has the same consistency guarantees as the `store` interface, meaning that
/// you should be able to "read your writes."
///
/// Please note that this interface is bare functions that take a reference to a bucket. This is to
/// get around the current lack of a way to "extend" a resource with additional methods inside of
/// wit. Future version of the interface will instead extend these methods on the base `bucket`
/// resource.
interface batch {
use store.{bucket, error};
/// Get the key-value pairs associated with the keys in the store. It returns a list of
/// key-value pairs.
///
/// If any of the keys do not exist in the store, it returns a `none` value for that pair in the
/// list.
///
/// MAY show an out-of-date value if there are concurrent writes to the store.
///
/// If any other error occurs, it returns an `Err(error)`.
get-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<list<option<tuple<string, list<u8>>>>, error>;
/// Set the values associated with the keys in the store. If the key already exists in the
/// store, it overwrites the value.
///
/// Note that the key-value pairs are not guaranteed to be set in the order they are provided.
///
/// If any of the keys do not exist in the store, it creates a new key-value pair.
///
/// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
/// rollback the key-value pairs that were already set. Thus, this batch operation does not
/// guarantee atomicity, implying that some key-value pairs could be set while others might
/// fail.
///
/// Other concurrent operations may also be able to see the partial results.
set-many: func(bucket: borrow<bucket>, key-values: list<tuple<string, list<u8>>>) -> result<_, error>;
/// Delete the key-value pairs associated with the keys in the store.
///
/// Note that the key-value pairs are not guaranteed to be deleted in the order they are
/// provided.
///
/// If any of the keys do not exist in the store, it skips the key.
///
/// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
/// rollback the key-value pairs that were already deleted. Thus, this batch operation does not
/// guarantee atomicity, implying that some key-value pairs could be deleted while others might
/// fail.
///
/// Other concurrent operations may also be able to see the partial results.
delete-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<_, error>;
}

View File

@@ -0,0 +1,122 @@
/// A keyvalue interface that provides eventually consistent key-value operations.
///
/// Each of these operations acts on a single key-value pair.
///
/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is
/// the common denominator for all data types defined by different key-value stores to handle data,
/// ensuring compatibility between different key-value stores. Note: the clients will be expecting
/// serialization/deserialization overhead to be handled by the key-value store. The value could be
/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects.
///
/// Data consistency in a key value store refers to the guarantee that once a write operation
/// completes, all subsequent read operations will return the value that was written.
///
/// Any implementation of this interface must have enough consistency to guarantee "reading your
/// writes." In particular, this means that the client should never get a value that is older than
/// the one it wrote, but it MAY get a newer value if one was written around the same time. These
/// guarantees only apply to the same client (which will likely be provided by the host or an
/// external capability of some kind). In this context a "client" is referring to the caller or
/// guest that is consuming this interface. Once a write request is committed by a specific client,
/// all subsequent read requests by the same client will reflect that write or any subsequent
/// writes. Another client running in a different context may or may not immediately see the result
/// due to the replication lag. As an example of all of this, if a value at a given key is A, and
/// the client writes B, then immediately reads, it should get B. If something else writes C in
/// quick succession, then the client may get C. However, a client running in a separate context may
/// still see A or B
interface store {
/// The set of errors which may be raised by functions in this package
variant error {
/// The host does not recognize the store identifier requested.
no-such-store,
/// The requesting component does not have access to the specified store
/// (which may or may not exist).
access-denied,
/// Some implementation-specific error has occurred (e.g. I/O)
other(string)
}
/// A response to a `list-keys` operation.
record key-response {
/// The list of keys returned by the query.
keys: list<string>,
/// The continuation token to use to fetch the next page of keys. If this is `null`, then
/// there are no more keys to fetch.
cursor: option<u64>
}
/// Get the bucket with the specified identifier.
///
/// `identifier` must refer to a bucket provided by the host.
///
/// `error::no-such-store` will be raised if the `identifier` is not recognized.
open: func(identifier: string) -> result<bucket, error>;
/// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the
/// bucket, and the bucket itself acts as a collection of all these entries.
///
/// It is worth noting that the exact terminology for bucket in key-value stores can very
/// depending on the specific implementation. For example:
///
/// 1. Amazon DynamoDB calls a collection of key-value pairs a table
/// 2. Redis has hashes, sets, and sorted sets as different types of collections
/// 3. Cassandra calls a collection of key-value pairs a column family
/// 4. MongoDB calls a collection of key-value pairs a collection
/// 5. Riak calls a collection of key-value pairs a bucket
/// 6. Memcached calls a collection of key-value pairs a slab
/// 7. Azure Cosmos DB calls a collection of key-value pairs a container
///
/// In this interface, we use the term `bucket` to refer to a collection of key-value pairs
resource bucket {
/// Get the value associated with the specified `key`
///
/// The value is returned as an option. If the key-value pair exists in the
/// store, it returns `Ok(value)`. If the key does not exist in the
/// store, it returns `Ok(none)`.
///
/// If any other error occurs, it returns an `Err(error)`.
get: func(key: string) -> result<option<list<u8>>, error>;
/// Set the value associated with the key in the store. If the key already
/// exists in the store, it overwrites the value.
///
/// If the key does not exist in the store, it creates a new key-value pair.
///
/// If any other error occurs, it returns an `Err(error)`.
set: func(key: string, value: list<u8>) -> result<_, error>;
/// Delete the key-value pair associated with the key in the store.
///
/// If the key does not exist in the store, it does nothing.
///
/// If any other error occurs, it returns an `Err(error)`.
delete: func(key: string) -> result<_, error>;
/// Check if the key exists in the store.
///
/// If the key exists in the store, it returns `Ok(true)`. If the key does
/// not exist in the store, it returns `Ok(false)`.
///
/// If any other error occurs, it returns an `Err(error)`.
exists: func(key: string) -> result<bool, error>;
/// Get all the keys in the store with an optional cursor (for use in pagination). It
/// returns a list of keys. Please note that for most KeyValue implementations, this is a
/// can be a very expensive operation and so it should be used judiciously. Implementations
/// can return any number of keys in a single response, but they should never attempt to
/// send more data than is reasonable (i.e. on a small edge device, this may only be a few
/// KB, while on a large machine this could be several MB). Any response should also return
/// a cursor that can be used to fetch the next page of keys. See the `key-response` record
/// for more information.
///
/// Note that the keys are not guaranteed to be returned in any particular order.
///
/// If the store is empty, it returns an empty list.
///
/// MAY show an out-of-date list of keys if there are concurrent writes to the store.
///
/// If any error occurs, it returns an `Err(error)`.
list-keys: func(cursor: option<u64>) -> result<key-response, error>;
}
}

View File

@@ -0,0 +1,16 @@
/// A keyvalue interface that provides watch operations.
///
/// This interface is used to provide event-driven mechanisms to handle
/// keyvalue changes.
interface watcher {
/// A keyvalue interface that provides handle-watch operations.
use store.{bucket};
/// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket`
/// that can be used to interact with the store.
on-set: func(bucket: bucket, key: string, value: list<u8>);
/// Handle the `delete` event for the given bucket and key. It includes a reference to the
/// `bucket` that can be used to interact with the store.
on-delete: func(bucket: bucket, key: string);
}

View File

@@ -0,0 +1,26 @@
package wasi:keyvalue@0.2.0-draft;
/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores.
/// Components targeting this world will be able to do:
///
/// 1. CRUD (create, read, update, delete) operations on key-value stores.
/// 2. Atomic `increment` and CAS (compare-and-swap) operations.
/// 3. Batch operations that can reduce the number of round trips to the network.
world imports {
/// The `store` capability allows the component to perform eventually consistent operations on
/// the key-value store.
import store;
/// The `atomic` capability allows the component to perform atomic / `increment` and CAS
/// (compare-and-swap) operations.
import atomics;
/// The `batch` capability allows the component to perform eventually consistent batch
/// operations that can reduce the number of round trips to the network.
import batch;
}
world watch-service {
include imports;
export watcher;
}

View File

@@ -0,0 +1,78 @@
package trailbase:runtime;
interface init-endpoint {
enum method-type {
get,
post,
head,
options,
patch,
delete,
put,
trace,
connect,
}
record init-result {
/// Registered http handlers (method, path)[].
http-handlers: list<tuple<method-type, string>>,
/// Registered jobs (name, spec)[].
job-handlers: list<tuple<string, string>>,
}
init: func() -> init-result;
}
interface host-endpoint {
thread-id: func() -> u64;
variant tx-error {
other(string)
}
variant value {
null,
text(string),
blob(list<u8>),
integer(s64),
real(f64),
}
// NOTE: Ideally, we'd use these but they currently block guests.
execute: func(query: string, params: list<value>) -> result<u64, tx-error>;
query: func(query: string, params: list<value>) -> result<list<list<value>>, tx-error>;
// However, transactions have to be sync.
tx-begin: func() -> result<_, tx-error>;
tx-commit: func() -> result<_, tx-error>;
tx-rollback: func() -> result<_, tx-error>;
tx-execute: func(query: string, params: list<value>) -> result<u64, tx-error>;
tx-query: func(query: string, params: list<value>) -> result<list<list<value>>, tx-error>;
}
// Note:
// * imports are provided by the host
// * exports are provided by the guest
// * includes to include a world into another world.
world trailbase {
// Pull in WASIp2 http interface for outbound requests.
import wasi:http/outgoing-handler@0.2.6;
// Pull in WASIp2 filesystem interfaces.
include wasi:filesystem/imports@0.2.6;
// Pull in WASI random interfaces.
include wasi:random/imports@0.2.6;
// Pull in WSAI Key-Value interfaces.
include wasi:keyvalue/imports@0.2.0-draft;
// Host-provided interfaces.
import host-endpoint;
// Guest-provided interfaces.
export init-endpoint;
}

View File

@@ -0,0 +1,36 @@
[package]
name = "trailbase-wasm-runtime-host"
version = "0.1.0"
edition = "2024"
license = "OSL-3.0"
description = "WASM runtime for the TrailBase framework"
homepage = "https://trailbase.io"
exclude = [
"**/node_modules/",
"**/dist/",
]
[dependencies]
bytes = "1.10.1"
futures-util = "0.3.31"
http = "1.3.1"
http-body-util = "0.1.3"
hyper = "1.6.0"
kanal = "0.1.1"
log = { version = "^0.4.21", default-features = false }
parking_lot = { workspace = true }
rusqlite = { workspace = true }
self_cell = "1.2.0"
serde = { version = "^1.0.203", features = ["derive"] }
serde_json = "^1.0.117"
thiserror = "2.0.14"
trailbase-schema = { workspace = true }
trailbase-sqlite = { workspace = true }
trailbase-wasm-common = { workspace = true }
trailbase-wasi-keyvalue = { workspace = true }
tokio = { version = "^1.38.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.41"
wasmtime = { workspace = true }
wasmtime-wasi = { workspace = true }
wasmtime-wasi-http = { workspace = true }
wasmtime-wasi-io = { workspace = true }

View File

@@ -0,0 +1,896 @@
#![forbid(clippy::unwrap_used)]
#![allow(clippy::needless_return)]
#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
mod sqlite;
use bytes::Bytes;
use core::future::Future;
use futures_util::TryFutureExt;
use futures_util::future::LocalBoxFuture;
use http_body_util::combinators::BoxBody;
use parking_lot::Mutex;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::SystemTime;
use trailbase::runtime::host_endpoint::{TxError, Value};
use trailbase_sqlite::{Params, Rows};
use trailbase_wasi_keyvalue::WasiKeyValueCtx;
use wasmtime::component::{Component, HasSelf, Linker, ResourceTable};
use wasmtime::{Config, Engine, Result, Store};
use wasmtime_wasi::p2::add_to_linker_async;
use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxView};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
use wasmtime_wasi_http::bindings::http::types::ErrorCode;
use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView};
use wasmtime_wasi_io::IoView;
use crate::exports::trailbase::runtime::init_endpoint::InitResult;
pub use trailbase_wasi_keyvalue::Store as KvStore;
static IN_FLIGHT: AtomicUsize = AtomicUsize::new(0);
// Documentation: https://docs.wasmtime.dev/api/wasmtime/component/macro.bindgen.html
wasmtime::component::bindgen!({
world: "trailbase:runtime/trailbase",
path: [
// Order-sensitive: will import *.wit from the folder.
"wit/deps-0.2.6/random",
"wit/deps-0.2.6/io",
"wit/deps-0.2.6/clocks",
"wit/deps-0.2.6/filesystem",
"wit/deps-0.2.6/sockets",
"wit/deps-0.2.6/cli",
"wit/deps-0.2.6/http",
"wit/keyvalue-0.2.0-draft",
// Ours:
"wit/trailbase.wit",
],
// NOTE: This doesn't seem to work even though it should be fixed:
// https://github.com/bytecodealliance/wasmtime/issues/10677
// i.e. can't add db locks to shared state.
require_store_data_send: false,
// NOTE: Doesn't work: https://github.com/bytecodealliance/wit-bindgen/issues/812.
// additional_derives: [
// serde::Deserialize,
// serde::Serialize,
// ],
// Interactions with `ResourceTable` can possibly trap so enable the ability
// to return traps from generated functions.
imports: {
"trailbase:runtime/host-endpoint/tx-commit": trappable,
"trailbase:runtime/host-endpoint/tx-rollback": trappable,
"trailbase:runtime/host-endpoint/tx-execute": trappable,
"trailbase:runtime/host-endpoint/tx-query": trappable,
"trailbase:runtime/host-endpoint/thread-id": trappable,
default: async | trappable,
},
exports: {
default: async,
},
});
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Wasmtime: {0}")]
Wasmtime(#[from] wasmtime::Error),
#[error("Channel closed")]
ChannelClosed,
#[error("Http Error: {0}")]
HttpErrorCode(ErrorCode),
#[error("Encoding")]
Encoding,
#[error("Other: {0}")]
Other(String),
}
pub enum Message {
Run(Box<dyn FnOnce(Rc<RuntimeInstance>) -> LocalBoxFuture<'static, ()> + Send>),
}
#[derive(Clone)]
struct LockedTransaction(Rc<Mutex<Option<sqlite::OwnedTx>>>);
unsafe impl Send for LockedTransaction {}
struct State {
resource_table: ResourceTable,
wasi_ctx: WasiCtx,
http: WasiHttpCtx,
kv: WasiKeyValueCtx,
shared: Arc<SharedState>,
tx: LockedTransaction,
}
impl Drop for State {
fn drop(&mut self) {
#[cfg(debug_assertions)]
if self.tx.0.lock().is_some() {
log::warn!("pending transaction locking the DB");
}
}
}
impl IoView for State {
fn table(&mut self) -> &mut ResourceTable {
return &mut self.resource_table;
}
}
impl WasiView for State {
fn ctx(&mut self) -> WasiCtxView<'_> {
return WasiCtxView {
ctx: &mut self.wasi_ctx,
table: &mut self.resource_table,
};
}
}
impl WasiHttpView for State {
fn ctx(&mut self) -> &mut WasiHttpCtx {
return &mut self.http;
}
fn table(&mut self) -> &mut ResourceTable {
return &mut self.resource_table;
}
/// Receives HTTP fetches from the guest.
///
/// Based on `WasiView`' default implementation.
fn send_request(
&mut self,
request: hyper::Request<wasmtime_wasi_http::body::HyperOutgoingBody>,
config: wasmtime_wasi_http::types::OutgoingRequestConfig,
) -> wasmtime_wasi_http::HttpResult<wasmtime_wasi_http::types::HostFutureIncomingResponse> {
// log::debug!(
// "send_request {:?} {}: {request:?}",
// request.uri().host(),
// request.uri().path()
// );
return match request.uri().host() {
Some("__sqlite") => {
let conn = self.shared.conn.clone();
Ok(
wasmtime_wasi_http::types::HostFutureIncomingResponse::pending(
wasmtime_wasi::runtime::spawn(async move {
Ok(sqlite::handle_sqlite_request(conn, request).await)
}),
),
)
}
_ => {
let handle = wasmtime_wasi::runtime::spawn(async move {
Ok(wasmtime_wasi_http::types::default_send_request_handler(request, config).await)
});
Ok(wasmtime_wasi_http::types::HostFutureIncomingResponse::pending(handle))
}
};
}
}
impl trailbase::runtime::host_endpoint::Host for State {
fn thread_id(&mut self) -> wasmtime::Result<u64> {
return Ok(self.shared.thread_id);
}
fn execute(
&mut self,
query: String,
params: Vec<Value>,
) -> impl Future<Output = wasmtime::Result<Result<u64, TxError>>> + Send {
let conn = self.shared.conn.clone();
let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect();
return self
.shared
.runtime
.spawn(async move {
conn
.execute(query, params)
.await
.map_err(|err| TxError::Other(err.to_string()))
.map(|v| v as u64)
})
.map_err(|err| wasmtime::Error::msg(err.to_string()));
}
fn query(
&mut self,
query: String,
params: Vec<Value>,
) -> impl Future<Output = wasmtime::Result<Result<Vec<Vec<Value>>, TxError>>> + Send {
let conn = self.shared.conn.clone();
let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect();
return self
.shared
.runtime
.spawn(async move {
let rows = conn
.write_query_rows(query, params)
.await
.map_err(|err| TxError::Other(err.to_string()))?;
let values: Vec<_> = rows
.into_iter()
.map(|trailbase_sqlite::Row(row, _col)| {
return row.into_iter().map(from_sqlite_value).collect::<Vec<_>>();
})
.collect();
Ok(values)
})
.map_err(|err| wasmtime::Error::msg(err.to_string()));
}
fn tx_begin(&mut self) -> impl Future<Output = wasmtime::Result<Result<(), TxError>>> + Send {
async fn begin(
conn: trailbase_sqlite::Connection,
tx: LockedTransaction,
) -> Result<(), TxError> {
assert!(tx.0.lock().is_none());
*tx.0.lock() = Some(
sqlite::new_tx(conn)
.await
.map_err(|err| TxError::Other(err.to_string()))?,
);
return Ok(());
}
let tx = self.tx.clone();
return self
.shared
.runtime
.spawn(begin(self.shared.conn.clone(), tx))
.map_err(|err| wasmtime::Error::msg(err.to_string()));
}
fn tx_commit(&mut self) -> wasmtime::Result<Result<(), TxError>> {
fn commit(tx: LockedTransaction) -> Result<(), TxError> {
let Some(tx) = tx.0.lock().take() else {
return Err(TxError::Other("no pending tx".to_string()));
};
// NOTE: this is the same as `tx.commit()` just w/o consuming.
let lock = tx.borrow_dependent();
lock
.execute_batch("COMMIT")
.map_err(|err| TxError::Other(err.to_string()))?;
return Ok(());
}
return Ok(commit(self.tx.clone()));
}
fn tx_rollback(&mut self) -> wasmtime::Result<Result<(), TxError>> {
fn rollback(tx: LockedTransaction) -> Result<(), TxError> {
let Some(tx) = tx.0.lock().take() else {
return Err(TxError::Other("no pending tx".to_string()));
};
// NOTE: this is the same as `tx.rollback()` just w/o consuming.
let lock = tx.borrow_dependent();
lock
.execute_batch("ROLLBACK")
.map_err(|err| TxError::Other(err.to_string()))?;
return Ok(());
}
return Ok(rollback(self.tx.clone()));
}
fn tx_execute(
&mut self,
query: String,
params: Vec<Value>,
) -> wasmtime::Result<Result<u64, TxError>> {
fn execute(tx: LockedTransaction, query: String, params: Vec<Value>) -> Result<u64, TxError> {
let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect();
let Some(ref tx) = *tx.0.lock() else {
return Err(TxError::Other("No open transaction".to_string()));
};
let lock = tx.borrow_dependent();
let mut stmt = lock
.prepare(&query)
.map_err(|err| TxError::Other(err.to_string()))?;
params
.bind(&mut stmt)
.map_err(|err| TxError::Other(err.to_string()))?;
return Ok(
stmt
.raw_execute()
.map_err(|err| TxError::Other(err.to_string()))? as u64,
);
}
return Ok(execute(self.tx.clone(), query, params));
}
fn tx_query(
&mut self,
query: String,
params: Vec<Value>,
) -> wasmtime::Result<Result<Vec<Vec<Value>>, TxError>> {
fn query_fn(
tx: LockedTransaction,
query: String,
params: Vec<Value>,
) -> Result<Vec<Vec<Value>>, TxError> {
let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect();
let Some(ref tx) = *tx.0.lock() else {
return Err(TxError::Other("No open transaction".to_string()));
};
let lock = tx.borrow_dependent();
let mut stmt = lock
.prepare(&query)
.map_err(|err| TxError::Other(err.to_string()))?;
params
.bind(&mut stmt)
.map_err(|err| TxError::Other(err.to_string()))?;
let rows =
Rows::from_rows(stmt.raw_query()).map_err(|err| TxError::Other(err.to_string()))?;
let values: Vec<_> = rows
.into_iter()
.map(|trailbase_sqlite::Row(row, _col)| {
return row.into_iter().map(from_sqlite_value).collect::<Vec<_>>();
})
.collect();
return Ok(values);
}
return Ok(query_fn(self.tx.clone(), query, params));
}
}
pub struct Runtime {
// Shared sender.
shared_sender: kanal::AsyncSender<Message>,
threads: Vec<(std::thread::JoinHandle<()>, kanal::AsyncSender<Message>)>,
}
impl Drop for Runtime {
fn drop(&mut self) {
for (handle, ch) in std::mem::take(&mut self.threads) {
// Dropping the private channel will trigger the event_loop to return.
drop(ch);
if let Err(err) = handle.join() {
log::error!("Failed to join main rt thread: {err:?}");
}
}
}
}
fn build_config(cache: Option<wasmtime::Cache>) -> Config {
let mut config = Config::new();
// Execution settings.
config.async_support(true);
config.epoch_interruption(false);
config.memory_reservation(64 * 1024 * 1024 /* bytes */);
// config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
// Compilation settings.
config.cache(cache);
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
config.parallel_compilation(true);
return config;
}
impl Runtime {
pub fn new(
n_threads: usize,
wasm_source_file: std::path::PathBuf,
conn: trailbase_sqlite::Connection,
kv_store: KvStore,
fs_root_path: Option<std::path::PathBuf>,
) -> Result<Self, Error> {
let engine = Engine::new(&build_config(Some(wasmtime::Cache::new(
wasmtime::CacheConfig::default(),
)?)))?;
// Load the component - a very expensive operation generating code. Compilation happens in
// parallel and will saturate the entire machine.
let component = {
log::info!("Compiling: {wasm_source_file:?}. May take some time...");
let start = SystemTime::now();
let component = wasmtime::CodeBuilder::new(&engine)
.wasm_binary_or_text_file(&wasm_source_file)?
.compile_component()?;
// NOTE: According to docs, this shouldn't do anything.
component.initialize_copy_on_write_image()?;
if let Ok(elapsed) = SystemTime::now().duration_since(start) {
log::info!("Loaded component {wasm_source_file:?} in: {elapsed:?}.");
}
component
};
let linker = {
let mut linker = Linker::<State>::new(&engine);
// Adds all the default WASI implementations: clocks, random, fs, ...
add_to_linker_async(&mut linker)?;
// Adds default HTTP interfaces - incoming and outgoing.
wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?;
// Add default KV interfaces.
trailbase_wasi_keyvalue::add_to_linker(&mut linker, |cx| {
trailbase_wasi_keyvalue::WasiKeyValue::new(&cx.kv, &mut cx.resource_table)
})?;
// Host interfaces.
trailbase::runtime::host_endpoint::add_to_linker::<_, HasSelf<State>>(&mut linker, |s| s)?;
linker
};
log::info!("Starting WASM runtime with {n_threads} threads.");
let (shared_sender, shared_receiver) = kanal::unbounded_async::<Message>();
let threads = (0..n_threads)
.map(|index| -> Result<_, Error> {
let (private_sender, private_receiver) = kanal::unbounded_async::<Message>();
let shared_receiver = shared_receiver.clone();
let engine = engine.clone();
let component = component.clone();
let linker = linker.clone();
let conn = conn.clone();
let kv_store = kv_store.clone();
let fs_root_path = fs_root_path.clone();
let handle = std::thread::Builder::new()
.name(format!("wasm-runtime-{index}"))
.spawn(move || {
// Note: Arc rather than Rc, since State and thus SharedState needs to be Send + Sync.
let tokio_runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.enable_io()
.build()
.expect("startup");
let shared_state = Arc::new(SharedState {
runtime: tokio_runtime,
conn,
thread_id: index as u64,
kv_store,
fs_root_path,
});
let instance = RuntimeInstance {
engine,
component,
linker,
shared: shared_state.clone(),
};
// RuntimeInstance::new(engine, component, linker, shared_state).expect("startup");
event_loop(shared_state, instance, private_receiver, shared_receiver);
})
.expect("failed to spawn thread");
return Ok((handle, private_sender));
})
.collect::<Result<Vec<_>, Error>>()?;
return Ok(Self {
shared_sender,
threads,
});
}
pub async fn call<O, F>(&self, f: F) -> Result<O, Error>
where
F: (AsyncFnOnce(&RuntimeInstance) -> O) + Send + 'static,
O: Send + 'static,
{
let (sender, receiver) = tokio::sync::oneshot::channel::<O>();
self
.shared_sender
.send(Message::Run(Box::new(move |runtime| {
Box::pin(async move {
let _ = sender.send(f(&*runtime).await);
})
})))
.await
.map_err(|_| Error::ChannelClosed)?;
return receiver.await.map_err(|_| Error::ChannelClosed);
}
}
fn event_loop(
shared_state: Arc<SharedState>,
instance: RuntimeInstance,
private_recv: kanal::AsyncReceiver<Message>,
shared_recv: kanal::AsyncReceiver<Message>,
) {
let thread_id = shared_state.thread_id;
let local = tokio::task::LocalSet::new();
let instance = Rc::new(instance);
local.block_on(&shared_state.runtime, async move {
let local_in_flight = Rc::new(AtomicUsize::new(0));
loop {
let receive_message = async || {
return tokio::select! {
msg = private_recv.recv() => msg,
msg = shared_recv.recv() => msg,
};
};
log::debug!(
"Waiting for new messages (thread: {thread_id}). In flight: {}, {}",
local_in_flight.load(Ordering::Relaxed),
IN_FLIGHT.load(Ordering::Relaxed)
);
match receive_message().await {
Ok(Message::Run(f)) => {
let instance = instance.clone();
let local_in_flight = local_in_flight.clone();
local_in_flight.fetch_add(1, Ordering::Relaxed);
IN_FLIGHT.fetch_add(1, Ordering::Relaxed);
tokio::task::spawn_local(async move {
f(instance).await;
IN_FLIGHT.fetch_sub(1, Ordering::Relaxed);
local_in_flight.fetch_sub(1, Ordering::Relaxed);
});
// Yield before listening for more messages to give JS a chance to run.
tokio::task::yield_now().await;
}
Err(_) => {
// Channel closed
return;
}
};
}
});
}
pub struct SharedState {
pub thread_id: u64,
pub runtime: tokio::runtime::Runtime,
pub conn: trailbase_sqlite::Connection,
pub kv_store: KvStore,
pub fs_root_path: Option<std::path::PathBuf>,
}
pub struct RuntimeInstance {
engine: Engine,
component: Component,
linker: Linker<State>,
shared: Arc<SharedState>,
}
impl RuntimeInstance {
// pub fn new(
// engine: Engine,
// component: Component,
// linker: Linker<State>,
// shared_state: SharedState,
// ) -> Result<Self, Error> {
// // let mut linker = Linker::<State>::new(&engine);
// //
// // // Adds all the default WASI implementations: clocks, random, fs, ...
// // add_to_linker_async(&mut linker)?;
// //
// // // Adds default HTTP interfaces - incoming and outgoing.
// // wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?;
// //
// // // Add default KV interfaces.
// // trailbase_wasi_keyvalue::add_to_linker(&mut linker, |cx| {
// // trailbase_wasi_keyvalue::WasiKeyValue::new(&cx.kv, &mut cx.resource_table)
// // })?;
// //
// // // Host interfaces.
// // trailbase::runtime::host_endpoint::add_to_linker::<_, HasSelf<State>>(&mut linker, |s|
// s)?;
//
// return Ok(Self {
// engine,
// component,
// linker,
// shared: Arc::new(shared_state),
// });
// }
fn new_store(&self) -> Result<Store<State>, Error> {
let mut wasi_ctx = WasiCtxBuilder::new();
wasi_ctx.inherit_stdio();
wasi_ctx.stdin(wasmtime_wasi::p2::pipe::ClosedInputStream);
// wasi_ctx.stdout(wasmtime_wasi::p2::Stdout);
// wasi_ctx.stderr(wasmtime_wasi::p2::Stderr);
wasi_ctx.args(&[""]);
wasi_ctx.allow_tcp(false);
wasi_ctx.allow_udp(false);
wasi_ctx.allow_ip_name_lookup(true);
if let Some(ref path) = self.shared.fs_root_path {
wasi_ctx
.preopened_dir(path, "/", DirPerms::READ, FilePerms::READ)
.map_err(|err| Error::Other(err.to_string()))?;
}
return Ok(Store::new(
&self.engine,
State {
resource_table: ResourceTable::new(),
wasi_ctx: wasi_ctx.build(),
http: WasiHttpCtx::new(),
kv: WasiKeyValueCtx::new(self.shared.kv_store.clone()),
shared: self.shared.clone(),
tx: LockedTransaction(Rc::new(Mutex::new(None))),
},
));
}
pub async fn call_init(&self) -> Result<InitResult, Error> {
let mut store = self.new_store()?;
let bindings = Trailbase::instantiate_async(&mut store, &self.component, &self.linker).await?;
return Ok(
bindings
.trailbase_runtime_init_endpoint()
.call_init(&mut store)
.await?,
);
}
pub async fn call_incoming_http_handler(
&self,
request: hyper::Request<BoxBody<Bytes, hyper::Error>>,
) -> Result<hyper::Response<wasmtime_wasi_http::body::HyperOutgoingBody>, Error> {
let mut store = self.new_store()?;
let proxy = wasmtime_wasi_http::bindings::Proxy::instantiate_async(
&mut store,
&self.component,
&self.linker,
)
.await?;
let req = store.data_mut().new_incoming_request(
wasmtime_wasi_http::bindings::http::types::Scheme::Http,
request,
)?;
let (sender, receiver) = tokio::sync::oneshot::channel::<
Result<hyper::Response<wasmtime_wasi_http::body::HyperOutgoingBody>, ErrorCode>,
>();
let out = store.data_mut().new_response_outparam(sender)?;
// NOTE: wstd streams out responses in chunks of 2kB. Only once everything has been streamed,
// `call_handle` will complete. This is also when the streaming response body completes.
//
// We cannot use `wasmtime_wasi::runtime::spawn` here, which aborts the call when the handle
// gets dropped, since we're not awaiting the response stream here. We'd either have to consume
// the entire response here, keep the handle alive or as we currently do use a non-aborting
// spawn.
//
// In the current setup, if the listening side hangs-up the they call may not be aborted.
// Depends on what the implementation does when the streaming body's receiving end gets
// out of scope.
let handle = self.shared.runtime.spawn(async move {
proxy
.wasi_http_incoming_handler()
.call_handle(&mut store, req, out)
.await
});
return match receiver.await {
Ok(Ok(resp)) => {
// NOTE: We cannot await the completion `call_handle` here with `handle.await?;`, since
// we're not consuming the response body, see above.
Ok(resp)
}
Ok(Err(err)) => {
handle
.await
.map_err(|err| Error::Other(err.to_string()))??;
Err(Error::HttpErrorCode(err))
}
Err(_) => {
log::debug!("channel closed");
handle
.await
.map_err(|err| Error::Other(err.to_string()))??;
Err(Error::ChannelClosed)
}
};
}
}
#[allow(unused)]
fn bytes_to_respone(
bytes: Vec<u8>,
) -> Result<wasmtime_wasi_http::types::HostFutureIncomingResponse, ErrorCode> {
let resp = http::Response::builder()
.status(200)
.body(sqlite::bytes_to_body(Bytes::from_owner(bytes)))
.map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?;
return Ok(
wasmtime_wasi_http::types::HostFutureIncomingResponse::ready(Ok(Ok(
wasmtime_wasi_http::types::IncomingResponse {
resp,
worker: None,
between_bytes_timeout: std::time::Duration::ZERO,
},
))),
);
}
fn to_sqlite_value(value: Value) -> trailbase_sqlite::Value {
return match value {
Value::Null => trailbase_sqlite::Value::Null,
Value::Text(s) => trailbase_sqlite::Value::Text(s),
Value::Real(f) => trailbase_sqlite::Value::Real(f),
Value::Integer(i) => trailbase_sqlite::Value::Integer(i),
Value::Blob(b) => trailbase_sqlite::Value::Blob(b),
};
}
fn from_sqlite_value(value: trailbase_sqlite::Value) -> Value {
return match value {
trailbase_sqlite::Value::Null => Value::Null,
trailbase_sqlite::Value::Text(s) => Value::Text(s),
trailbase_sqlite::Value::Real(f) => Value::Real(f),
trailbase_sqlite::Value::Integer(i) => Value::Integer(i),
trailbase_sqlite::Value::Blob(b) => Value::Blob(b),
};
}
#[cfg(test)]
mod tests {
use super::*;
use http::{Response, StatusCode};
use http_body_util::combinators::BoxBody;
use trailbase_wasm_common::{HttpContext, HttpContextKind};
#[tokio::test]
async fn test_init() {
let conn = trailbase_sqlite::Connection::open_in_memory().unwrap();
let kv_store = KvStore::new();
let runtime = Runtime::new(
2,
"../../client/testfixture/wasm/wasm_rust_guest_testfixture.wasm".into(),
conn.clone(),
kv_store,
None,
)
.unwrap();
runtime
.call(async |instance| {
instance.call_init().await.unwrap();
})
.await
.unwrap();
let response = send_http_request(
&runtime,
"http://localhost:4000/transaction",
"/transaction",
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
1,
conn
.query_row_f("SELECT COUNT(*) FROM tx;", (), |row| row.get::<_, i64>(0))
.await
.unwrap()
.unwrap()
)
}
#[tokio::test]
async fn test_transaction() {
let conn = trailbase_sqlite::Connection::open_in_memory().unwrap();
let kv_store = KvStore::new();
let runtime = Arc::new(
Runtime::new(
2,
"../../client/testfixture/wasm/wasm_rust_guest_testfixture.wasm".into(),
conn.clone(),
kv_store,
None,
)
.unwrap(),
);
let futures: Vec<_> = (0..256)
.map(|_| {
let runtime = runtime.clone();
tokio::spawn(async move {
send_http_request(
&runtime,
"http://localhost:4000/transaction",
"/transaction",
)
.await
})
})
.collect();
for future in futures {
future.await.unwrap().unwrap();
}
}
async fn send_http_request(
runtime: &Runtime,
uri: &str,
registered_path: &str,
) -> Result<Response<BoxBody<Bytes, ErrorCode>>, Error> {
fn to_header_value(context: &HttpContext) -> hyper::http::HeaderValue {
return hyper::http::HeaderValue::from_bytes(
&serde_json::to_vec(&context).unwrap_or_default(),
)
.unwrap();
}
let uri = uri.to_string();
let registered_path = registered_path.to_string();
return runtime
.call(async |instance| {
let context = HttpContext {
kind: HttpContextKind::Http,
registered_path,
path_params: vec![],
user: None,
};
let request = hyper::Request::builder()
.uri(uri)
.header("__context", to_header_value(&context))
.body(sqlite::bytes_to_body(Bytes::from_static(b"")))
.unwrap();
return instance.call_incoming_http_handler(request).await;
})
.await
.unwrap();
}
}

View File

@@ -0,0 +1,225 @@
use bytes::Bytes;
use http_body_util::{BodyExt, combinators::BoxBody};
use rusqlite::Transaction;
use self_cell::{MutBorrow, self_cell};
use tokio::time::Duration;
use trailbase_schema::json::{JsonError, rich_json_to_value, value_to_rich_json};
use trailbase_sqlite::connection::ArcLockGuard;
use trailbase_wasm_common::{SqliteRequest, SqliteResponse};
use wasmtime_wasi_http::bindings::http::types::ErrorCode;
self_cell!(
pub(crate) struct OwnedTx {
owner: MutBorrow<ArcLockGuard>,
#[covariant]
dependent: Transaction,
}
);
pub(crate) async fn new_tx(conn: trailbase_sqlite::Connection) -> Result<OwnedTx, rusqlite::Error> {
for _ in 0..200 {
let Some(lock) = conn.try_write_arc_lock_for(Duration::from_micros(100)) else {
tokio::time::sleep(Duration::from_micros(400)).await;
continue;
};
return OwnedTx::try_new(MutBorrow::new(lock), |owner| {
return owner.borrow_mut().transaction();
});
}
return Err(rusqlite::Error::ToSqlConversionFailure(
"Failed to acquire lock".into(),
));
}
async fn handle_sqlite_request_impl(
conn: trailbase_sqlite::Connection,
request: hyper::Request<wasmtime_wasi_http::body::HyperOutgoingBody>,
) -> Result<SqliteResponse, String> {
return match request.uri().path() {
// "/tx_begin" => {
// let new_tx = new_tx(conn).await.map_err(sqlite_err)?;
//
// CURRENT_TX.with(|tx: &Mutex<_>| {
// *tx.lock() = Some(new_tx);
// });
//
// Ok(SqliteResponse::TxBegin)
// }
// "/tx_commit" => {
// let tx = CURRENT_TX.with(|tx: &Mutex<_>| {
// return tx.lock().take();
// });
// if let Some(tx) = tx {
// // NOTE: this is the same as `tx.commit()` just w/o consuming.
// let lock = tx.borrow_dependent();
// lock.execute_batch("COMMIT").map_err(sqlite_err)?;
// }
//
// Ok(SqliteResponse::TxCommit)
// }
// "/tx_execute" => {
// let sqlite_request = to_request(request).await?;
//
// let params = json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?;
//
// let rows_affected = CURRENT_TX.with(move |tx: &Mutex<_>| -> Result<usize, String> {
// let Some(ref tx) = *tx.lock() else {
// return Err("No open transaction".to_string());
// };
// let lock = tx.borrow_dependent();
//
// let mut stmt = lock.prepare(&sqlite_request.query).map_err(sqlite_err)?;
//
// params.bind(&mut stmt).map_err(sqlite_err)?;
//
// return stmt.raw_execute().map_err(sqlite_err);
// })?;
//
// Ok(SqliteResponse::Execute { rows_affected })
// }
// "/tx_query " => {
// let sqlite_request = to_request(request).await?;
//
// let params = json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?;
//
// let rows = CURRENT_TX.with(move |tx: &Mutex<_>| -> Result<Rows, String> {
// let Some(ref tx) = *tx.lock() else {
// return Err("No open transaction".to_string());
// };
// let lock = tx.borrow_dependent();
//
// let mut stmt = lock.prepare(&sqlite_request.query).map_err(sqlite_err)?;
//
// params.bind(&mut stmt).map_err(sqlite_err)?;
//
// return Rows::from_rows(stmt.raw_query()).map_err(sqlite_err);
// })?;
//
// let json_rows = rows
// .iter()
// .map(|row| -> Result<Vec<serde_json::Value>, String> {
// return row_to_rich_json_array(row).map_err(sqlite_err);
// })
// .collect::<Result<Vec<_>, _>>()?;
//
// Ok(SqliteResponse::Query { rows: json_rows })
// }
// "/tx_rollback " => {
// let tx = CURRENT_TX.with(|tx: &Mutex<_>| {
// return tx.lock().take();
// });
// if let Some(tx) = tx {
// // NOTE: this is the same as `tx.rollback()` just w/o consuming.
// let lock = tx.borrow_dependent();
// lock.execute_batch("ROLLBACK").map_err(sqlite_err)?;
// }
//
// Ok(SqliteResponse::TxRollback)
// }
"/execute" => {
let sqlite_request = to_request(request).await?;
let rows_affected = conn
.execute(
sqlite_request.query,
json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?,
)
.await
.map_err(sqlite_err)?;
Ok(SqliteResponse::Execute { rows_affected })
}
"/query" => {
let sqlite_request = to_request(request).await?;
let rows = conn
.write_query_rows(
sqlite_request.query,
json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?,
)
.await
.map_err(sqlite_err)?;
let json_rows = rows
.iter()
.map(|row| -> Result<Vec<serde_json::Value>, String> {
return row_to_rich_json_array(row).map_err(sqlite_err);
})
.collect::<Result<Vec<_>, _>>()?;
Ok(SqliteResponse::Query { rows: json_rows })
}
_ => Err("Not found".to_string()),
};
}
pub(crate) async fn handle_sqlite_request(
conn: trailbase_sqlite::Connection,
request: hyper::Request<wasmtime_wasi_http::body::HyperOutgoingBody>,
) -> Result<wasmtime_wasi_http::types::IncomingResponse, ErrorCode> {
return match handle_sqlite_request_impl(conn, request).await {
Ok(response) => to_response(response),
Err(err) => to_response(SqliteResponse::Error(err)),
};
}
async fn to_request(
request: hyper::Request<wasmtime_wasi_http::body::HyperOutgoingBody>,
) -> Result<SqliteRequest, String> {
let (_parts, body) = request.into_parts();
let bytes: Bytes = body.collect().await.map_err(sqlite_err)?.to_bytes();
return serde_json::from_slice(&bytes).map_err(sqlite_err);
}
fn to_response(
response: SqliteResponse,
) -> Result<wasmtime_wasi_http::types::IncomingResponse, ErrorCode> {
let body =
serde_json::to_vec(&response).map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?;
let resp = http::Response::builder()
.status(200)
.body(bytes_to_body(Bytes::from_owner(body)))
.map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?;
return Ok(wasmtime_wasi_http::types::IncomingResponse {
resp,
worker: None,
between_bytes_timeout: std::time::Duration::ZERO,
});
}
pub(crate) fn json_values_to_sqlite_params(
values: Vec<serde_json::Value>,
) -> Result<Vec<trailbase_sqlite::Value>, JsonError> {
return values.into_iter().map(rich_json_to_value).collect();
}
pub fn row_to_rich_json_array(
row: &trailbase_sqlite::Row,
) -> Result<Vec<serde_json::Value>, JsonError> {
return (0..row.column_count())
.map(|i| -> Result<serde_json::Value, JsonError> {
let value = row.get_value(i).ok_or(JsonError::ValueNotFound)?;
return value_to_rich_json(value);
})
.collect();
}
#[inline]
pub fn bytes_to_body<E>(bytes: Bytes) -> BoxBody<Bytes, E> {
BoxBody::new(http_body_util::Full::new(bytes).map_err(|_| unreachable!()))
}
#[inline]
pub fn sqlite_err<E: std::error::Error>(err: E) -> String {
return err.to_string();
}
// #[inline]
// fn empty<E>() -> BoxBody<Bytes, E> {
// BoxBody::new(http_body_util::Empty::new().map_err(|_| unreachable!()))
// }

View File

@@ -0,0 +1 @@
../wasm-runtime-guest/wit

View File

@@ -46,7 +46,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
dev: false,
disable_auth_ui: false,
cors_allowed_origins: vec![],
js_runtime_threads: None,
..Default::default()
},
|state: AppState| async move {