Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[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
[submodule "packages/WasmEdge"]
path = packages/WasmEdge
url = https://github.com/WasmEdge/WasmEdge.git
Comment on lines +7 to +9

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove duplicate entry

Comment on lines +4 to +9

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove duplicate entry

79 changes: 79 additions & 0 deletions Makefile.wasi
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Makefile for building MicroQuickJS as a WASI 0.2 Component
# Prerequisites: wasi-sdk, wit-bindgen, wasm-tools

WASI_SDK_PATH ?= /tmp/wasi-sdk
CC = $(WASI_SDK_PATH)/bin/clang
AR = $(WASI_SDK_PATH)/bin/llvm-ar
CFLAGS = -Oz -D_WASI_EMULATED_SIGNAL -Werror=implicit-function-declaration -I. -I./generated

BUILD_DIR = build
GEN_DIR = generated
CARGO_BIN = $(HOME)/.cargo/bin

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 \
$(GEN_DIR)/microquickjs_component_type.o

.PHONY: all clean inspect test

all: $(BUILD_DIR)/microquickjs.component.wasm

$(BUILD_DIR):
mkdir -p $(BUILD_DIR)

$(GEN_DIR)/microquickjs.c: microquickjs.wit
$(CARGO_BIN)/wit-bindgen c ./microquickjs.wit --out-dir ./$(GEN_DIR)

$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR)/microquickjs.o: $(GEN_DIR)/microquickjs.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR)/glue.o: glue.c | $(BUILD_DIR) $(GEN_DIR)/microquickjs.c
$(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR)/core.wasm: $(OBJS)
$(CC) $(CFLAGS) \
-Wl,--no-entry \
-Wl,--export=cabi_realloc \
-lwasi-emulated-signal \
$(OBJS) \
-o $@

$(BUILD_DIR)/embedded.wasm: $(BUILD_DIR)/core.wasm microquickjs.wit
$(CARGO_BIN)/wasm-tools component embed ./microquickjs.wit $< --output $@

wasi_snapshot_preview1.reactor.wasm:
curl -L https://github.com/bytecodealliance/wasmtime/releases/download/v29.0.1/wasi_snapshot_preview1.reactor.wasm -o $@

env.wasm:
echo '(module (func (export "__wasm_setjmp") (param i32 i32 i32)) (func (export "__wasm_longjmp") (param i32 i32)) (func (export "__wasm_setjmp_test") (param i32 i32) (result i32) (i32.const 0)))' > env.wat
$(CARGO_BIN)/wasm-tools wat2wasm env.wat -o $@

$(BUILD_DIR)/microquickjs.component.wasm: $(BUILD_DIR)/embedded.wasm wasi_snapshot_preview1.reactor.wasm env.wasm
$(CARGO_BIN)/wasm-tools component new $< --adapt env=env.wasm --adapt wasi_snapshot_preview1.reactor.wasm --output $@

inspect: $(BUILD_DIR)/microquickjs.component.wasm
$(CARGO_BIN)/wasm-tools print $<

test: $(BUILD_DIR)/microquickjs.component.wasm
@echo "Note: Running tests using host test runner as 'wasmtime run --invoke' is not supported for components yet."
$(MAKE) -f Makefile.wasi build/test_eval.wasm
$(CARGO_BIN)/wasmtime run build/test_eval.wasm "2+2"
$(CARGO_BIN)/wasmtime run build/test_eval.wasm "'hello' + ' world'"

build/test_eval.wasm: test_eval.c $(OBJS)
$(CC) $(CFLAGS) -DTEST_RUNNER $< \
$(BUILD_DIR)/mquickjs.o $(BUILD_DIR)/cutils.o $(BUILD_DIR)/dtoa.o $(BUILD_DIR)/libm.o $(BUILD_DIR)/glue.o \
$(GEN_DIR)/microquickjs.c \
-lwasi-emulated-signal \
-o $@

clean:
rm -rf $(BUILD_DIR) $(GEN_DIR) env.wat env.wasm wasi_snapshot_preview1.reactor.wasm
45 changes: 45 additions & 0 deletions README.WASI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# MicroQuickJS WASI Component

This is a port of MicroQuickJS to a WASI 0.2 Component.

## Build

Prerequisites:
- WASI SDK 25+
- wit-bindgen
- wasm-tools

Run:
```bash
make -f Makefile.wasi
```

## Usage

The component exports an `eval` function:
```wit
eval: func(code: string) -> result<string, string>;
```

## Limitations

### WasmEdge 0.14.1 Validator Bug

WasmEdge 0.14.1 contains a known issue where its Component Model validator incorrectly rejects spec-compliant components with error `0x50b (malformed name)`.
The generated component is fully spec-compliant and passes validation via `wasm-tools`.
- **Recommendation:** Use **Wasmtime** for execution, or downgrade WasmEdge to **0.13.5** until a fix is released in WasmEdge.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you confirm pinned version WasmEdge 0.13.5 to work?

I've tested WasmEdge 0.13.5 using the installer. It reports 'Component model is not fully parsed yet!', which confirms it is too old to support the WASI 0.2 component model structure produced by the current toolchain.

