Skip to content

Commit

Permalink
Initial support for Global imports
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate committed Sep 29, 2023
1 parent 8edcf01 commit ff74af6
Show file tree
Hide file tree
Showing 5 changed files with 447 additions and 52 deletions.
Binary file added apps/tests/romfs/global.wasm
Binary file not shown.
32 changes: 32 additions & 0 deletions apps/tests/src/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ test('add.wasm', async () => {
assert.equal(add(39, 3), 42);
});

test('global.wasm', async () => {
const g = new WebAssembly.Global({ value: 'i32', mutable: true }, 6);
assert.equal(g.value, 6, 'getting initial value from JS');

const { module, instance } = await WebAssembly.instantiateStreaming(
fetch(new URL('global.wasm', Switch.entrypoint)),
{
js: { global: g },
}
);
assert.equal(WebAssembly.Module.imports(module), [
{ module: 'js', name: 'global', kind: 'global' },
]);
assert.equal(WebAssembly.Module.exports(module), [
{ name: 'getGlobal', kind: 'function' },
{ name: 'incGlobal', kind: 'function' },
]);
const getGlobal = instance.exports.getGlobal as Function;
assert.type(getGlobal, 'function');
const incGlobal = instance.exports.incGlobal as Function;
assert.type(incGlobal, 'function');

assert.equal(g.value, 6, 'getting initial value from JS after init');
assert.equal(getGlobal(), 6, 'getting initial value from wasm');

g.value = 42;
assert.equal(getGlobal(), 42, 'getting JS-updated value from wasm');

incGlobal();
assert.equal(g.value, 43, 'getting wasm-updated value from JS');
});

test('Imported function throws an Error is propagated', async () => {
const e = new Error('will be thrown');
const { instance } = await WebAssembly.instantiateStreaming(
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type FontFaceState = Opaque<'FontFaceState'>;
export type ImageOpaque = Opaque<'ImageOpaque'>;
export type WasmModuleOpaque = Opaque<'WasmModuleOpaque'>;
export type WasmInstanceOpaque = Opaque<'WasmInstanceOpaque'>;
export type WasmGlobalOpaque = Opaque<'WasmGlobalOpaque'>;

export interface Vibration {
duration: number;
Expand Down Expand Up @@ -311,8 +312,11 @@ export interface Native {
// wasm
wasmNewModule(b: ArrayBuffer): WasmModuleOpaque;
wasmNewInstance(b: WasmModuleOpaque, imports: any): WasmInstanceOpaque;
wasmNewGlobal(): WasmGlobalOpaque;
wasmModuleExports(m: WasmModuleOpaque): any[];
wasmModuleImports(m: WasmModuleOpaque): any[];
wasmGlobalGet(g: WasmGlobalOpaque): any;
wasmGlobalSet(g: WasmGlobalOpaque, v: any): void;
wasmCallFunc(
b: WasmInstanceOpaque,
name: string,
Expand Down
72 changes: 67 additions & 5 deletions packages/runtime/src/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
SwitchClass,
WasmModuleOpaque,
WasmInstanceOpaque,
WasmGlobalOpaque,
} from './switch';

declare const Switch: SwitchClass;
Expand Down Expand Up @@ -83,22 +84,82 @@ function toWasmError(e: unknown) {
return e;
}

interface GlobalInternals<T extends ValueType = ValueType> {
descriptor: GlobalDescriptor<T>;
value?: ValueTypeMap[T];
opaque?: WasmGlobalOpaque;
}

const globalInternalsMap = new WeakMap<Global, GlobalInternals<any>>();

/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Global) */
export class Global<T extends ValueType = ValueType>
implements WebAssembly.Global
{
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Global/value) */
value: ValueTypeMap[T];
constructor(descriptor: GlobalDescriptor<T>, value?: ValueTypeMap[T]) {
globalInternalsMap.set(this, { descriptor, value });
}

constructor(descriptor: GlobalDescriptor<T>, v?: ValueTypeMap[T]) {
throw new Error('Method not implemented.');
/**
* The value contained inside the global variable — this can be used to directly set and get the global's value.
*/
get value(): ValueTypeMap[T] {
const i = globalInternalsMap.get(this)!;
//console.log('get');
//console.log(i);
return i.opaque ? Switch.native.wasmGlobalGet(i.opaque) : i.value;
}

set value(v: ValueTypeMap[T]) {
const i = globalInternalsMap.get(this)!;
//console.log('set');
//console.log(i);
i.opaque ? Switch.native.wasmGlobalSet(i.opaque, v) : i.value;
}

/**
* Old-style method that returns the value contained inside the global variable.
*/
valueOf() {
return this.value;
}
}

function bindGlobal(g: Global, opaque = Switch.native.wasmNewGlobal()) {
const i = globalInternalsMap.get(g);
if (!i) throw new Error(`No internal state for Global`);
i.opaque = opaque;
return opaque;
}

function unwrapImports(importObject: Imports = {}) {
return Object.entries(importObject).flatMap(([m, i]) =>
Object.entries(i).map(([n, v]) => {
let val;
let i;
let kind: ImportExportKind;
if (typeof v === 'function') {
kind = 'function';
val = v;
} else if (v instanceof Global) {
kind = 'global';
i = v.value;
val = bindGlobal(v);
} else {
// TODO: Handle "memory" / "table" types
throw new Error(`Unsupported import type`);
}
return {
module: m,
name: n,
kind,
val,
i,
};
})
);
}

interface InstanceInternals {
module: Module;
opaque: WasmInstanceOpaque;
Expand All @@ -117,7 +178,7 @@ export class Instance implements WebAssembly.Instance {

const op = Switch.native.wasmNewInstance(
modInternal.opaque,
importObject
unwrapImports(importObject)
);

this.exports = {};
Expand Down Expand Up @@ -173,6 +234,7 @@ export class Module implements WebAssembly.Module {
constructor(bytes: BufferSource) {
const buffer = bufferSourceToArrayBuffer(bytes);
moduleInternalsMap.set(this, {
// Hold a reference to the bytes to prevent garbage collection
buffer,
opaque: Switch.native.wasmNewModule(buffer),
});
Expand Down
Loading

0 comments on commit ff74af6

Please sign in to comment.