Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hosted Modules

Hosted modules expose native Rust functionality to JavaScript through the runtime.

Use hosted modules for Otter-owned APIs such as:

  • otter:kv;
  • otter:sql;
  • otter:ffi;
  • standard-facing or runtime-specific modules.

Hosted modules must enforce capabilities at the Rust boundary. Do not trust JavaScript wrappers or TypeScript declarations as the only permission check.

The hosted module crate is crates/otter-modules.

Resolution

Hosted modules are registered on the runtime builder:

let mut runtime = otter_runtime::Runtime::builder()
    .hosted_modules(otter_modules::hosted_modules().iter().copied())
    .build()?;

The module loader resolves registered otter:* specifiers directly to their specifier string. The module graph creates a hosted module namespace instead of reading a source file, so JavaScript can import the surface with normal ESM syntax:

import { openKv } from "otter:kv";
const store = openKv(":memory:");

Unregistered otter:* specifiers fail resolution.

File Layout

Use the repository naming convention:

FilePurpose
module_ext.rsRust implementation of native functions
module.jsJavaScript shim, wrapper, or polyfill

Rust code owns permissions, resource opening, async dispatch, GC rooting, and external-memory accounting. JavaScript shims may normalize arguments or provide ergonomic exports, but they must not be the only enforcement layer.

Native Boundary

Hosted module native functions should:

  1. validate arguments on the isolate thread;
  2. check capabilities before opening host resources;
  3. allocate through NativeCtx;
  4. store persistent JS references as branded roots, not raw Gc<T>;
  5. account host buffers with ExternalMemory;
  6. for async work, copy owned host data into the future and post an owned completion back to the isolate.

Do not move VM values, handles, contexts, frames, or handle scopes into Rust futures.

Bootstrap And Builders

The production builder/spec flow handles namespace installation. Hosted modules should use runtime-owned specs such as RuntimeNamespaceSpec or the HostedModuleCtx / RuntimeObjectBuilder API when their surface needs capability-aware installation. Keep module registration centralized and easy to audit. If capability enforcement or bootstrap order is delicate, prefer explicit manual code over hiding control flow behind a macro.

Module namespaces that need runtime capabilities install through HostedModuleCtx::method with HostedNativeCall::dynamic(...) closures that capture owned, Send + Sync host data such as a cloned CapabilitySet. Plain namespace exports should use HostedModuleCtx::builtin_method, HostedModuleCtx::property, and HostedModuleCtx::readonly_property. Receiver-backed resource objects should be created with RuntimeObjectBuilder::from_host_data(ctx, data) and accessed through runtime_with_host_data / runtime_with_host_data_mut. This is still a static registration path: the module specifier list is fixed, resolution is centralized, and no per-call metadata parser or hot-path dynamic registry is introduced.

Macros are appropriate when they generate the same static specs and builder calls a manual implementation would write. If a module surface needs new macro ergonomics, add the macro over the builder API rather than bypassing it.

Active Modules

The current active slices are:

  • otter:kv: openKv / kv, with in-memory and file-backed JSON stores.
  • otter:sql: openSql / sql, backed by SQLite.
  • otter:ffi: dlopen, permission-checked library loading metadata.

otter:kv and otter:sql have module-graph tests that import the module specifier and execute the exported native functions.