diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5149590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +build/ +generated/ +mquickjs_build_native +mquickjs_atom.h +mqjs_stdlib.h +*.o +*.d +*.wasm +*.host.o +/wasi_snapshot_preview1.reactor.wasm +/output.txt +mqjs +example +example_stdlib +mqjs_stdlib +mquickjs_build +mquickjs_build_atoms +test_builtin.bin +dtoa_test +libm_test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c4a10d2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "packages/wasi-sdk"] + path = packages/wasi-sdk + url = https://github.com/WebAssembly/wasi-sdk +[submodule "packages/wasmedge"] + path = packages/wasmedge + url = https://github.com/WasmEdge/WasmEdge diff --git a/Makefile.wasi b/Makefile.wasi new file mode 100644 index 0000000..46e52a8 --- /dev/null +++ b/Makefile.wasi @@ -0,0 +1,76 @@ +WASI_SDK_PATH ?= /opt/wasi-sdk +CC = $(WASI_SDK_PATH)/bin/clang +ADAPTER ?= wasi_snapshot_preview1.reactor.wasm + +# -mexec-model is only valid for linking, not compilation +CFLAGS = -Oz -D_WASI_EMULATED_SIGNAL -D_WASI_EMULATED_PROCESS_CLOCKS \ + -Werror=implicit-function-declaration -fno-math-errno -fno-trapping-math \ + --target=wasm32-wasi -mllvm -wasm-enable-sjlj + +LDFLAGS = --target=wasm32-wasi -mexec-model=reactor \ + -Wl,--no-entry -Wl,--export=cabi_realloc -Wl,--export=__wasm_call_ctors \ + -lwasi-emulated-signal -lwasi-emulated-process-clocks -lsetjmp -lm + +BUILD_DIR = build +GEN_DIR = generated + +OBJS = $(BUILD_DIR)/mquickjs.o $(BUILD_DIR)/cutils.o $(BUILD_DIR)/dtoa.o $(BUILD_DIR)/libm.o \ + $(BUILD_DIR)/microquickjs.o $(BUILD_DIR)/glue.o $(BUILD_DIR)/wasi_preview1_stubs.o + +all: $(BUILD_DIR)/microquickjs.component.wasm + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +# Native build tool to generate stdlib headers +mquickjs_build_native: mquickjs_build.c mqjs_stdlib.c cutils.c + gcc -O2 -Wall -I. $^ -o $@ -lm + +$(BUILD_DIR)/mquickjs_atom.h: mquickjs_build_native | $(BUILD_DIR) + ./mquickjs_build_native -a -m32 > $@ + +$(BUILD_DIR)/mqjs_stdlib.h: mquickjs_build_native | $(BUILD_DIR) + ./mquickjs_build_native -m32 > $@ + +headers: $(BUILD_DIR)/mquickjs_atom.h $(BUILD_DIR)/mqjs_stdlib.h + +# wit-bindgen code generation +$(GEN_DIR)/microquickjs.c $(GEN_DIR)/microquickjs.h: microquickjs.wit + mkdir -p $(GEN_DIR) + wit-bindgen c microquickjs.wit --out-dir $(GEN_DIR) --world microquickjs + +# Wasm object files +$(BUILD_DIR)/mquickjs.o: mquickjs.c $(BUILD_DIR)/mquickjs_atom.h $(BUILD_DIR)/mqjs_stdlib.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -I. -I$(BUILD_DIR) -c $< -o $@ + +$(BUILD_DIR)/cutils.o: cutils.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -I. -c $< -o $@ + +$(BUILD_DIR)/dtoa.o: dtoa.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -I. -c $< -o $@ + +$(BUILD_DIR)/libm.o: libm.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -I. -c $< -o $@ + +$(BUILD_DIR)/microquickjs.o: $(GEN_DIR)/microquickjs.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -I$(GEN_DIR) -c $< -o $@ + +$(BUILD_DIR)/glue.o: glue.c $(GEN_DIR)/microquickjs.h $(BUILD_DIR)/mqjs_stdlib.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -I. -I$(GEN_DIR) -I$(BUILD_DIR) -c $< -o $@ + +$(BUILD_DIR)/wasi_preview1_stubs.o: wasi_preview1_stubs.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +# Core Wasm module +$(BUILD_DIR)/core.wasm: $(OBJS) $(GEN_DIR)/microquickjs.c + $(CC) $(OBJS) $(GEN_DIR)/microquickjs_component_type.o $(LDFLAGS) -o $@ + +# Final Component +$(BUILD_DIR)/microquickjs.component.wasm: $(BUILD_DIR)/core.wasm microquickjs.wit $(ADAPTER) + wasm-tools component embed ./microquickjs.wit $< --world microquickjs --output $(BUILD_DIR)/embedded.wasm + wasm-tools component new $(BUILD_DIR)/embedded.wasm --adapt wasi_snapshot_preview1=$(ADAPTER) --output $@ + +clean: + rm -rf $(BUILD_DIR) $(GEN_DIR) mquickjs_build_native + +.PHONY: all headers clean diff --git a/README.WASI.md b/README.WASI.md new file mode 100644 index 0000000..3e83612 --- /dev/null +++ b/README.WASI.md @@ -0,0 +1,69 @@ +# MicroQuickJS WASI 0.2 Component + +This project ports MicroQuickJS to a WASI 0.2 WebAssembly Component. + +## Exported Interface + +The component exports the `engine` interface via the following WIT definition: + +```wit +package local:microquickjs; + +interface engine { + resource js-value { + is-int: func() -> bool; + is-bool: func() -> bool; + is-null: func() -> bool; + is-undefined: func() -> bool; + is-exception: func() -> bool; + is-number: func() -> bool; + is-string: func() -> bool; + is-error: func() -> bool; + is-function: func() -> bool; + + to-string: func() -> string; + to-int32: func() -> s32; + to-float64: func() -> f64; + + get-property: func(name: string) -> js-value; + set-property: func(name: string, val: borrow); + + call: func(args: list>) -> js-value; + } + + new-int32: func(val: s32) -> js-value; + new-float64: func(val: f64) -> js-value; + new-bool: func(val: bool) -> js-value; + new-string: func(val: string) -> js-value; + new-object: func() -> js-value; + new-array: func() -> js-value; + + get-global-object: func() -> js-value; + + eval: func(code: string) -> result; +} + +world microquickjs { + export engine; +} +``` + +## Building + +Prerequisites: +- **WASI SDK**: [https://github.com/WebAssembly/wasi-sdk](https://github.com/WebAssembly/wasi-sdk) (Set `WASI_SDK_PATH` to your installation directory). +- **Tooling**: `wit-bindgen`, `wasm-tools`, `gcc` (for native helper). +- **Adapter**: `wasi_snapshot_preview1.reactor.wasm` should be present in the root directory. + +Run the build: +```bash +make -f Makefile.wasi +``` + +The final component will be at `build/microquickjs.component.wasm`. + +## Architecture & Implementation + +- **Arena Memory**: Uses a fixed 4MiB static arena for JS heap, ensuring predictable memory footprint. +- **Error Handling**: Uses the WebAssembly Exception Handling proposal for robust `setjmp/longjmp` support. +- **Memory Safety**: Uses the Component Model Canonical ABI with `cabi_realloc` for all host-returned strings. diff --git a/dtoa.c b/dtoa.c index 604f3f0..e2aefb9 100644 --- a/dtoa.c +++ b/dtoa.c @@ -35,7 +35,7 @@ #include "cutils.h" #include "dtoa.h" -/* +/* TODO: - test n_digits=101 instead of 100 - simplify subnormal handling diff --git a/glue.c b/glue.c new file mode 100644 index 0000000..4dd4842 --- /dev/null +++ b/glue.c @@ -0,0 +1,236 @@ +#include +#include +#include +#include + +#include "mquickjs.h" + +// cabi_realloc is provided by wit-bindgen's generated microquickjs.c. +void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size); + +// WASI shim stubs +JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_clearTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } + +// wit-bindgen generated header +#include "generated/microquickjs.h" +// stdlib descriptor generated by native build tool +#include "build/mqjs_stdlib.h" + +// Singleton context +static uint8_t s_mem[4 * 1024 * 1024]; // 4 MiB arena — static, fixed footprint +static JSContext *s_ctx = NULL; + +static void ensure_context(void) { + if (s_ctx) return; + s_ctx = JS_NewContext(s_mem, sizeof(s_mem), &js_stdlib); +} + +/// Helper: Copy a string into WASI-managed memory for return to host. +static char *make_wasi_string(const char *src, size_t len) { + if (!src) return NULL; + char *out = (char *)cabi_realloc(NULL, 0, 1, len); + if (!out) return NULL; + memcpy(out, src, len); + return out; +} + +// Resource: js-value +struct exports_local_microquickjs_engine_js_value_t { + JSValue val; + JSGCRef root; +}; + +static exports_local_microquickjs_engine_own_js_value_t make_own_value(JSValue val) { + exports_local_microquickjs_engine_js_value_t *rep = malloc(sizeof(*rep)); + rep->val = val; + JS_AddGCRef(s_ctx, &rep->root); + rep->root.val = val; + return exports_local_microquickjs_engine_js_value_new(rep); +} + +void exports_local_microquickjs_engine_js_value_destructor(exports_local_microquickjs_engine_js_value_t *rep) { + JS_DeleteGCRef(s_ctx, &rep->root); + free(rep); +} + +// js-value methods +bool exports_local_microquickjs_engine_method_js_value_is_int(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsInt(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_bool(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsBool(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_null(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsNull(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_undefined(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsUndefined(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_exception(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsException(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_number(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsNumber(s_ctx, self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_string(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsString(s_ctx, self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_error(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsError(s_ctx, self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_function(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsFunction(s_ctx, self->val); +} + +void exports_local_microquickjs_engine_method_js_value_to_string(exports_local_microquickjs_engine_borrow_js_value_t self, microquickjs_string_t *ret) { + ensure_context(); + size_t len; + JSCStringBuf buf; + const char *cstr = JS_ToCStringLen(s_ctx, &len, self->val, &buf); + if (!cstr) { + ret->ptr = NULL; + ret->len = 0; + } else { + ret->ptr = (uint8_t *)make_wasi_string(cstr, len); + ret->len = len; + } +} + +int32_t exports_local_microquickjs_engine_method_js_value_to_int32(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + int res; + JS_ToInt32(s_ctx, &res, self->val); + return (int32_t)res; +} + +double exports_local_microquickjs_engine_method_js_value_to_float64(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + double res; + JS_ToNumber(s_ctx, &res, self->val); + return res; +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_method_js_value_get_property(exports_local_microquickjs_engine_borrow_js_value_t self, microquickjs_string_t *name) { + ensure_context(); + char *cname = malloc(name->len + 1); + memcpy(cname, name->ptr, name->len); + cname[name->len] = '\0'; + JSValue res = JS_GetPropertyStr(s_ctx, self->val, cname); + free(cname); + return make_own_value(res); +} + +void exports_local_microquickjs_engine_method_js_value_set_property(exports_local_microquickjs_engine_borrow_js_value_t self, microquickjs_string_t *name, exports_local_microquickjs_engine_borrow_js_value_t val) { + ensure_context(); + char *cname = malloc(name->len + 1); + memcpy(cname, name->ptr, name->len); + cname[name->len] = '\0'; + JSValue prop_val = val->val; + JS_SetPropertyStr(s_ctx, self->val, cname, prop_val); + free(cname); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_method_js_value_call(exports_local_microquickjs_engine_borrow_js_value_t self, exports_local_microquickjs_engine_list_borrow_js_value_t *args) { + ensure_context(); + JS_PushArg(s_ctx, self->val); // function + JS_PushArg(s_ctx, JS_UNDEFINED); // this + for (size_t i = 0; i < args->len; i++) { + JS_PushArg(s_ctx, args->ptr[i]->val); + } + JSValue res = JS_Call(s_ctx, (int)args->len); + return make_own_value(res); +} + +// Free functions +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_int32(int32_t val) { + ensure_context(); + return make_own_value(JS_NewInt32(s_ctx, val)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_float64(double val) { + ensure_context(); + return make_own_value(JS_NewFloat64(s_ctx, val)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_bool(bool val) { + ensure_context(); + return make_own_value(JS_NewBool(val)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_string(microquickjs_string_t *val) { + ensure_context(); + return make_own_value(JS_NewStringLen(s_ctx, (const char *)val->ptr, val->len)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_object(void) { + ensure_context(); + return make_own_value(JS_NewObject(s_ctx)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_array(void) { + ensure_context(); + return make_own_value(JS_NewArray(s_ctx, 0)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_get_global_object(void) { + ensure_context(); + return make_own_value(JS_GetGlobalObject(s_ctx)); +} + +bool exports_local_microquickjs_engine_eval(microquickjs_string_t *code, microquickjs_string_t *ret, microquickjs_string_t *err) { + ensure_context(); + JSValue val = JS_Eval(s_ctx, (const char *)code->ptr, code->len, "", JS_EVAL_RETVAL); + size_t len; + JSCStringBuf buf; + + if (JS_IsException(val)) { + JSValue exc = JS_GetException(s_ctx); + const char *cstr = JS_ToCStringLen(s_ctx, &len, exc, &buf); + if (!cstr) { + static const char unknown[] = "Error: unknown exception"; + err->ptr = (uint8_t *)cabi_realloc(NULL, 0, 1, sizeof(unknown) - 1); + memcpy(err->ptr, unknown, sizeof(unknown) - 1); + err->len = sizeof(unknown) - 1; + } else { + err->ptr = (uint8_t *)make_wasi_string(cstr, len); + err->len = len; + } + return false; + } + + const char *cstr = JS_ToCStringLen(s_ctx, &len, val, &buf); + if (!cstr) { + static const char undef[] = "undefined"; + ret->ptr = (uint8_t *)make_wasi_string(undef, sizeof(undef) - 1); + ret->len = sizeof(undef) - 1; + } else { + ret->ptr = (uint8_t *)make_wasi_string(cstr, len); + ret->len = len; + } + return true; +} diff --git a/microquickjs.wit b/microquickjs.wit new file mode 100644 index 0000000..31e8866 --- /dev/null +++ b/microquickjs.wit @@ -0,0 +1,41 @@ +package local:microquickjs; + +interface engine { + resource js-value { + is-int: func() -> bool; + is-bool: func() -> bool; + is-null: func() -> bool; + is-undefined: func() -> bool; + is-exception: func() -> bool; + is-number: func() -> bool; + is-string: func() -> bool; + is-error: func() -> bool; + is-function: func() -> bool; + + to-string: func() -> string; + to-int32: func() -> s32; + to-float64: func() -> f64; + + get-property: func(name: string) -> js-value; + set-property: func(name: string, val: borrow); + + call: func(args: list>) -> js-value; + } + + new-int32: func(val: s32) -> js-value; + new-float64: func(val: f64) -> js-value; + new-bool: func(val: bool) -> js-value; + new-string: func(val: string) -> js-value; + new-object: func() -> js-value; + new-array: func() -> js-value; + + get-global-object: func() -> js-value; + + /// Evaluate JavaScript code and return the result as a string. + /// Returns ok(result) on success, err(message) on syntax or runtime error. + eval: func(code: string) -> result; +} + +world microquickjs { + export engine; +} diff --git a/mquickjs.c b/mquickjs.c index a950f3c..4aca29f 100644 --- a/mquickjs.c +++ b/mquickjs.c @@ -30,6 +30,11 @@ #include #include #include +#ifdef __wasi__ +#define getenv(x) NULL +#define signal(x, y) (void)0 +#define system(x) (-1) +#endif #include "cutils.h" #include "dtoa.h" diff --git a/packages/wasi-sdk b/packages/wasi-sdk new file mode 160000 index 0000000..003cf14 --- /dev/null +++ b/packages/wasi-sdk @@ -0,0 +1 @@ +Subproject commit 003cf14969ecca789c1922f9047e9a31872e9b52 diff --git a/packages/wasmedge b/packages/wasmedge new file mode 160000 index 0000000..b836225 --- /dev/null +++ b/packages/wasmedge @@ -0,0 +1 @@ +Subproject commit b836225c7816de1bea26478976036538024c87ef diff --git a/readline_tty.c b/readline_tty.c index 9a7e929..902a7f5 100644 --- a/readline_tty.c +++ b/readline_tty.c @@ -40,9 +40,11 @@ #else #include #include +#ifndef __wasi__ #include #include #endif +#endif #include "readline_tty.h" @@ -106,6 +108,13 @@ static void set_processed_input(BOOL enable) SetConsoleMode(handle, mode); } +#elif defined(__wasi__) + +int readline_tty_init(void) +{ + return 80; +} + #else /* init terminal so that we can grab keys */ /* XXX: merge with cp_utils.c */ @@ -185,6 +194,17 @@ void term_flush(void) const char *readline_tty(ReadlineState *s, const char *prompt, BOOL multi_line) { +#if defined(__wasi__) + printf("%s", prompt); + fflush(stdout); + if (fgets((char *)s->term_cmd_buf, s->term_cmd_buf_size, stdin)) { + size_t len = strlen((char *)s->term_cmd_buf); + if (len > 0 && s->term_cmd_buf[len - 1] == '\n') + s->term_cmd_buf[len - 1] = '\0'; + return (const char *)s->term_cmd_buf; + } + return NULL; +#else int len, i, ctrl_c_count, c, ret; const char *ret_str; uint8_t buf[128]; @@ -235,6 +255,7 @@ const char *readline_tty(ReadlineState *s, set_processed_input(TRUE); #endif return ret_str; +#endif } BOOL readline_is_interrupted(void) diff --git a/wasi_preview1_stubs.c b/wasi_preview1_stubs.c new file mode 100644 index 0000000..06101ae --- /dev/null +++ b/wasi_preview1_stubs.c @@ -0,0 +1,36 @@ +#include + +int __imported_wasi_snapshot_preview1_args_sizes_get(size_t *argc, size_t *argv_buf_size) { + if (argc) *argc = 0; + if (argv_buf_size) *argv_buf_size = 0; + return 0; +} + +int __imported_wasi_snapshot_preview1_args_get(char **argv, char *argv_buf) { + return 0; +} + +int __imported_wasi_snapshot_preview1_fd_close(int fd) { + return 0; +} + +int __imported_wasi_snapshot_preview1_fd_seek(int fd, long long offset, int whence, long long *new_offset) { + return 0; +} + +int __imported_wasi_snapshot_preview1_fd_write(int fd, const void *iovs, size_t iovs_len, size_t *nwritten) { + return 0; +} + +void __imported_wasi_snapshot_preview1_proc_exit(int rval) { +} + +int __imported_wasi_snapshot_preview1_environ_sizes_get(size_t *environ_count, size_t *environ_buf_size) { + if (environ_count) *environ_count = 0; + if (environ_buf_size) *environ_buf_size = 0; + return 0; +} + +int __imported_wasi_snapshot_preview1_environ_get(char **environ, char *environ_buf) { + return 0; +}