zena:extensions
import {…} from 'zena:extensions';
Loading components as extensions, for programs the zena command's
host runs: zena-cli:host/extensions as the compiler synthesizes it
from the host's WIT. An extension is a long-lived instance this
program calls through objects, and which calls this program through
objects this program made for it; the host forwards the calls as
typed values. The host grants it with --allow-extensions (or
ZENA_ALLOW_EXTENSIONS=1), apart from spawning; anywhere else the
imports are stubs that trap.
This program decides what an extension gets. loadExtension takes a
binding for every interface the extension may import: from the host
(fromHost), from an interface this program exports (fromProgram),
or from another extension (fromExtension). A component that imports
anything unbound does not load. loadTrustedExtension is for a
component trusted as this program trusts itself: it may import every
interface the host serves and every one this program exports.
This program's own WIT declares its side of each interface in a
shape the host pairs with the extension's, in full in
docs/design/runtime-components.md. An interface this program serves
is exported as a resource whose methods are the extension's free
functions. The object serving an extension comes from this program's
plugin object, which the load names (LoadOptions.plugin): an
instance of the class behind a plugin kind, a resource this program
exports, named after the extension's world, whose methods vend one
object per served interface. The host asks the plugin object for
each once, so the objects are the extension's identity and this
program's code sees no handle. An interface this program calls is imported as
a resource whose methods are the extension's functions with their
results wrapped in result<_, string>, with a static
attach(extension: u32) -> own<resource>, which the host serves with
an object whose method calls go to that extension. A resource the
shared interface declares crosses as a proxy, whose method calls go to
its owner and whose drop drops the owner's handle.
A call on an attached object must be awaited: a program that blocks
on it, with blockOn, keeps its instance entered, so the extension's
calls back cannot start and both sides wait on each other.
Examples ​
import { fromHost, fromProgram, loadExtension } from 'zena:extensions';
import { Policy } from 'pds:extension/policy';
let loaded = loadExtension('extensions/policy.wasm', [
fromHost('wasi:clocks/monotonic-clock@0.3.0'),
fromProgram('pds:extension/storage@1.0.0'),
], {memoryLimit: 64 * 1024 * 1024, callTimeout: milliseconds(200)});
if (loaded is (true, Extension)) {
let policy = Policy.attach(loaded[1].handle);
let verdict = await policy.checkWrite(record);
}
Classes
Extension
final class Extension implements Disposable
The most memory the instance may hold, its linear memory and GC heap together, in bytes. A growth past it fails, and a Zena component then traps, which fails the call. No limit if absent or not positive.
The most time one call may take. The limit is checked every few milliseconds, so it is approximate. No limit if absent or not positive.
Run with inlining off, so a trap's backtrace names every function on the way to it. This program's own setting if absent.
This program's object for the extension: an instance of the class
behind the plugin kind kind, which the host asks for the object
serving each interface the extension imports from this program. A
load that serves the extension anything needs one. An exported
resource's class is a Disposable, as the host disposes it with
the extension.
The plugin kind: the name of the resource this program exports for
plugin's class, which is also the name of the world the extension
is written to.
A loaded extension. handle is what attach takes. Disposing it
drops it; so does the end of this program's run.
new()
handle: u32
drop(): void
Drops the extension and its memory. Calls already made complete; later calls on an object attached to it fail. Dropping again does nothing. The plugin object and the objects it vended for the extension are disposed when this program next yields to the host, since their disposal is this program's own code, which cannot run inside this call.
Binding
class Binding(name: String, servedBy: Source)
WIT record binding.
Type aliases
LoadOptions
type LoadOptions = {memoryLimit?: i64, callTimeout?: Duration, debug?: boolean, plugin?: Disposable, kind?: String}
The limits an extension runs under. Every field is optional.
Functions
fromHost
function fromHost(name: String): Binding
name, served by the host: a WASI or zena-cli:host interface.
fromProgram
function fromProgram(name: String, exported: String | null = null): Binding
name, served by this program's export exported, which is name
when absent: a resource whose methods are the extension's functions,
vended by the plugin object the load names.
fromExtension
function fromExtension(name: String, ext: Extension): Binding
name, served by extension's export of the same name.
claimPlugin
function claimPlugin(token: u32): Disposable | null
The plugin object registered under token by a load, taken out of
the registry; null when none is. For the create static the compiler
generates for a plugin kind, not for programs.
loadExtension
async function loadExtension(path: String, bindings: Array<Binding>, options: LoadOptions = {}): Future<Result<Extension, String>>
Loads the component at path, in this program's view of the file
system, with bindings as everything it may import. Its exports are
checked against this program's imports and its imports against the
bindings, and then it is instantiated. The error says why it could
not be: a path this program cannot reach, an import without a
binding, a binding to something this program does not export, or a
function whose type does not match.
A trap, a time limit or a memory failure in a call fails that call, and the extension starts again, with fresh state, at the next call.
The load compiles and instantiates on a thread of its own; this program's other tasks run meanwhile, so a server loading an extension keeps answering.
loadTrustedExtension
async function loadTrustedExtension(path: String, options: LoadOptions = {}): Future<Result<Extension, String>>
loadExtension for a component this program trusts as it trusts
itself: it may import every interface the host serves and every
interface this program exports, by the same name. The checks on the
interfaces it does import, and the limits, are the same.
checkExtension
function checkExtension(path: String, witPath: String, world: String, deps: Array<String> | null = null): Array<String>
What keeps the component at path from being loaded as an extension
written to world of the WIT document at witPath, one line each,
or nothing when it fits: it must export every interface the world
exports, with the world's functions and resources, and import nothing
but interfaces the world imports, the same way, and interfaces the
host serves. A document the program's world was checked against with
--extension-wit is the one to check the extension against here, so
a component that fits loads. A component written in Zena fits by
construction, since zena build --wit --world checks it against the
same document; this is for one built with other tools, before it is
loaded, or at install time. The check reads the files, and so needs
the grant that loading does.
deps are WIT files and directories whose packages the document may
use; when absent, the WASI packages the standard library carries, in
wit/ under $ZENA_STDLIB_DIR, or under /stdlib, where the zena
command's hosts preopen it.