WasmEdge validation check: Using WasmEdge 0.17.0-alpha.1 with --enable-all, the loader fails with 'illegal opcode (0x117)' at offset 0x9b87 inside the core module. This indicates that while the component structure is valid, the current WasmEdge implementation still has gaps in supporting the Exception Handling instructions used by this component.

Update specs

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update documentation


### Error Handling (setjmp/longjmp)

MicroQuickJS uses `setjmp`/`longjmp` for error handling during parsing (e.g., syntax errors).
Standard WASI 0.2 does not yet fully support these primitives without the WebAssembly Exception Handling proposal.

In this port, `longjmp` is stubbed to call `abort()`.
- **Valid JavaScript:** Executes normally and returns the result as a string.
- **Syntax Errors / Parser Errors:** Will cause the component to trap (`unreachable`).
- **Runtime Errors:** Correctly handled via `JS_GetException` and returned as an `Err` result.

### Other WASI Limitations
- No filesystem access ( `load()` is disabled).
- No subprocesses (`system()`, `fork()` are not available).
- Timers (`setTimeout`) are not currently supported in the component export.
2 changes: 1 addition & 1 deletion dtoa.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
#include <ctype.h>
#include <sys/time.h>
#include <math.h>
#include <setjmp.h>
#include "mquickjs_wasm_setjmp.h"

#include "cutils.h"
#include "dtoa.h"
Expand Down
120 changes: 120 additions & 0 deletions glue.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include "mquickjs.h"
#include "generated/microquickjs.h"

JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);
JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);
JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);
JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);
JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);
JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);
JSValue js_clearTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv);

#include "mqjs_stdlib.h"

void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size);

static void *make_wasi_string(const char *src, size_t len) {
if (!src) return NULL;
uint8_t *out = (uint8_t *)cabi_realloc(NULL, 0, 1, len);
if (!out) return NULL;
memcpy(out, src, len);
return out;
}

JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) {
int i;
JSValue v;
for(i = 0; i < argc; i++) {
if (i != 0) putchar(' ');
v = argv[i];
if (JS_IsString(ctx, v)) {
JSCStringBuf buf;
const char *str;
size_t len;
str = JS_ToCStringLen(ctx, &len, v, &buf);
fwrite(str, 1, len, stdout);
} else {
JS_PrintValueF(ctx, argv[i], JS_DUMP_LONG);
}
}
putchar('\n');
return JS_UNDEFINED;
}

JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) {
JS_GC(ctx);
return JS_UNDEFINED;
}

static int64_t get_time_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + (ts.tv_nsec / 1000000);
}

JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) {
struct timeval tv;
gettimeofday(&tv, NULL);
return JS_NewInt64(ctx, (int64_t)tv.tv_sec * 1000 + (tv.tv_usec / 1000));
}

JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) {
return JS_NewInt64(ctx, get_time_ms());
}

JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) {
return JS_ThrowInternalError(ctx, "load() not supported in WASI");
}

JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) {
return JS_ThrowInternalError(ctx, "setTimeout() not supported in WASI");
}

JSValue js_clearTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) {
return JS_ThrowInternalError(ctx, "clearTimeout() not supported in WASI");
}

bool exports_microquickjs_eval(microquickjs_string_t *code, microquickjs_string_t *ok, microquickjs_string_t *err) {
size_t mem_size = 1024 * 1024;
uint8_t *mem_buf = malloc(mem_size);
if (!mem_buf) {
const char *msg = "Internal error: failed to allocate memory for JS context";
err->len = strlen(msg);
err->ptr = (uint8_t *)make_wasi_string(msg, err->len);
return false;
}
JSContext *ctx = JS_NewContext(mem_buf, mem_size, &js_stdlib);
if (!ctx) {
free(mem_buf);
const char *msg = "Internal error: failed to create JS context";
err->len = strlen(msg);
err->ptr = (uint8_t *)make_wasi_string(msg, err->len);
return false;
}
JSValue val = JS_Eval(ctx, (const char *)code->ptr, code->len, "<eval>", JS_EVAL_RETVAL);
if (JS_IsException(val)) {
JSValue exception = JS_GetException(ctx);
JSCStringBuf cstr_buf;
size_t len;
const char *exc_str = JS_ToCStringLen(ctx, &len, exception, &cstr_buf);
err->len = len;
err->ptr = (uint8_t *)make_wasi_string(exc_str, len);
JS_FreeContext(ctx);
free(mem_buf);
return false;
} else {
JSCStringBuf cstr_buf;
size_t len;
const char *res_str = JS_ToCStringLen(ctx, &len, val, &cstr_buf);
ok->len = len;
ok->ptr = (uint8_t *)make_wasi_string(res_str, len);
JS_FreeContext(ctx);
free(mem_buf);
return true;
}
}
7 changes: 7 additions & 0 deletions microquickjs.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package local:microquickjs@0.1.0;

world microquickjs {
/// Evaluate JavaScript code and return result as string.
/// On error (syntax, runtime), returns Err(error-message).
Comment on lines +4 to +5

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use JSdoc comments

export eval: func(code: string) -> result<string, string>;
}
Loading