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
21 changes: 15 additions & 6 deletions docs/wasm-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,14 @@ list restricts native parsing to those identifiers.

## The WASM ABI

There are **no imports** — codesight instantiates with an empty import object, so
the module must not require WASI, JS-binding glue, or any host functions.
The host instantiates every module with a **minimal WASI import object** (Node's
built-in `node:wasi` — clock/random/exit/stderr only; **no filesystem, no network,
no env/args**). Pure-compute modules with no imports (Rust/AssemblyScript on
`wasm32-unknown-unknown`) ignore it; modules whose runtime needs WASI (e.g. Go
`//go:wasmexport` reactors) get exactly those minimal capabilities. A module that
exports `_initialize` is initialized before any other export is called. The module
must not require JS-binding glue or host functions beyond WASI. `node:wasi` is built
in, so codesight keeps its zero-dependency guarantee.

A conforming module exports a small fixed core plus one optional `parse*` function
per capability it provides:
Expand Down Expand Up @@ -277,10 +283,13 @@ from built-in `ast`/`regex` results in the scan summary.

## Plugin skeleton

Implement the module in any toolchain that compiles to a `wasm32` module with **no
imports** (no WASI, no JS-binding glue). The allocator and the per-kind marshalling
are boilerplate; the only part that changes is your extraction logic. Export only
the `parse*` functions for the kinds you support.
Implement the module in any toolchain that targets `wasm32` and imports **at most
`wasi_snapshot_preview1`** (no JS-binding glue, no other host functions). Pure-compute
languages (Rust/AssemblyScript via `wasm32-unknown-unknown`) need no imports at all;
full-runtime languages (Go via `GOOS=wasip1 -buildmode=c-shared`) import WASI, which
the host supplies minimally. The allocator and the per-kind marshalling are
boilerplate; the only part that changes is your extraction logic. Export only the
`parse*` functions for the kinds you support.

Required exports and their behavior, in pseudocode:

Expand Down
46 changes: 45 additions & 1 deletion src/wasm/plugin-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { createRequire } from "node:module";
import type { NativeLang } from "../types.js";

// `node:wasi` is loaded lazily (below) rather than via a static import: importing
// it emits a one-time ExperimentalWarning at module-load, which would fire on
// every run. Deferring the load to first plugin use lets us install a targeted
// warning filter first, and avoids loading WASI at all when native AST is off.
const nodeRequire = createRequire(import.meta.url);

/** Capability namespace, part of the discovery filename: codesight-<lang>-<CAPABILITY>.wasm */
const CAPABILITY = "ast";
/** ABI/contract version the host understands. A plugin reporting a different value is skipped. */
Expand Down Expand Up @@ -65,6 +72,24 @@ export function resetNativePluginProvider(): void {
// Instance cache keyed by resolved .wasm path (or sentinel for "tried, not loadable").
const cache = new Map<string, LoadedPlugin | null>();

// node:wasi prints a one-time "ExperimentalWarning: WASI is an experimental
// feature..." to stderr when constructed. Suppress *only* that specific warning
// (installed lazily, the first time we actually use WASI) so it doesn't pollute
// CLI output — every other process warning passes through unchanged.
let wasiWarningSuppressed = false;
function suppressWasiExperimentalWarning(): void {
if (wasiWarningSuppressed) return;
wasiWarningSuppressed = true;
const original = process.emitWarning.bind(process);
(process as { emitWarning: unknown }).emitWarning = (warning: unknown, ...args: unknown[]) => {
const opt = args[0];
const type = opt && typeof opt === "object" ? (opt as { type?: string }).type : opt;
const message = typeof warning === "string" ? warning : (warning as Error | undefined)?.message ?? "";
if (type === "ExperimentalWarning" && String(message).includes("WASI")) return;
return (original as (...a: unknown[]) => void)(warning, ...args);
};
}

/**
* Resolve + load the plugin for `lang`, searching `pluginDirs` in order. Returns
* null when no plugin is available; a malformed/incompatible plugin is treated
Expand All @@ -90,7 +115,26 @@ export function loadPlugin(lang: NativeLang, pluginDirs: string[]): LoadedPlugin
try {
const bytes = readFileSync(wasmPath);
const module = new WebAssembly.Module(bytes);
const instance = new WebAssembly.Instance(module, {}); // no imports

// Always instantiate with a WASI import object. Pure-compute (no-imports)
// plugins ignore the extras; plugins whose runtime needs WASI (e.g. Go
// reactors built with //go:wasmexport) get a minimal, capability-restricted
// environment — NO filesystem, NO network, no args/env. node:wasi is built
// in, so this keeps the zero-dependency constraint. "Plugins are pure
// compute" — if a kind ever needs project context, it flows through the ABI,
// not the filesystem.
suppressWasiExperimentalWarning();
const { WASI } = nodeRequire("node:wasi") as typeof import("node:wasi");
const wasi = new WASI({ version: "preview1" });
const instance = new WebAssembly.Instance(module, wasi.getImportObject() as WebAssembly.Imports);

// Reactor modules (Go) export `_initialize` and must be initialized before
// any other export is called. No-imports modules (Rust/AssemblyScript) have
// neither `_initialize` nor `_start` and are used as-is.
if (typeof (instance.exports as Record<string, unknown>)._initialize === "function") {
wasi.initialize(instance);
}

loaded = bindExports(instance.exports);
} catch {
loaded = null; // unloadable plugin → treated as absent
Expand Down
Loading