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
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"author": "The Extism Authors <oss@extism.org>",
"license": "BSD-3-Clause",
"devDependencies": {
"@bjorn3/browser_wasi_shim": "^0.2.14",
"@bjorn3/browser_wasi_shim": "^0.2.17",
"@playwright/test": "^1.39.0",
"@types/node": "^20.8.7",
"@typescript-eslint/eslint-plugin": "^6.8.0",
Expand Down
2 changes: 1 addition & 1 deletion src/foreground-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export async function createForegroundPlugin(
modules: WebAssembly.Module[],
context: CallContext = new CallContext(ArrayBuffer, opts.logger, opts.config),
): Promise<ForegroundPlugin> {
const wasi = opts.wasiEnabled ? await loadWasi(opts.allowedPaths) : null;
const wasi = opts.wasiEnabled ? await loadWasi(opts.allowedPaths, opts.enableWasiOutput) : null;

const imports: Record<string, Record<string, any>> = {
...(wasi ? { wasi_snapshot_preview1: await wasi.importObject() } : {}),
Expand Down
43 changes: 29 additions & 14 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,39 +49,39 @@ export class PluginOutput extends DataView {
throw new Error('Cannot set values on output');
}

setInt16(_byteOffset: number, _value: number, _littleEndian?: boolean | undefined): void {
setInt16(_byteOffset: number, _value: number, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}

setInt32(_byteOffset: number, _value: number, _littleEndian?: boolean | undefined): void {
setInt32(_byteOffset: number, _value: number, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}

setUint8(_byteOffset: number, _value: number): void {
throw new Error('Cannot set values on output');
}

setUint16(_byteOffset: number, _value: number, _littleEndian?: boolean | undefined): void {
setUint16(_byteOffset: number, _value: number, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}

setUint32(_byteOffset: number, _value: number, _littleEndian?: boolean | undefined): void {
setUint32(_byteOffset: number, _value: number, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}

setFloat32(_byteOffset: number, _value: number, _littleEndian?: boolean | undefined): void {
setFloat32(_byteOffset: number, _value: number, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}

setFloat64(_byteOffset: number, _value: number, _littleEndian?: boolean | undefined): void {
setFloat64(_byteOffset: number, _value: number, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}

setBigInt64(_byteOffset: number, _value: bigint, _littleEndian?: boolean | undefined): void {
setBigInt64(_byteOffset: number, _value: bigint, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}

setBigUint64(_byteOffset: number, _value: bigint, _littleEndian?: boolean | undefined): void {
setBigUint64(_byteOffset: number, _value: bigint, _littleEndian?: boolean): void {
throw new Error('Cannot set values on output');
}
}
Expand Down Expand Up @@ -130,14 +130,14 @@ export interface ExtismPluginOptions {
/**
* Whether or not to enable WASI preview 1.
*/
useWasi?: boolean | undefined;
useWasi?: boolean;

/**
* Whether or not to run the Wasm module in a Worker thread. Requires
* {@link Capabilities#hasWorkerCapability | `CAPABILITIES.hasWorkerCapability`} to
* be true.
*/
runInWorker?: boolean | undefined;
runInWorker?: boolean;

/**
* A logger implementation. Must provide `info`, `debug`, `warn`, and `error` methods.
Expand All @@ -163,7 +163,14 @@ export interface ExtismPluginOptions {
functions?: { [key: string]: { [key: string]: (callContext: CallContext, ...args: any[]) => any } } | undefined;
allowedPaths?: { [key: string]: string } | undefined;
allowedHosts?: string[] | undefined;
config?: PluginConfigLike | undefined;

/**
* Whether WASI stdout should be forwarded to the host.
*
* Overrides the `EXTISM_ENABLE_WASI_OUTPUT` environment variable.
*/
enableWasiOutput?: boolean;
config?: PluginConfigLike;
fetch?: typeof fetch;
sharedArrayBufferSize?: number;
}
Expand All @@ -172,6 +179,7 @@ export interface InternalConfig {
logger: Console;
allowedHosts: string[];
allowedPaths: { [key: string]: string };
enableWasiOutput: boolean;
functions: { [namespace: string]: { [func: string]: any } };
fetch: typeof fetch;
wasiEnabled: boolean;
Expand Down Expand Up @@ -241,8 +249,8 @@ export type ManifestWasm = (
| ManifestWasmResponse
| ManifestWasmModule
) & {
name?: string | undefined;
hash?: string | undefined;
name?: string;
hash?: string;
};

/**
Expand All @@ -263,7 +271,7 @@ export type ManifestWasm = (
*/
export interface Manifest {
wasm: Array<ManifestWasm>;
config?: PluginConfigLike | undefined;
config?: PluginConfigLike;
}

/**
Expand Down Expand Up @@ -373,4 +381,11 @@ export interface Capabilities {
* - ✅ webkit (via [`@bjorn3/browser_wasi_shim`](https://www.npmjs.com/package/@bjorn3/browser_wasi_shim))
*/
supportsWasiPreview1: boolean;

/**
* Whether or not the `EXTISM_ENABLE_WASI_OUTPUT` environment variable has been set.
*
* This value is consulted whenever {@link ExtismPluginOptions#enableWasiOutput} is omitted.
*/
extismStdoutEnvVarSet: boolean;
}
42 changes: 42 additions & 0 deletions src/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,48 @@ if (typeof WebAssembly === 'undefined') {
await plugin.close();
}
});

// TODO(chrisdickinson): this turns out to be pretty tricky to test, since
// deno and node's wasi bindings bypass JS entirely and write directly to
// their respective FDs. I'm settling for tests that exercise both behaviors.
test('when EXTISM_ENABLE_WASI_OUTPUT is not set, WASI output is stifled', async () => {
if ((globalThis as unknown as any).process) {
(
globalThis as unknown as Record<string, { env: Record<string, string> }>
).process.env.EXTISM_ENABLE_WASI_OUTPUT = '';
} else if ((globalThis as unknown as any).Deno) {
globalThis.Deno.env.set('EXTISM_ENABLE_WASI_OUTPUT', '');
}
const plugin = await createPlugin('http://localhost:8124/wasm/wasistdout.wasm', {
useWasi: true,
});

try {
await plugin.call('say_hello');
} finally {
await plugin.close();
}
});

test('respects enableWasiOutput', async () => {
if ((globalThis as unknown as any).process) {
(
globalThis as unknown as Record<string, { env: Record<string, string> }>
).process.env.EXTISM_ENABLE_WASI_OUTPUT = '';
} else if ((globalThis as unknown as any).Deno) {
globalThis.Deno.env.set('EXTISM_ENABLE_WASI_OUTPUT', '');
}
const plugin = await createPlugin('http://localhost:8124/wasm/wasistdout.wasm', {
useWasi: true,
enableWasiOutput: true,
});

try {
await plugin.call('say_hello');
} finally {
await plugin.close();
}
});
}

if (CAPABILITIES.fsAccess && CAPABILITIES.supportsWasiPreview1) {
Expand Down
2 changes: 2 additions & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function createPlugin(
): Promise<Plugin> {
opts = { ...opts };
opts.useWasi ??= false;
opts.enableWasiOutput ??= opts.useWasi ? CAPABILITIES.extismStdoutEnvVarSet : false;
opts.functions = opts.functions || {};
opts.allowedPaths ??= {};
opts.allowedHosts ??= <any>[].concat(opts.allowedHosts || []);
Expand All @@ -93,6 +94,7 @@ export async function createPlugin(
wasiEnabled: opts.useWasi,
logger: opts.logger,
config: opts.config,
enableWasiOutput: opts.enableWasiOutput,
sharedArrayBufferSize: Number(opts.sharedArrayBufferSize) || 1 << 16,
};

Expand Down
2 changes: 2 additions & 0 deletions src/polyfills/browser-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ export const CAPABILITIES: Capabilities = {
: true,

supportsWasiPreview1: true,

extismStdoutEnvVarSet: false,
};
48 changes: 40 additions & 8 deletions src/polyfills/browser-wasi.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
import { WASI, Fd, File, OpenFile } from '@bjorn3/browser_wasi_shim';
import { WASI, Fd, File, OpenFile, wasi } from '@bjorn3/browser_wasi_shim';
import { type InternalWasi } from '../mod.ts';

export async function loadWasi(_allowedPaths: { [from: string]: string }): Promise<InternalWasi> {
class Output extends Fd {
#mode: string;

constructor(mode: string) {
super();
this.#mode = mode;
}

fd_write(view8: Uint8Array, iovs: [wasi.Iovec]): { ret: number; nwritten: number } {
let nwritten = 0;
const decoder = new TextDecoder();
const str = iovs.reduce((acc, iovec, idx, all) => {
nwritten += iovec.buf_len;
const buffer = view8.slice(iovec.buf, iovec.buf + iovec.buf_len);
return acc + decoder.decode(buffer, { stream: idx !== all.length - 1 });
}, '');

(console[this.#mode] as any)(str);

return { ret: 0, nwritten };
}
}

export async function loadWasi(
_allowedPaths: { [from: string]: string },
enableWasiOutput: boolean,
): Promise<InternalWasi> {
const args: Array<string> = [];
const envVars: Array<string> = [];
const fds: Fd[] = [
new OpenFile(new File([])), // stdin
new OpenFile(new File([])), // stdout
new OpenFile(new File([])), // stderr
];
const fds: Fd[] = enableWasiOutput
? [
new Output('log'), // fd 0 is dup'd to stdout
new Output('log'),
new Output('error'),
]
: [
new OpenFile(new File([])), // stdin
new OpenFile(new File([])), // stdout
new OpenFile(new File([])), // stderr
];

const context = new WASI(args, envVars, fds);

Expand Down Expand Up @@ -38,7 +70,7 @@ export async function loadWasi(_allowedPaths: { [from: string]: string }): Promi
} else {
init();
}
} else if (instance.exports._start) {
} else {
context.start({
exports: {
memory,
Expand Down
2 changes: 2 additions & 0 deletions src/polyfills/bun-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ export const CAPABILITIES: Capabilities = {

// See https://github.com/oven-sh/bun/issues/1960
supportsWasiPreview1: false,

extismStdoutEnvVarSet: Boolean(process.env.EXTISM_ENABLE_WASI_OUTPUT),
};
4 changes: 4 additions & 0 deletions src/polyfills/deno-capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Capabilities } from '../interfaces.ts';

const { Deno } = globalThis as unknown as { Deno: { env: Map<string, string> } };

export const CAPABILITIES: Capabilities = {
// When false, shared buffers have to be copied to an array
// buffer before passing to Text{En,De}coding()
Expand All @@ -16,4 +18,6 @@ export const CAPABILITIES: Capabilities = {
hasWorkerCapability: true,

supportsWasiPreview1: true,

extismStdoutEnvVarSet: Boolean(Deno.env.get('EXTISM_ENABLE_WASI_OUTPUT')),
};
28 changes: 27 additions & 1 deletion src/polyfills/deno-wasi.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import Context from 'https://deno.land/std@0.200.0/wasi/snapshot_preview1.ts';
import { type InternalWasi } from '../interfaces.ts';
import { devNull } from 'node:os';
import { open } from 'node:fs/promises';
import { closeSync } from 'node:fs';

export async function loadWasi(allowedPaths: { [from: string]: string }): Promise<InternalWasi> {
async function createDevNullFDs() {
const [stdin, stdout] = await Promise.all([open(devNull, 'r'), open(devNull, 'w')]);

const fr = new globalThis.FinalizationRegistry((held: number) => {
try {
closeSync(held);
} catch {
// The fd may already be closed.
}
});
fr.register(stdin, stdin.fd);
fr.register(stdout, stdout.fd);

return [stdin.fd, stdout.fd, stdout.fd];
}

export async function loadWasi(
allowedPaths: { [from: string]: string },
enableWasiOutput: boolean,
): Promise<InternalWasi> {
const [stdin, stdout, stderr] = enableWasiOutput ? [0, 1, 2] : await createDevNullFDs();
const context = new Context({
preopens: allowedPaths,
exitOnReturn: false,
stdin,
stdout,
stderr,
});

return {
Expand Down
2 changes: 2 additions & 0 deletions src/polyfills/node-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export const CAPABILITIES: Capabilities = {
hasWorkerCapability: true,

supportsWasiPreview1: true,

extismStdoutEnvVarSet: Boolean(process.env.EXTISM_ENABLE_WASI_OUTPUT),
};
Loading