diff --git a/Makefile.wasi b/Makefile.wasi new file mode 100644 index 0000000..bb0da20 --- /dev/null +++ b/Makefile.wasi @@ -0,0 +1,103 @@ +WASI_SDK_PATH ?= /opt/wasi-sdk +ADAPTER ?= wasi_snapshot_preview1.reactor.wasm + +CC = $(WASI_SDK_PATH)/bin/clang +HOST_CC = gcc + +# Component build flags +CFLAGS = -Oz \ + --target=wasm32-wasi \ + -D_WASI_EMULATED_SIGNAL \ + -Ibuild -Igenerated -I. \ + -mllvm -wasm-enable-sjlj + +LDFLAGS = -mexec-model=reactor \ + -Wl,--export=cabi_realloc \ + -Wl,--export=__wasm_call_ctors \ + -lwasi-emulated-signal \ + -lwasi-emulated-process-clocks \ + -lsetjmp \ + -lm + +# CLI build flags (for tests) +CLI_CFLAGS = -Oz -mllvm -wasm-enable-sjlj \ + --target=wasm32-wasip2 \ + -Ibuild -I. + +CLI_LDFLAGS = -lm -lsetjmp + +BUILD_DIR = build +GEN_DIR = generated + +CORE_WASM = $(BUILD_DIR)/core.wasm +COMPONENT_WASM = $(BUILD_DIR)/microquickjs.component.wasm +CLI_WASM = $(BUILD_DIR)/mqjs.wasm + +SRCS = glue.c \ + mquickjs.c \ + cutils.c \ + dtoa.c \ + libm.c \ + $(GEN_DIR)/microquickjs.c + +OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS))) + +all: $(COMPONENT_WASM) $(CLI_WASM) + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +# Header generation +$(BUILD_DIR)/mqjs_stdlib.h: mqjs_stdlib.c mquickjs_build.c cutils.c | $(BUILD_DIR) + $(HOST_CC) -O2 -I. -o $(BUILD_DIR)/mqjs_stdlib_native $^ -lm + ./$(BUILD_DIR)/mqjs_stdlib_native -m32 > $@ + +$(BUILD_DIR)/mquickjs_atom.h: mqjs_stdlib.c mquickjs_build.c cutils.c | $(BUILD_DIR) + $(HOST_CC) -O2 -I. -o $(BUILD_DIR)/mqjs_stdlib_native $^ -lm + ./$(BUILD_DIR)/mqjs_stdlib_native -a -m32 > $@ + +# Component Object Compilation +$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD_DIR)/microquickjs.o: $(GEN_DIR)/microquickjs.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +# Core WASM +$(CORE_WASM): $(OBJS) $(GEN_DIR)/microquickjs_component_type.o + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +# Component composition +$(COMPONENT_WASM): $(CORE_WASM) + wasm-tools component embed ./microquickjs.wit --world microquickjs $(CORE_WASM) -o $(BUILD_DIR)/embedded.wasm + wasm-tools component new --skip-validation --adapt wasi_snapshot_preview1=$(ADAPTER) $(BUILD_DIR)/embedded.wasm -o $@ + +# CLI WASM (wasip2) +CLI_SRCS = mqjs.c mquickjs.c dtoa.c libm.c cutils.c readline_tty_wasi.c readline.c +CLI_OBJS = $(patsubst %.c, $(BUILD_DIR)/%_cli.o, $(CLI_SRCS)) + +$(BUILD_DIR)/%_cli.o: %.c | $(BUILD_DIR) + $(CC) $(CLI_CFLAGS) -c -o $@ $< + +$(CLI_WASM): $(CLI_OBJS) + $(CC) $(CLI_CFLAGS) -o $@ $^ $(CLI_LDFLAGS) + +# Dependencies +$(BUILD_DIR)/glue.o: glue.c $(BUILD_DIR)/mqjs_stdlib.h +$(BUILD_DIR)/mquickjs.o: mquickjs.c $(BUILD_DIR)/mquickjs_atom.h +$(BUILD_DIR)/mquickjs_cli.o: mquickjs.c $(BUILD_DIR)/mquickjs_atom.h +$(BUILD_DIR)/mqjs_cli.o: mqjs.c $(BUILD_DIR)/mqjs_stdlib.h + +test: $(CLI_WASM) + wasmtime run -W all-proposals=y --wasi cli=y $(CLI_WASM) tests/test_closure.js + wasmtime run -W all-proposals=y --wasi cli=y $(CLI_WASM) tests/test_language.js + wasmtime run -W all-proposals=y --wasi cli=y $(CLI_WASM) tests/test_loop.js + wasmtime run -W all-proposals=y --wasi cli=y $(CLI_WASM) tests/test_builtin.js + +microbench: $(CLI_WASM) + wasmtime run -W all-proposals=y --wasi cli=y $(CLI_WASM) tests/microbench.js + +clean: + rm -rf $(BUILD_DIR) $(GEN_DIR) + +.PHONY: all clean test microbench diff --git a/README.md b/README.md index 3e53b59..584f5ff 100644 --- a/README.md +++ b/README.md @@ -376,3 +376,23 @@ MQuickJS is released under the MIT license. Unless otherwise specified, the MQuickJS sources are copyright Fabrice Bellard and Charlie Gordon. + +## WASI Component build + +Build the WASI 0.2 component using `Makefile.wasi`: + +```bash +make -f Makefile.wasi +``` + +Run the basic tests using the `wasm32-wasip2` build binary executable: + +```bash +make -f Makefile.wasi test +``` + +Running the QuickJS micro benchmark: + +```bash +make -f Makefile.wasi microbench +``` diff --git a/glue.c b/glue.c new file mode 100644 index 0000000..a63433a --- /dev/null +++ b/glue.c @@ -0,0 +1,285 @@ +#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) + { JS_GC(ctx); 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; } + +#include "generated/microquickjs.h" +#include "mqjs_stdlib.h" + +static uint8_t s_mem[4 * 1024 * 1024]; // 4 MiB arena +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); +} + +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); +} + +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 = cabi_realloc(NULL, 0, 1, 1); + ret->ptr[0] = '\0'; + ret->len = 0; + } else { + ret->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(ret->ptr, 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 = 0; + JS_ToInt32(s_ctx, &res, self->val); + return res; +} + +double exports_local_microquickjs_engine_method_js_value_to_float64( + exports_local_microquickjs_engine_borrow_js_value_t self) +{ + ensure_context(); + double res = 0; + 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'; + JS_SetPropertyStr(s_ctx, self->val, cname, val->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(); + 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); +} + +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 = cabi_realloc(NULL, 0, 1, sizeof(unknown) - 1); + memcpy(err->ptr, unknown, sizeof(unknown) - 1); + err->len = sizeof(unknown) - 1; + } else { + err->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(err->ptr, 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 = cabi_realloc(NULL, 0, 1, sizeof(undef) - 1); + memcpy(ret->ptr, undef, sizeof(undef) - 1); + ret->len = sizeof(undef) - 1; + } else { + ret->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(ret->ptr, 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_build.c b/mquickjs_build.c index 6173271..9c33296 100644 --- a/mquickjs_build.c +++ b/mquickjs_build.c @@ -286,7 +286,7 @@ static int atom_cmp(const void *p1, const void *p2) /* js_atom_table must be properly aligned because the property hash table uses the low bits of the atom pointer value */ -#define ATOM_ALIGN 64 +#define ATOM_ALIGN 256 static void dump_atoms(BuildContext *ctx) { @@ -898,8 +898,8 @@ int build_atoms(const char *stdlib_name, const JSPropDef *global_obj, printf("/* this file is automatically generated - do not edit */\n\n"); printf("#include \"mquickjs_priv.h\"\n\n"); - printf("static const uint%u_t __attribute((aligned(%d))) js_stdlib_table[] = {\n", - JSW * 8, ATOM_ALIGN); + printf("const uint32_t __attribute((aligned(%d))) js_stdlib_table[] = {\n", + ATOM_ALIGN); dump_atoms(s); diff --git a/readline_tty_wasi.c b/readline_tty_wasi.c new file mode 100644 index 0000000..5be3d35 --- /dev/null +++ b/readline_tty_wasi.c @@ -0,0 +1,16 @@ +#include +#include +#include +#include +#include "readline_tty.h" + +int readline_tty_init(void) { return 80; } +void term_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); +} +void term_flush(void) { fflush(stdout); } +const char *readline_tty(ReadlineState *s, const char *prompt, BOOL multi_line) { return NULL; } +BOOL readline_is_interrupted(void) { return 0; }