From 3e79c2172d6429736b71fbcd29dbcd614755eef5 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Sat, 23 Sep 2023 13:02:41 -0300 Subject: [PATCH] WASM progress point --- .vscode/settings.json | 3 +- apps/wasm/src/main.ts | 40 ++++++--- packages/runtime/src/switch.ts | 6 ++ packages/runtime/src/wasm.ts | 94 ++++++++++++++------ source/main.c | 3 +- source/wasm.c | 151 ++++++++++++++++++++++++--------- source/wasm.h | 4 +- 7 files changed, 218 insertions(+), 83 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c3f36e72..be9d5d86 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,8 @@ "fs.h": "c", "canvas.h": "c", "font.h": "c", - "image.h": "c" + "image.h": "c", + "wasm3.h": "c" }, "C_Cpp.default.compilerPath": "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc" } \ No newline at end of file diff --git a/apps/wasm/src/main.ts b/apps/wasm/src/main.ts index 41cfbc71..a4d6dbe5 100644 --- a/apps/wasm/src/main.ts +++ b/apps/wasm/src/main.ts @@ -1,12 +1,30 @@ -const importObject = { - imports: { - imported_func(arg: number) { - console.log(arg); - }, - }, -}; - +// `simple.wasm` exports a function named `exported_func()`, +// 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)); -WebAssembly.instantiate(wasm, importObject).then((results) => { - results.instance.exports.exported_func(); -}); +//const wasm = require('fs').readFileSync(__dirname + '/../romfs/simple.wasm'); + +const mod = new WebAssembly.Module(wasm); +console.log(WebAssembly.Module.exports(mod)); +console.log(WebAssembly.Module.imports(mod)); +//const importObject = { +// imports: { +// imported_func(arg /* @type any */) { +// console.log({ arg }); +// }, +// }, +//}; +// +//WebAssembly.instantiate(wasm, importObject) +// .then((results) => { +// console.log(WebAssembly.Module.exports(results.module)); +// console.log(WebAssembly.Module.imports(results.module)); +// //console.log(results.instance.exports.add); +// //console.log(results.instance.exports.add(1, 5)); +// if (typeof results.instance.exports.exported_func === 'function') { +// console.log(results.instance.exports.exported_func()); +// } else { +// throw new Error(`"exported_func" was not exported from WASM file`); +// } +// }); diff --git a/packages/runtime/src/switch.ts b/packages/runtime/src/switch.ts index faa7aec9..c8e0581f 100644 --- a/packages/runtime/src/switch.ts +++ b/packages/runtime/src/switch.ts @@ -10,6 +10,7 @@ export type CanvasRenderingContext2DState = Opaque<'CanvasRenderingContext2DState'>; export type FontFaceState = Opaque<'FontFaceState'>; export type ImageOpaque = Opaque<'ImageOpaque'>; +export type WasmModuleOpaque = Opaque<'WasmModuleOpaque'>; export interface Vibration { duration: number; @@ -305,6 +306,11 @@ export interface Native { connect(cb: Callback, ip: string, port: number): void; write(cb: Callback, fd: number, data: ArrayBuffer): void; read(cb: Callback, fd: number, buffer: ArrayBuffer): void; + + // wasm + wasmNewModule(b: ArrayBuffer): WasmModuleOpaque; + wasmModuleExports(m: WasmModuleOpaque): any[]; + wasmModuleImports(m: WasmModuleOpaque): any[]; } interface Internal { diff --git a/packages/runtime/src/wasm.ts b/packages/runtime/src/wasm.ts index 3de36462..c8e37d09 100644 --- a/packages/runtime/src/wasm.ts +++ b/packages/runtime/src/wasm.ts @@ -1,4 +1,5 @@ -import type { SwitchClass } from './switch'; +import type { SwitchClass, WasmModuleOpaque } from './switch'; +import { bufferSourceToArrayBuffer } from './utils'; declare const Switch: SwitchClass; @@ -54,27 +55,36 @@ export type Imports = Record; export type ModuleImports = Record; export type ValueType = keyof ValueTypeMap; -export class CompileError extends Error implements WebAssembly.CompileError {} -export class RuntimeError extends Error implements WebAssembly.RuntimeError {} -export class LinkError extends Error implements WebAssembly.LinkError {} +export class CompileError extends Error implements WebAssembly.CompileError { + name = 'CompileError'; +} +export class RuntimeError extends Error implements WebAssembly.RuntimeError { + name = 'RuntimeError'; +} +export class LinkError extends Error implements WebAssembly.LinkError { + name = 'LinkError'; +} +/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Global) */ export class Global implements WebAssembly.Global { - value: any; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Global/value) */ + value: ValueTypeMap[T]; constructor(descriptor: GlobalDescriptor, v?: ValueTypeMap[T]) { throw new Error('Method not implemented.'); } valueOf() { - throw new Error('Method not implemented.'); + return this.value; } } export class Instance implements WebAssembly.Instance { - exports: Exports; - constructor() { + readonly exports: Exports; + + constructor(module: Module, importObject?: Imports) { this.exports = {}; } } @@ -96,9 +106,21 @@ export class Memory implements WebAssembly.Memory { } } +interface ModuleInternals { + buffer: ArrayBuffer; + opaque: WasmModuleOpaque; +} + +const moduleInternalsMap = new WeakMap(); + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module) */ export class Module implements WebAssembly.Module { constructor(bytes: BufferSource) { - throw new Error('Method not implemented.'); + const buffer = bufferSourceToArrayBuffer(bytes); + moduleInternalsMap.set(this, { + buffer, + opaque: Switch.native.wasmNewModule(buffer), + }); } /** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module/customSections) */ @@ -111,12 +133,16 @@ export class Module implements WebAssembly.Module { /** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module/exports) */ static exports(moduleObject: Module): ModuleExportDescriptor[] { - throw new Error('Method not implemented.'); + const i = moduleInternalsMap.get(moduleObject); + if (!i) throw new Error(`No internal state for Module`); + return Switch.native.wasmModuleExports(i.opaque); } /** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module/imports) */ static imports(moduleObject: Module): ModuleImportDescriptor[] { - throw new Error('Method not implemented.'); + const i = moduleInternalsMap.get(moduleObject); + if (!i) throw new Error(`No internal state for Module`); + return Switch.native.wasmModuleImports(i.opaque); } } @@ -149,14 +175,19 @@ export class Table implements WebAssembly.Table { * [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/compile) */ export async function compile(bytes: BufferSource): Promise { - const buffer = - bytes instanceof ArrayBuffer - ? bytes - : bytes.buffer.slice( - bytes.byteOffset, - bytes.byteOffset + bytes.byteLength - ); - return new Module(buffer); + return new Module(bytes); +} + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/compileStreaming) */ +export async function compileStreaming( + source: Response | PromiseLike +): Promise { + const res = await source; + if (!res.ok) { + // TODO: throw error? + } + const buf = await res.arrayBuffer(); + return compile(buf); } /** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate) */ @@ -173,11 +204,24 @@ export async function instantiate( importObject?: Imports ) { if (bytes instanceof Module) { - return new Instance(); + return new Instance(bytes, importObject); } - const module = await compile(bytes); - return { - instance: new Instance(), - module, - }; + const m = await compile(bytes); + const instance = new Instance(m, importObject); + return { module: m, instance }; +} + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming) */ +export async function instantiateStreaming( + source: Response | PromiseLike, + importObject?: Imports +): Promise { + const m = await compileStreaming(source); + const instance = await instantiate(m, importObject); + return { module: m, instance }; +} + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/validate) */ +export function validate(bytes: BufferSource): boolean { + throw new Error('Method not implemented.'); } diff --git a/source/main.c b/source/main.c index a99e69d1..77cb75c6 100644 --- a/source/main.c +++ b/source/main.c @@ -491,6 +491,7 @@ int main(int argc, char *argv[]) nx_init_fs(ctx, native_obj); nx_init_image(ctx, native_obj); nx_init_tcp(ctx, native_obj); + nx_init_wasm(ctx, native_obj); JSValue exit_func = JS_NewCFunction(ctx, js_exit, "exit", 0); JS_SetPropertyStr(ctx, switch_obj, "exit", exit_func); @@ -527,8 +528,6 @@ int main(int argc, char *argv[]) JS_CFUNC_DEF("resolveDns", 0, js_dns_resolve)}; JS_SetPropertyFunctionList(ctx, native_obj, function_list, countof(function_list)); - js_wasm_init(ctx, native_obj); - // `Switch.entrypoint` JS_SetPropertyStr(ctx, switch_obj, "entrypoint", JS_NewString(ctx, js_path)); diff --git a/source/wasm.c b/source/wasm.c index a76d9de9..351aea20 100644 --- a/source/wasm.c +++ b/source/wasm.c @@ -1,86 +1,153 @@ +/** + * Modified from `txiki.js` by Saúl Ibarra Corretgé + * - https://github.com/saghul/txiki.js/blob/master/src/wasm.c + * - https://github.com/saghul/txiki.js/blob/master/src/js/polyfills/wasm.js + */ #include "types.h" #include "wasm.h" +#include -static JSClassID js_wasm_runtime_id; -static JSClassID js_wasm_module_id; +JSValue nx_throw_wasm_error(JSContext *ctx, const char *name, M3Result r) +{ + // CHECK_NOT_NULL(r); + JSValue obj = JS_NewError(ctx); + JS_DefinePropertyValueStr(ctx, obj, "message", JS_NewString(ctx, r), JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "wasmError", JS_NewString(ctx, name), JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + if (JS_IsException(obj)) + obj = JS_NULL; + return JS_Throw(ctx, obj); +} -typedef struct { +static JSClassID nx_wasm_module_class_id; + +typedef struct +{ IM3Module module; bool loaded; } nx_wasm_module_t; -static JSValue js_wasm_new_memory(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue nx_wasm_new_module(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); - if (nx_ctx->wasm_env == NULL) { + if (nx_ctx->wasm_env == NULL) + { nx_ctx->wasm_env = m3_NewEnvironment(); } - JSValue obj = JS_NewObjectClass(ctx, js_wasm_runtime_id); + JSValue obj = JS_NewObjectClass(ctx, nx_wasm_module_class_id); + nx_wasm_module_t *m = js_mallocz(ctx, sizeof(nx_wasm_module_t)); + JS_SetOpaque(obj, m); - IM3Runtime runtime = m3_NewRuntime(nx_ctx->wasm_env, 64 * 1024, NULL); - if (!runtime) { - JS_FreeValue(ctx, obj); + size_t size; + uint8_t *buf = JS_GetArrayBuffer(ctx, &size, argv[0]); + // TODO: error handling + M3Result r = m3_ParseModule(nx_ctx->wasm_env, &m->module, buf, size); + if (r) + { + JS_FreeValue(ctx, obj); + return nx_throw_wasm_error(ctx, "CompileError", r); } return obj; } -static JSValue js_wasm_new_module(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); - - if (nx_ctx->wasm_env == NULL) { - nx_ctx->wasm_env = m3_NewEnvironment(); +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->loaded) + { + // printf("freeing module\n"); + m3_FreeModule(m->module); + } + js_free_rt(rt, m); } - - JSValue obj = JS_NewObjectClass(ctx, js_wasm_module_id); - return obj; } -static void finalizer_wasm_runtime(JSRuntime *rt, JSValue val) +// static JSClassID nx_wasm_instance_class_id; +// +// typedef struct { +// IM3Runtime runtime; +// IM3Module module; +// bool loaded; +// } nx_wasm_instance_t; + +static JSValue nx_wasm_module_exports(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - IM3Runtime runtime = JS_GetOpaque(val, js_wasm_runtime_id); - if (runtime) + nx_wasm_module_t *m = JS_GetOpaque2(ctx, argv[0], nx_wasm_module_class_id); + if (!m) + return JS_EXCEPTION; + + JSValue exports = JS_NewArray(ctx); + if (JS_IsException(exports)) + return exports; + + for (size_t i = 0, j = 0; i < m->module->numFunctions; ++i) { - m3_FreeRuntime(runtime); + IM3Function f = &m->module->functions[i]; + if (f->export_name) + { + JSValue item = JS_NewObject(ctx); + JS_DefinePropertyValueStr(ctx, item, "kind", JS_NewString(ctx, "function"), JS_PROP_C_W_E); + JS_DefinePropertyValueStr(ctx, item, "name", JS_NewString(ctx, f->export_name), JS_PROP_C_W_E); + JS_DefinePropertyValueUint32(ctx, exports, j, item, JS_PROP_C_W_E); + j++; + } } + + // TODO: other export types. + + return exports; } -static void finalizer_wasm_module(JSRuntime *rt, JSValue val) +static JSValue nx_wasm_module_imports(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - nx_wasm_module_t *module = JS_GetOpaque(val, js_wasm_module_id); - if (module) + nx_wasm_module_t *m = JS_GetOpaque2(ctx, argv[0], nx_wasm_module_class_id); + if (!m) + return JS_EXCEPTION; + + JSValue imports = JS_NewArray(ctx); + if (JS_IsException(imports)) + return imports; + + for (size_t i = 0, j = 0; i < m->module->numFunctions; ++i) { - if (!module->loaded) { - printf("freeing module\n"); - m3_FreeModule(module->module); + IM3Function f = &m->module->functions[i]; + if (f->import.moduleUtf8 && f->import.fieldUtf8) + { + JSValue item = JS_NewObject(ctx); + JS_DefinePropertyValueStr(ctx, item, "kind", JS_NewString(ctx, "function"), JS_PROP_C_W_E); + JS_DefinePropertyValueStr(ctx, item, "module", JS_NewString(ctx, f->import.moduleUtf8), JS_PROP_C_W_E); + JS_DefinePropertyValueStr(ctx, item, "name", JS_NewString(ctx, f->import.fieldUtf8), JS_PROP_C_W_E); + JS_DefinePropertyValueUint32(ctx, imports, j, item, JS_PROP_C_W_E); + j++; } - js_free_rt(rt, module); } + + // TODO: other import types. + + return imports; } static const JSCFunctionListEntry function_list[] = { - JS_CFUNC_DEF("wasmNewModule", 1, js_wasm_new_module), - JS_CFUNC_DEF("wasmNewMemory", 1, js_wasm_new_memory), + JS_CFUNC_DEF("wasmNewModule", 1, nx_wasm_new_module), + JS_CFUNC_DEF("wasmModuleExports", 1, nx_wasm_module_exports), + JS_CFUNC_DEF("wasmModuleImports", 1, nx_wasm_module_imports), }; -void js_wasm_init(JSContext *ctx, JSValueConst native_obj) { +void nx_init_wasm(JSContext *ctx, JSValueConst native_obj) +{ JSRuntime *rt = JS_GetRuntime(ctx); - JS_NewClassID(&js_wasm_runtime_id); - JSClassDef js_wasm_runtime_class = { - "WebAssembly.Memory", - .finalizer = finalizer_wasm_runtime, - }; - JS_NewClass(rt, js_wasm_runtime_id, &js_wasm_runtime_class); - - JS_NewClassID(&js_wasm_module_id); - JSClassDef js_wasm_module_class = { + JS_NewClassID(&nx_wasm_module_class_id); + JSClassDef nx_wasm_module_class = { "WebAssembly.Module", .finalizer = finalizer_wasm_module, }; - JS_NewClass(rt, js_wasm_module_id, &js_wasm_module_class); + JS_NewClass(rt, nx_wasm_module_class_id, &nx_wasm_module_class); - JS_SetPropertyFunctionList(ctx, native_obj, function_list, sizeof(function_list) / sizeof(function_list[0])); + JS_SetPropertyFunctionList(ctx, native_obj, function_list, countof(function_list)); } diff --git a/source/wasm.h b/source/wasm.h index 7e72b691..4f4e79e4 100644 --- a/source/wasm.h +++ b/source/wasm.h @@ -1,4 +1,4 @@ #pragma once -#include +#include "types.h" -void js_wasm_init(JSContext *ctx, JSValueConst native_obj); +void nx_init_wasm(JSContext *ctx, JSValueConst native_obj);