Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fallback to manifest values if they're not specified in ExtismPluginOptions #73

Merged
merged 35 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c8070a4
fix: fallback to manifest values if they're not specified in ExtismPl…
mhmd-azeez Jun 25, 2024
fb9b1a3
add tests
mhmd-azeez Jun 25, 2024
7e73e7f
log manifest
mhmd-azeez Jun 25, 2024
d004368
fix tests
mhmd-azeez Jun 25, 2024
24c1f8a
refactor
mhmd-azeez Jun 26, 2024
d3e0dbc
fix tests
mhmd-azeez Jun 26, 2024
5f658d4
fix property names to match the json structure
mhmd-azeez Jun 26, 2024
99618e2
add manifest timeout
mhmd-azeez Jun 26, 2024
4d2d57d
fix tests
mhmd-azeez Jun 26, 2024
70b1e71
fix call timeout
mhmd-azeez Jun 26, 2024
4c458e8
fix tests
mhmd-azeez Jun 26, 2024
4fe346f
make manifest property names consistent with ExtismPluginOptions
mhmd-azeez Jun 26, 2024
4568274
memory options
mhmd-azeez Jun 26, 2024
0bff08b
add tests for memory
mhmd-azeez Jun 27, 2024
dd3908c
add more tests
mhmd-azeez Jun 27, 2024
3248e19
add timeout test
mhmd-azeez Jun 27, 2024
43e75ac
add cancellation to call context
mhmd-azeez Jun 27, 2024
4f4edc8
add guard before host functions
mhmd-azeez Jun 30, 2024
7fdd7f6
timeout.wasm => sleep.wasm
mhmd-azeez Jul 1, 2024
aa3b507
use worker.terminate to cancel a call
mhmd-azeez Jul 2, 2024
9914611
refactor
mhmd-azeez Jul 2, 2024
c033ea6
fix build
mhmd-azeez Jul 2, 2024
355d1ad
revert background plugin cancellation work
mhmd-azeez Jul 2, 2024
4bf6cc9
refactor
mhmd-azeez Jul 2, 2024
c68125d
bring back cancellation
mhmd-azeez Jul 2, 2024
b6300e4
fix build
mhmd-azeez Jul 2, 2024
6404b64
refactor
mhmd-azeez Jul 4, 2024
5032877
remove timeout
mhmd-azeez Jul 4, 2024
2a8f747
apply chris' suggestions
mhmd-azeez Jul 7, 2024
8cbfdd8
use log_error instead of error_set
mhmd-azeez Jul 7, 2024
b868ec5
formatting
mhmd-azeez Jul 7, 2024
a780636
use release build
mhmd-azeez Jul 7, 2024
9630a69
fix tests
mhmd-azeez Jul 7, 2024
13fa089
fix test
mhmd-azeez Jul 7, 2024
35495de
pass worker into BackgroundPlugin's ctor
mhmd-azeez Jul 7, 2024
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
116 changes: 84 additions & 32 deletions src/background-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*eslint-disable no-empty*/
import { CallContext, RESET, IMPORT_STATE, EXPORT_STATE, STORE, GET_BLOCK } from './call-context.ts';
import { PluginOutput, SAB_BASE_OFFSET, SharedArrayBufferSection, type InternalConfig } from './interfaces.ts';
import { CallContext, RESET, IMPORT_STATE, EXPORT_STATE, STORE, GET_BLOCK, ENV } from './call-context.ts';
import { MemoryOptions, PluginOutput, SAB_BASE_OFFSET, SharedArrayBufferSection, type InternalConfig } from './interfaces.ts';
import { readBodyUpTo } from './utils.ts';
import { WORKER_URL } from './worker-url.ts';
import { Worker } from 'node:worker_threads';
import { CAPABILITIES } from './polyfills/deno-capabilities.ts';
Expand Down Expand Up @@ -36,25 +37,47 @@ const AtomicsWaitAsync =
})();

