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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion contracts/embedder-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ declared, per-function capability (`suspending()`), replacing v0.1's
undeclared "permitted cast"; amendment A2 (2026-08-10) extends A1 to
host-resource methods/statics (class-prototype authority), adds the
stage-3 decorator form, and makes interface members receive their
containing object as `this`.** This document supersedes `descriptor-ir.md`'s interim
containing object as `this`; amendment A3 (2026-08-11) lets `instantiate`
accept untranslated artifacts (`{ componentBytes, translator }`) and run
the translation internally.** This document supersedes `descriptor-ir.md`'s interim
"host value mapping" table as the destination for host-facing value shapes.
The runtime's *raw* boundary (`instance.exports`, `HostImports`) keeps the
`definitions.py` interpreter shapes as an **internal** surface; the
Expand Down Expand Up @@ -367,6 +369,14 @@ const instance = await instantiate(artifacts, {
- Bindgen emits the world's `Imports` type (this record, fully typed) and
`Exports` type; `instantiate` verifies the world digest
(`contracts/digest.md`) before trusting either.
- **Untranslated artifacts** (A3): `instantiate` also accepts
`{ componentBytes, translator }` where `translator` is the
translator-shim wasm bytes or a shared `Translator` instance, and
translates internally — bytes in, instance out. Prefer the shared
instance across several instantiations (the wasm compile is the cost
worth sharing; warm translation is sub-millisecond).
`requiredImports` still takes a plan: translate explicitly to inspect
the import surface before instantiating.
- **Per-interface module authoring** (the consumers' file layout) is a
helper over the same record: a module's named export, camelCase of the
interface short-name, provides that interface
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ runs both, and CI does too — these cannot silently rot).
| example | what it teaches |
|---|---|
| [`hello-world/`](hello-world/) | the smallest complete embedding: translate → instantiate → call one export; no imports |
| [`kitchen-sink/`](kitchen-sink/) | a representative tour: imports (sync / fallible / **suspending**), resources both directions, and the non-obvious value spellings (enum, variant, flags, outermost vs nested option/result, the option-boxing rule) |
| [`kitchen-sink/`](kitchen-sink/) | a representative tour: imports (sync / fallible / **suspending**), resources both directions, **streams and futures** (natural producers in, handles out), and the non-obvious value spellings (enum, variant, flags, outermost vs nested option/result, the option-boxing rule) |

The normative reference behind both is
[`contracts/embedder-api.md`](../contracts/embedder-api.md).
Expand Down
40 changes: 16 additions & 24 deletions examples/hello-world/host.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,38 @@
// The host half of the hello-world example.
// The host half of the hello-world example — the whole pipeline is one
// call: give `instantiate` the component bytes and the translator, get
// typed-shaped exports back.
//
// Pipeline, in full:
// 1. translate — the translator shim (a wasm module itself) turns the
// component binary into an execution plan + FACT adapter modules;
// 2. instantiate — the embedder API links host imports (none here) and
// returns typed-shaped exports;
// 3. call — exports are uniformly Promise-shaped (a sync guest resolves
// immediately); values cross per contracts/embedder-api.md.
// The two wasm files are read with `Deno.readFile`, so this script runs
// with a scoped read permission (run.sh passes it):
//
// Run with: ./run.sh (or: deno run --allow-read host.ts, after building
// the guest and the translator shim — run.sh does both).
// deno run --allow-read=..,../../target host.ts
//
// (Deno's `import ... with { type: "bytes" }` will make this flag-free
// once it stabilizes — it is behind --unstable-raw-imports as of Deno
// 2.9, and `type: "text"` is not an option for binaries: lossy UTF-8
// decoding corrupts them.)
//
// Inside this repository `@deltic/runtime` resolves through the Deno
// workspace; a published consumer uses the same specifier via JSR/npm or
// the `deltic-embedder.mjs` release bundle (deltic#16 tracks packaging).

import { Translator } from "@deltic/runtime/shim";
import { instantiate } from "@deltic/runtime/embedder";

// --- 1. translate ----------------------------------------------------------

const shimWasm = await Deno.readFile(
const translator = await Deno.readFile(
new URL(
"../../target/wasm32-unknown-unknown/release/translator_shim.wasm",
import.meta.url,
),
);
const translator = await Translator.create(shimWasm);

const componentBytes = await Deno.readFile(
new URL("build/hello.component.wasm", import.meta.url),
);
const { plan, adapters } = translator.translate(componentBytes);

// --- 2. instantiate --------------------------------------------------------

// The second argument is the imports record. This world imports nothing,
// so it is empty — see ../kitchen-sink for the full shape.
const component = await instantiate({ plan, componentBytes, adapters }, {});

// --- 3. call ---------------------------------------------------------------
const component = await instantiate({ componentBytes, translator }, {
// ... imports would go here; this world has none. See ../kitchen-sink.
});

// Exports are uniformly Promise-shaped (a sync guest resolves immediately).
const greeting = await component.exports.greet("component model");
console.log(greeting);

Expand Down
1 change: 1 addition & 0 deletions examples/hello-world/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ wasm-tools component new \
-o build/hello.component.wasm
wasm-tools validate --features component-model build/hello.component.wasm

deno check host.ts
deno run --allow-read=..,../../target host.ts
20 changes: 16 additions & 4 deletions examples/kitchen-sink/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ One world exercising the surfaces an embedder actually touches:
| host-implemented imports: sync, fallible, **suspending** | `notify` interface | `run-batch` | §2 |
| host-implemented resource (ctor / method / static / dispose) | `notify.channel` | `run-batch` | §3 |
| guest-implemented resource (`using`) | `api.counter` | `Counter` | §6 |
| streams: producers in, `Stream<T>` handle out | `tally`, `countdown` | §8 | §8 |
| futures: Promise in, EAGER `Future<T>` handle out | `promised-double`, `deferred-answer` | §9 | §9 |

Run it:

Expand Down Expand Up @@ -39,10 +41,20 @@ What to notice:
handed over as-is (the runtime calls `[Symbol.dispose]` when the guest
drops its handle); the guest's `counter` comes back as a constructible
class the host can `using`-scope.

Deliberately absent (to stay approachable): streams/futures and async-typed
functions — see `contracts/embedder-api.md` §"Streams and futures" until an
example covers them.
- **Streams lower from natural producers and lift as handles.** Pass an
array / ReadableStream / AsyncIterable where the guest expects a
`stream<T>`; a guest-produced stream arrives as a `Stream<T>` handle
whose `for await` yields *chunks* (`number[]` batches; `Uint8Array` for
`u8`). Guest-side, every stream/future write is a rendezvous — the
producer halves run in `spawn_local` tasks (see the guest doc comments).
- **A future-typed result is the one exception to Promise-shaped
exports**: `deferredAnswer()` returns an eager `Future<u32>` handle
synchronously (a Promise wrapper would adopt the thenable handle and
make `drop`/`cancel` unreachable). Awaiting the handle yields the value.

Deliberately absent (to stay approachable): async-typed *imports* and
`error-context` — see `contracts/embedder-api.md` until an example covers
them.

The authoritative reference is
[`contracts/embedder-api.md`](../../contracts/embedder-api.md); if this
Expand Down
74 changes: 74 additions & 0 deletions examples/kitchen-sink/guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion examples/kitchen-sink/guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ publish = false
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "=0.60.0"
# `async-spawn` backs the stream/future producer halves (countdown,
# deferred-answer): a write is a rendezvous, so it completes in a spawned
# background task while the reader half returns immediately.
wit-bindgen = { version = "=0.60.0", features = ["async-spawn"] }

[profile.release]
opt-level = "s"
Expand Down
48 changes: 48 additions & 0 deletions examples/kitchen-sink/guest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ use std::cell::Cell;
wit_bindgen::generate!({
path: "../wit",
world: "kitchen-sink",
// No `async:` option: the WIT's own `async func` markers drive
// per-function codegen — the sync exports/imports above stay sync
// (keeping the suspending-import demonstration honest: those imports
// are sync-LOWERED), while §8/§9's stream/future functions get async
// bodies.
});

use exports::deltic::kitchen_sink::api::{
Guest, GuestCounter, Level, Perms, Point, Shape,
};
use deltic::kitchen_sink::notify;
use wit_bindgen::rt::async_support::{FutureReader, StreamReader, StreamResult};

struct Component;

Expand Down Expand Up @@ -137,6 +143,48 @@ impl Guest for Component {
notify::log(notify::Level::Info, "batch: done");
Ok(reading)
}

/// §8 — consume a host-supplied stream to exhaustion.
async fn tally(mut numbers: StreamReader<u32>) -> u64 {
let mut sum = 0u64;
while let Some(v) = numbers.next().await {
sum += u64::from(v);
}
sum
}

/// §8 — produce a stream. The writer pumps in a spawned task (each
/// write is a rendezvous with the host-side reader); the reader half
/// returns immediately. Dropping the writer closes the stream.
async fn countdown(start: u32) -> StreamReader<u32> {
let (mut writer, reader) = wit_stream::new();
wit_bindgen::rt::async_support::spawn_local(async move {
for v in (1..=start).rev() {
let (result, _buf) = writer.write(vec![v]).await;
if !matches!(result, StreamResult::Complete(_)) {
break;
}
}
});
reader
}

/// §9 — await a host-supplied future (the host passed a Promise).
async fn promised_double(f: FutureReader<u32>) -> u32 {
f.await * 2
}

/// §9 — produce a future. Same rendezvous rule as streams: the write
/// completes only when the host receives the value, so it runs in a
/// spawned task while the reader half is handed back.
async fn deferred_answer() -> FutureReader<u32> {
let (writer, reader) = wit_future::new(|| 0u32);
wit_bindgen::rt::async_support::spawn_local(async move {
wit_bindgen::yield_async().await;
let _ = writer.write(42).await;
});
reader
}
}

export!(Component);
Loading
Loading