diff --git a/apps/wasm/romfs/add.wasm b/apps/wasm/romfs/add.wasm new file mode 100644 index 00000000..357f72da Binary files /dev/null and b/apps/wasm/romfs/add.wasm differ diff --git a/apps/wasm/src/main.ts b/apps/wasm/src/main.ts index a4d6dbe5..1907f491 100644 --- a/apps/wasm/src/main.ts +++ b/apps/wasm/src/main.ts @@ -2,12 +2,20 @@ // which invokes the imported function `imported_func()` with // a single parameter containing the value 42. // https://github.com/mdn/webassembly-examples/blob/main/js-api-examples/simple.wat -const wasm = Switch.readFileSync(new URL('simple.wasm', Switch.entrypoint)); -//const wasm = require('fs').readFileSync(__dirname + '/../romfs/simple.wasm'); +const wasm = Switch.readFileSync(new URL('add.wasm', Switch.entrypoint)); +//const wasm = require('fs').readFileSync(__dirname + '/../romfs/add.wasm'); const mod = new WebAssembly.Module(wasm); console.log(WebAssembly.Module.exports(mod)); console.log(WebAssembly.Module.imports(mod)); + +WebAssembly.instantiate(mod).then((instance) => { + console.log(Object.keys(instance.exports)); + if (typeof instance.exports.add === 'function') { + console.log(instance.exports.add(6, 9)); + } +}); + //const importObject = { // imports: { // imported_func(arg /* @type any */) { diff --git a/packages/runtime/src/switch.ts b/packages/runtime/src/switch.ts index c8e0581f..9f08f145 100644 --- a/packages/runtime/src/switch.ts +++ b/packages/runtime/src/switch.ts @@ -11,6 +11,7 @@ export type CanvasRenderingContext2DState = export type FontFaceState = Opaque<'FontFaceState'>; export type ImageOpaque = Opaque<'ImageOpaque'>; export type WasmModuleOpaque = Opaque<'WasmModuleOpaque'>; +export type WasmInstanceOpaque = Opaque<'WasmInstanceOpaque'>; export interface Vibration { duration: number; @@ -309,8 +310,14 @@ export interface Native { // wasm wasmNewModule(b: ArrayBuffer): WasmModuleOpaque; + wasmNewInstance(b: WasmModuleOpaque, imports: any): WasmInstanceOpaque; wasmModuleExports(m: WasmModuleOpaque): any[]; wasmModuleImports(m: WasmModuleOpaque): any[]; + wasmCallFunc( + b: WasmInstanceOpaque, + name: string, + ...args: unknown[] + ): unknown; } interface Internal { diff --git a/packages/runtime/src/wasm.ts b/packages/runtime/src/wasm.ts index c8e37d09..a793c889 100644 --- a/packages/runtime/src/wasm.ts +++ b/packages/runtime/src/wasm.ts @@ -1,5 +1,9 @@ -import type { SwitchClass, WasmModuleOpaque } from './switch'; import { bufferSourceToArrayBuffer } from './utils'; +import type { + SwitchClass, + WasmModuleOpaque, + WasmInstanceOpaque, +} from './switch'; declare const Switch: SwitchClass; @@ -81,14 +85,47 @@ export class Global } } +interface InstanceInternals { + module: Module; + opaque: WasmInstanceOpaque; +} + +const instanceInternalsMap = new WeakMap(); + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Instance) */ export class Instance implements WebAssembly.Instance { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Instance/exports) */ readonly exports: Exports; - constructor(module: Module, importObject?: Imports) { + constructor(moduleObject: Module, importObject?: Imports) { + const modInternal = moduleInternalsMap.get(moduleObject); + if (!modInternal) throw new Error(`No internal state for Module`); + + const op = Switch.native.wasmNewInstance( + modInternal.opaque, + importObject + ); + this.exports = {}; + for (const e of Module.exports(moduleObject)) { + if (e.kind === 'function') { + this.exports[e.name] = callFunc.bind(null, op, e.name); + } + } + Object.freeze(this.exports); + + instanceInternalsMap.set(this, { module: moduleObject, opaque: op }); } } +function callFunc( + op: WasmInstanceOpaque, + name: string, + ...args: unknown[] +): unknown { + return Switch.native.wasmCallFunc(op, name, ...args); +} + /** * [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory) */ diff --git a/source/wasm.c b/source/wasm.c index 351aea20..a0f08c2d 100644 --- a/source/wasm.c +++ b/source/wasm.c @@ -23,9 +23,57 @@ static JSClassID nx_wasm_module_class_id; typedef struct { IM3Module module; - bool loaded; + uint8_t *data; + size_t size; } nx_wasm_module_t; +static nx_wasm_module_t *nx_wasm_module_get(JSContext *ctx, JSValueConst obj) +{ + return JS_GetOpaque2(ctx, obj, nx_wasm_module_class_id); +} + +static void finalizer_wasm_module(JSRuntime *rt, JSValue val) +{ + nx_wasm_module_t *m = JS_GetOpaque(val, nx_wasm_module_class_id); + if (m) + { + if (m->module) + m3_FreeModule(m->module); + js_free_rt(rt, m); + } +} + +static JSClassID nx_wasm_instance_class_id; + +typedef struct +{ + IM3Runtime runtime; + IM3Module module; + bool loaded; +} nx_wasm_instance_t; + +static nx_wasm_instance_t *nx_wasm_instance_get(JSContext *ctx, JSValueConst obj) +{ + return JS_GetOpaque2(ctx, obj, nx_wasm_instance_class_id); +} + +static void finalizer_wasm_instance(JSRuntime *rt, JSValue val) +{ + nx_wasm_instance_t *i = JS_GetOpaque(val, nx_wasm_instance_class_id); + if (i) + { + if (i->module) + { + // Free the module, only if it wasn't previously loaded. + if (!i->loaded) + m3_FreeModule(i->module); + } + if (i->runtime) + m3_FreeRuntime(i->runtime); + js_free_rt(rt, i); + } +} + static JSValue nx_wasm_new_module(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); @@ -37,6 +85,8 @@ static JSValue nx_wasm_new_module(JSContext *ctx, JSValueConst this_val, int arg JSValue obj = JS_NewObjectClass(ctx, nx_wasm_module_class_id); nx_wasm_module_t *m = js_mallocz(ctx, sizeof(nx_wasm_module_t)); + // TODO: OOM error handling + JS_SetOpaque(obj, m); size_t size; @@ -50,34 +100,52 @@ static JSValue nx_wasm_new_module(JSContext *ctx, JSValueConst this_val, int arg return nx_throw_wasm_error(ctx, "CompileError", r); } + m->data = buf; + m->size = size; + return obj; } -static void finalizer_wasm_module(JSRuntime *rt, JSValue val) +static JSValue nx_wasm_new_instance(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - nx_wasm_module_t *m = JS_GetOpaque(val, nx_wasm_module_class_id); - if (m) + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); + + JSValue obj = JS_NewObjectClass(ctx, nx_wasm_instance_class_id); + nx_wasm_instance_t *i = js_mallocz(ctx, sizeof(nx_wasm_instance_t)); + // TODO: OOM error handling + + JS_SetOpaque(obj, i); + + nx_wasm_module_t *m = nx_wasm_module_get(ctx, argv[0]); + + M3Result r = m3_ParseModule(nx_ctx->wasm_env, &i->module, m->data, m->size); + // CHECK_NULL(r); // Should never fail because we already parsed it. TODO: clone it? + + /* Create a runtime per module to avoid symbol clash. */ + i->runtime = m3_NewRuntime(nx_ctx->wasm_env, /* TODO: adjust */ 512 * 1024, NULL); + if (!i->runtime) { - if (!m->loaded) - { - // printf("freeing module\n"); - m3_FreeModule(m->module); - } - js_free_rt(rt, m); + JS_FreeValue(ctx, obj); + return JS_ThrowOutOfMemory(ctx); } -} -// static JSClassID nx_wasm_instance_class_id; -// -// typedef struct { -// IM3Runtime runtime; -// IM3Module module; -// bool loaded; -// } nx_wasm_instance_t; + // TODO: add imports + + r = m3_LoadModule(i->runtime, i->module); + if (r) + { + JS_FreeValue(ctx, obj); + return nx_throw_wasm_error(ctx, "LinkError", r); + } + + i->loaded = true; + + return obj; +} static JSValue nx_wasm_module_exports(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - nx_wasm_module_t *m = JS_GetOpaque2(ctx, argv[0], nx_wasm_module_class_id); + nx_wasm_module_t *m = nx_wasm_module_get(ctx, argv[0]); if (!m) return JS_EXCEPTION; @@ -105,7 +173,7 @@ static JSValue nx_wasm_module_exports(JSContext *ctx, JSValueConst this_val, int static JSValue nx_wasm_module_imports(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - nx_wasm_module_t *m = JS_GetOpaque2(ctx, argv[0], nx_wasm_module_class_id); + nx_wasm_module_t *m = nx_wasm_module_get(ctx, argv[0]); if (!m) return JS_EXCEPTION; @@ -132,16 +200,131 @@ static JSValue nx_wasm_module_imports(JSContext *ctx, JSValueConst this_val, int return imports; } +static JSValue nx__wasm_result(JSContext *ctx, M3ValueType type, const void *stack) +{ + switch (type) + { + case c_m3Type_i32: + { + int32_t val = *(int32_t *)stack; + return JS_NewInt32(ctx, val); + } + case c_m3Type_i64: + { + int64_t val = *(int64_t *)stack; + if (val == (int32_t)val) + return JS_NewInt32(ctx, (int32_t)val); + else + return JS_NewBigInt64(ctx, val); + } + case c_m3Type_f32: + { + float val = *(float *)stack; + return JS_NewFloat64(ctx, (double)val); + } + case c_m3Type_f64: + { + double val = *(double *)stack; + return JS_NewFloat64(ctx, val); + } + default: + return JS_UNDEFINED; + } +} + +static JSValue nx_wasm_call_func(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + nx_wasm_instance_t *i = nx_wasm_instance_get(ctx, argv[0]); + if (!i) + return JS_EXCEPTION; + + const char *fname = JS_ToCString(ctx, argv[1]); + if (!fname) + return JS_EXCEPTION; + + IM3Function func; + M3Result r = m3_FindFunction(&func, i->runtime, fname); + if (r) + { + JS_FreeCString(ctx, fname); + return nx_throw_wasm_error(ctx, "RuntimeError", r); + } + + JS_FreeCString(ctx, fname); + + int nargs = argc - 2; + if (nargs == 0) + { + r = m3_Call(func, 0, NULL); + } + else + { + const char *m3_argv[nargs + 1]; + for (int i = 0; i < nargs; i++) + { + m3_argv[i] = JS_ToCString(ctx, argv[i + 2]); + } + m3_argv[nargs] = NULL; + r = m3_CallArgv(func, nargs, m3_argv); + for (int i = 0; i < nargs; i++) + { + JS_FreeCString(ctx, m3_argv[i]); + } + } + + if (r) + return nx_throw_wasm_error(ctx, "RuntimeError", r); + + // https://webassembly.org/docs/js/ See "ToJSValue" + // NOTE: here we support returning BigInt, because we can. + + int ret_count = m3_GetRetCount(func); + + if (ret_count == 0) + { + return JS_UNDEFINED; + } + + uint64_t valbuff[ret_count]; + const void *valptrs[ret_count]; + memset(valbuff, 0, sizeof(valbuff)); + for (int i = 0; i < ret_count; i++) + { + valptrs[i] = &valbuff[i]; + } + + r = m3_GetResults(func, ret_count, valptrs); + if (r) + return nx_throw_wasm_error(ctx, "RuntimeError", r); + + if (ret_count == 1) + { + return nx__wasm_result(ctx, m3_GetRetType(func, 0), valptrs[0]); + } + else + { + JSValue rets = JS_NewArray(ctx); + for (int i = 0; i < ret_count; i++) + { + JS_SetPropertyUint32(ctx, rets, i, nx__wasm_result(ctx, m3_GetRetType(func, i), valptrs[i])); + } + return rets; + } +} + static const JSCFunctionListEntry function_list[] = { JS_CFUNC_DEF("wasmNewModule", 1, nx_wasm_new_module), + JS_CFUNC_DEF("wasmNewInstance", 1, nx_wasm_new_instance), JS_CFUNC_DEF("wasmModuleExports", 1, nx_wasm_module_exports), JS_CFUNC_DEF("wasmModuleImports", 1, nx_wasm_module_imports), + JS_CFUNC_DEF("wasmCallFunc", 1, nx_wasm_call_func), }; void nx_init_wasm(JSContext *ctx, JSValueConst native_obj) { JSRuntime *rt = JS_GetRuntime(ctx); + /* WebAssembly.Module */ JS_NewClassID(&nx_wasm_module_class_id); JSClassDef nx_wasm_module_class = { "WebAssembly.Module", @@ -149,5 +332,13 @@ void nx_init_wasm(JSContext *ctx, JSValueConst native_obj) }; JS_NewClass(rt, nx_wasm_module_class_id, &nx_wasm_module_class); + /* WebAssembly.Instance */ + JS_NewClassID(&nx_wasm_instance_class_id); + JSClassDef nx_wasm_instance_class = { + "WebAssembly.Instance", + .finalizer = finalizer_wasm_instance, + }; + JS_NewClass(rt, nx_wasm_instance_class_id, &nx_wasm_instance_class); + JS_SetPropertyFunctionList(ctx, native_obj, function_list, countof(function_list)); }