class BackgroundPlugin {
worker: Worker;
sharedData: SharedArrayBuffer;
sharedDataView: DataView;
hostFlag: Int32Array;
opts: InternalConfig;
worker?: Worker | undefined;
modules: WebAssembly.Module[];
names: string[];
mhmd-azeez marked this conversation as resolved.
Show resolved Hide resolved

#context: CallContext;
#request: [(result: any[]) => void, (result: any[]) => void] | null = null;

constructor(worker: Worker, sharedData: SharedArrayBuffer, opts: InternalConfig, context: CallContext) {
this.worker = worker;
constructor(worker: Worker, sharedData: SharedArrayBuffer, names: string[], modules: WebAssembly.Module[], opts: InternalConfig, context: CallContext) {
this.sharedData = sharedData;
this.sharedDataView = new DataView(sharedData);
this.hostFlag = new Int32Array(sharedData);
this.opts = opts;
this.names = names;
this.modules = modules;
this.#context = context;

this.hostFlag[0] = SAB_BASE_OFFSET;
this.setWorker(worker);
}

async restartWorker() {
await this.close();

const worker = await createWorker(this.opts, this.names, this.modules, this.sharedData);
this.setWorker(worker);
}

setWorker(worker: Worker) {

this.#context[RESET]();

if (this.#request) {
this.#request[1]([new Error('Call canceled due to call to restartWorker()')])
}
this.#request = null;
mhmd-azeez marked this conversation as resolved.
Show resolved Hide resolved

this.worker = worker;
this.worker.on('message', (ev) => this.#handleMessage(ev));
}

Expand Down Expand Up @@ -126,6 +149,10 @@ class BackgroundPlugin {

this.#request = [resolve as any, reject as any];

if (!this.worker) {
throw new Error('worker not initialized');
}
mhmd-azeez marked this conversation as resolved.
Show resolved Hide resolved

this.worker.postMessage({
type: 'invoke',
handler,
Expand Down Expand Up @@ -212,7 +239,7 @@ class BackgroundPlugin {
//
// - https://github.com/nodejs/node/pull/44409
// - https://github.com/denoland/deno/issues/14786
const timer = setInterval(() => {}, 0);
const timer = setInterval(() => { }, 0);
try {
if (!func) {
throw Error(`Plugin error: host function "${ev.namespace}" "${ev.func}" does not exist`);
Expand Down Expand Up @@ -337,7 +364,7 @@ class RingBufferWriter {

signal() {
const old = Atomics.load(this.flag, 0);
while (Atomics.compareExchange(this.flag, 0, old, this.outputOffset) === old) {}
while (Atomics.compareExchange(this.flag, 0, old, this.outputOffset) === old) { }
Atomics.notify(this.flag, 0, 1);
}

Expand Down Expand Up @@ -412,11 +439,13 @@ class HttpContext {
fetch: typeof fetch;
lastStatusCode: number;
allowedHosts: string[];
memoryOptions: MemoryOptions;

constructor(_fetch: typeof fetch, allowedHosts: string[]) {
constructor(_fetch: typeof fetch, allowedHosts: string[], memoryOptions: MemoryOptions) {
this.fetch = _fetch;
this.allowedHosts = allowedHosts;
this.lastStatusCode = 0;
this.memoryOptions = memoryOptions;
}

contribute(functions: Record<string, Record<string, any>>) {
Expand Down Expand Up @@ -453,9 +482,23 @@ class HttpContext {
});

this.lastStatusCode = response.status;
const result = callContext.store(new Uint8Array(await response.arrayBuffer()));

return result;
try {
let bytes = this.memoryOptions.maxHttpResponseBytes ?
await readBodyUpTo(response, this.memoryOptions.maxHttpResponseBytes) :
new Uint8Array(await response.arrayBuffer());

const result = callContext.store(bytes);

return result;
} catch (err) {
if (err instanceof Error) {
const ptr = callContext.store(new TextEncoder().encode(err.message));
callContext[ENV].log_error(ptr);
return 0n;
}
return 0n;
}
}
}

Expand All @@ -464,11 +507,28 @@ export async function createBackgroundPlugin(
names: string[],
modules: WebAssembly.Module[],
): Promise<BackgroundPlugin> {
const worker = new Worker(WORKER_URL);
const context = new CallContext(SharedArrayBuffer, opts.logger, opts.config);
const httpContext = new HttpContext(opts.fetch, opts.allowedHosts);
const context = new CallContext(SharedArrayBuffer, opts.logger, opts.config, opts.memory);
const httpContext = new HttpContext(opts.fetch, opts.allowedHosts, opts.memory);
httpContext.contribute(opts.functions);

// NB(chrisdickinson): We *have* to create the SharedArrayBuffer in
// the parent context because -- for whatever reason! -- chromium does
// not allow the creation of shared buffers in worker contexts, but firefox
// and webkit do.
const sharedData = new (SharedArrayBuffer as any)(opts.sharedArrayBufferSize);
new Uint8Array(sharedData).subarray(8).fill(0xfe);

const worker = await createWorker(opts, names, modules, sharedData);
return new BackgroundPlugin(worker, sharedData, names, modules, opts, context);
}

async function createWorker(
opts: InternalConfig,
names: string[],
modules: WebAssembly.Module[],
sharedData: SharedArrayBuffer): Promise<Worker> {
const worker = new Worker(WORKER_URL);

await new Promise((resolve, reject) => {
worker.on('message', function handler(ev) {
if (ev?.type !== 'initialized') {
Expand All @@ -480,13 +540,16 @@ export async function createBackgroundPlugin(
});
});

// NB(chrisdickinson): We *have* to create the SharedArrayBuffer in
// the parent context because -- for whatever reason! -- chromium does
// not allow the creation of shared buffers in worker contexts, but firefox
// and webkit do.
const sharedData = new (SharedArrayBuffer as any)(opts.sharedArrayBufferSize);
const onready = new Promise((resolve, reject) => {
worker.on('message', function handler(ev) {
if (ev?.type !== 'ready') {
reject(new Error(`received unexpected message (type=${ev?.type})`));
}

new Uint8Array(sharedData).subarray(8).fill(0xfe);
worker.removeListener('message', handler);
resolve(null);
});
});

const { fetch: _, logger: __, ...rest } = opts;
const message = {
Expand All @@ -498,19 +561,8 @@ export async function createBackgroundPlugin(
sharedData,
};

const onready = new Promise((resolve, reject) => {
worker.on('message', function handler(ev) {
if (ev?.type !== 'ready') {
reject(new Error(`received unexpected message (type=${ev?.type})`));
}

worker.removeListener('message', handler);
resolve(null);
});
});

worker.postMessage(message);
await onready;

return new BackgroundPlugin(worker, sharedData, opts, context);
}
return worker;
}
29 changes: 26 additions & 3 deletions src/call-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type PluginConfig, PluginOutput } from './interfaces.ts';
import { type PluginConfig, PluginOutput, MemoryOptions } from './interfaces.ts';
import { CAPABILITIES } from './polyfills/deno-capabilities.ts';

export const BEGIN = Symbol('begin');
Expand Down Expand Up @@ -49,16 +49,18 @@ export class CallContext {
#logger: Console;
#decoder: TextDecoder;
#encoder: TextEncoder;
#arrayBufferType: { new (size: number): ArrayBufferLike };
#arrayBufferType: { new(size: number): ArrayBufferLike };
#config: PluginConfig;
#vars: Map<string, number> = new Map();
#memoryOptions: MemoryOptions;

/** @hidden */
constructor(type: { new (size: number): ArrayBufferLike }, logger: Console, config: PluginConfig) {
constructor(type: { new(size: number): ArrayBufferLike }, logger: Console, config: PluginConfig, memoryOptions: MemoryOptions) {
this.#arrayBufferType = type;
this.#logger = logger;
this.#decoder = new TextDecoder();
this.#encoder = new TextEncoder();
this.#memoryOptions = memoryOptions;

this.#stack = [];

Expand All @@ -76,6 +78,18 @@ export class CallContext {
const block = new Block(new this.#arrayBufferType(Number(size)), true);
const index = this.#blocks.length;
this.#blocks.push(block);

if (this.#memoryOptions.maxPages) {
const pageSize = 64 * 1024;
const totalBytes = this.#blocks.reduce((acc, block) => acc + (block?.buffer.byteLength ?? 0), 0)
const totalPages = Math.ceil(totalBytes / pageSize);

if (totalPages > this.#memoryOptions.maxPages) {
this.#logger.error(`memory limit exceeded: ${totalPages} pages requested, ${this.#memoryOptions.maxPages} allowed`);
return 0n;
}
}

return Block.indexToAddress(index);
}

Expand Down Expand Up @@ -282,6 +296,15 @@ export class CallContext {
return 0n;
}

const valueBlock = this.#blocks[Block.addressToIndex(valueaddr)];
if (this.#memoryOptions.maxVarBytes) {
const currentBytes = [...this.#vars.values()].map(idx => this.#blocks[idx]?.byteLength ?? 0).reduce((acc, length) => acc + length, 0)
const totalBytes = currentBytes + (valueBlock?.byteLength ?? 0);
if (totalBytes > this.#memoryOptions.maxVarBytes) {
throw Error(`var memory limit exceeded: ${totalBytes} bytes requested, ${this.#memoryOptions.maxVarBytes} allowed`);
}
}

this.#vars.set(key, Block.addressToIndex(valueaddr));
},

Expand Down
23 changes: 13 additions & 10 deletions src/foreground-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ export class ForegroundPlugin {
#instancePair: InstantiatedModule;
#active: boolean = false;
#wasi: InternalWasi[];
#opts: InternalConfig;

constructor(context: CallContext, instancePair: InstantiatedModule, wasi: InternalWasi[]) {
constructor(opts: InternalConfig, context: CallContext, instancePair: InstantiatedModule, wasi: InternalWasi[]) {
this.#context = context;
this.#instancePair = instancePair;
this.#wasi = wasi;
this.#opts = opts;
}

async reset(): Promise<boolean> {
Expand Down Expand Up @@ -61,6 +63,7 @@ export class ForegroundPlugin {

async call(funcName: string, input?: string | Uint8Array): Promise<PluginOutput | null> {
const inputIdx = this.#context[STORE](input);

const [errorIdx, outputIdx] = await this.callBlock(funcName, inputIdx);
const shouldThrow = errorIdx !== null;
const idx = errorIdx ?? outputIdx;
Expand Down Expand Up @@ -103,7 +106,7 @@ export async function createForegroundPlugin(
opts: InternalConfig,
names: string[],
modules: WebAssembly.Module[],
context: CallContext = new CallContext(ArrayBuffer, opts.logger, opts.config),
context: CallContext = new CallContext(ArrayBuffer, opts.logger, opts.config, opts.memory),
): Promise<ForegroundPlugin> {
const imports: Record<string, Record<string, any>> = {
[EXTISM_ENV]: context[ENV],
Expand All @@ -127,7 +130,7 @@ export async function createForegroundPlugin(

const instance = await instantiateModule(['main'], modules[mainIndex], imports, opts, wasiList, names, modules, seen);

return new ForegroundPlugin(context, [modules[mainIndex], instance], wasiList);
return new ForegroundPlugin(opts, context, [modules[mainIndex], instance], wasiList);
}

async function instantiateModule(
Expand Down Expand Up @@ -211,9 +214,9 @@ async function instantiateModule(
const instance = providerExports.find((xs) => xs.name === '_start')
? await instantiateModule([...current, module], provider, imports, opts, wasiList, names, modules, new Map())
: !linked.has(provider)
? (await instantiateModule([...current, module], provider, imports, opts, wasiList, names, modules, linked),
linked.get(provider))
: linked.get(provider);
? (await instantiateModule([...current, module], provider, imports, opts, wasiList, names, modules, linked),
linked.get(provider))
: linked.get(provider);

if (!instance) {
// circular import, either make a trampoline or bail
Expand Down Expand Up @@ -254,10 +257,10 @@ async function instantiateModule(
const guestType = instance.exports.hs_init
? 'haskell'
: instance.exports._initialize
? 'reactor'
: instance.exports._start
? 'command'
: 'none';
? 'reactor'
: instance.exports._start
? 'command'
: 'none';

if (wasi) {
await wasi?.initialize(instance);
Expand Down
Loading
Loading