From 903b8b59ddf1e1014d0d0e1e095d6acf9063cbf1 Mon Sep 17 00:00:00 2001 From: ngocphuongnb Date: Mon, 20 Oct 2025 11:57:20 +0700 Subject: [PATCH] fix: prevent shared Option causing race by switching to value instead of pointer --- Makefile | 19 ++++++- options.go | 8 +-- options_test.go | 6 +- proxy.go | 2 - qjs.wasm | Bin 1038759 -> 1038767 bytes qjswasm/helpers.c | 59 +++++++++++--------- qjswasm/proxy.c | 126 ++++++++++++++++++++++++++++++++++++++++++ qjswasm/qjs.c | 99 +++------------------------------ qjswasm/qjs.h | 6 ++ qjswasm/qjswasm.cmake | 7 +++ runtime.go | 16 +++--- runtime_test.go | 66 ++++++++++++++++++---- testutils_test.go | 4 +- 13 files changed, 267 insertions(+), 151 deletions(-) create mode 100644 qjswasm/proxy.c diff --git a/Makefile b/Makefile index fc13c4e..a1ff139 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build clean +.PHONY: build build-debug clean build: @echo "Configuring and building qjs..." @@ -16,6 +16,23 @@ build: wasm-opt -O3 qjs.wasm -o qjs.wasm +build-debug: + @echo "Configuring and building qjs with runtime address debug..." + cd qjswasm/quickjs && \ + rm -rf build && \ + cmake -B build \ + -DQJS_BUILD_LIBC=ON \ + -DQJS_BUILD_CLI_WITH_MIMALLOC=OFF \ + -DQJS_DEBUG_RUNTIME_ADDRESS=ON \ + -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake \ + -DCMAKE_PROJECT_INCLUDE=../qjswasm.cmake + @echo "Building qjs target..." + make -C qjswasm/quickjs/build qjswasm -j$(nproc) + @echo "Copying build/qjswasm to top-level as qjs.wasm..." + cp qjswasm/quickjs/build/qjswasm qjs.wasm + + wasm-opt -O3 qjs.wasm -o qjs.wasm + clean: @echo "Cleaning build directory..." cd quickjs && rm -rf build diff --git a/options.go b/options.go index 6b4a9a2..004b48e 100644 --- a/options.go +++ b/options.go @@ -213,16 +213,16 @@ func (o *EvalOption) Free() { } } -func getRuntimeOption(registry *ProxyRegistry, options ...*Option) (option *Option, err error) { - if len(options) == 0 || options[0] == nil { - option = &Option{} +func getRuntimeOption(registry *ProxyRegistry, options ...Option) (option Option, err error) { + if len(options) == 0 { + option = Option{} } else { option = options[0] } if option.CWD == "" { if option.CWD, err = os.Getwd(); err != nil { - return nil, fmt.Errorf("cannot get current working directory: %w", err) + return Option{}, fmt.Errorf("cannot get current working directory: %w", err) } } diff --git a/options_test.go b/options_test.go index 68e20c9..edcf81e 100644 --- a/options_test.go +++ b/options_test.go @@ -39,11 +39,7 @@ func TestEvalOptions(t *testing.T) { t.Run("explicit_cwd_provided", func(t *testing.T) { tempDir := t.TempDir() - options := &qjs.Option{ - CWD: tempDir, - } - - runtime, err := qjs.New(options) + runtime, err := qjs.New(qjs.Option{CWD: tempDir}) require.NoError(t, err) runtime.Close() }) diff --git a/proxy.go b/proxy.go index eee3f52..6c4103b 100644 --- a/proxy.go +++ b/proxy.go @@ -125,7 +125,6 @@ func createFuncProxyWithRegistry(registry *ProxyRegistry) JsFunctionProxy { ) (rs uint64) { goFunc, this := getProxyFuncParams(registry, module.Memory(), thisVal, argc, argv) - // Ensure panic recovery for safe execution defer func() { if r := recover(); r != nil { rs = handlePanicRecovery(this, r) @@ -182,7 +181,6 @@ func getProxyFuncParams( isAsync := args[2] // The third argument is the async flag goFunc, goContext := retrieveGoResources(registry, functionHandle, jsContextHandle) - promise := extractPromiseIfAsync(goContext, args, isAsync) fnArgs := extractFunctionArguments(goContext, args) this := createThisContext(goContext, thisRef, fnArgs, promise, isAsync) diff --git a/qjs.wasm b/qjs.wasm index 6a86362f481f5acac8bb224268ff9069a30b8cf1..b1f0d255cccbdf6a09d736286a29bd33e67d12fa 100755 GIT binary patch delta 318 zcmZ3!*?#?I`wchPIOJH^Sy#x^Wx;;4D3 z$}dPQDya-EDPowzEC^ENms$=Kt*8u3%qdM}h!6q_IVa}iIOpdTmlTyIm*f{Q++*eg zs)`4y28x!XR+KPAvO@(JqBbuTFk)m22@MO6l-zt*@E$W`$mBhuK8%w#bBd|SPBu2l z+C1ATn{l(gF$?45xhArV&o}Qj`OLz|K6$;R5et(8Q~l(JmP&#O3|N?zU;~wqXQfCLm@8Viq7~ h1!6WJW(Q&pAm#*OE+FOxVjdvo1!BJK-8TF(a{*BSW7z-z delta 329 zcmZ3#*?#$E`wchPIN0S_*crK5ST;Xk^Wv-*4D3`@)@O=Soc1PZyQ zmH>qcQj1C|gG-7S<}eEZ1)URfa-8$?ic5-0lS}f8816B1g0u%D=4B=`gzy24i3jq5 zDoRo-N*Kb~p#lsMAQOX8OeFi25*gZRQkH zlikc>%ECBV-#BYBhnXzn{mp7-pII1LCreoyO%AYD5@vExU{GKXc*~^3?bvk_$aG|z z+-$AL$-~GEl5k|7yxv-W@*``V%`=U&87K4Ev@k|APqt~FY{Lk|OhC*G#4JF}3dC$c e%nrmHK+FlmTtLhX#5_RE3&eceC)@DL%mn~zCTN!c diff --git a/qjswasm/helpers.c b/qjswasm/helpers.c index ff1a3f8..afd8682 100644 --- a/qjswasm/helpers.c +++ b/qjswasm/helpers.c @@ -3,6 +3,37 @@ #include #include +#ifdef QJS_DEBUG_RUNTIME_ADDRESS +/** + * Allocates a random-sized block of memory to randomize address space layout. + * This helps in debugging by ensuring runtime objects are allocated at different + * addresses on each run, making pointer-related bugs more apparent. + */ +void randomize_address_space(void) +{ + static unsigned int call_counter = 0; + int stack_variable; + + // Generate entropy from multiple sources for better randomization + unsigned int entropy = (unsigned int)((uintptr_t)&stack_variable ^ // Stack address (varies per call) + (uintptr_t)time(NULL) ^ // Current time + (uintptr_t)clock() ^ // Clock ticks (higher resolution) + (++call_counter) // Incremental counter + ); + + // Allocate 1-1024 bytes to perturb the address space + size_t allocation_size = (entropy % 1024) + 1; + volatile void *random_allocation = malloc(allocation_size); + + // Note: Intentionally not freeing to affect subsequent allocations + // Touch the allocated memory to ensure it's not optimized away + if (random_allocation) + { + *((volatile char *)random_allocation) = 0; + } +} +#endif + JSValue JS_NewNull() { return JS_NULL; } JSValue JS_NewUndefined() { return JS_UNDEFINED; } JSValue JS_NewUninitialized() { return JS_UNINITIALIZED; } @@ -606,34 +637,8 @@ JSValue QJS_Call(JSContext *ctx, JSValue func, JSValue this, int argc, uint64_t return JS_Call(ctx, func, this, argc, js_argv); } -// Create a new QJS_PROXY_VALUE instance directly in C for better performance -JSValue QJS_NewProxyValue(JSContext *ctx, int64_t proxyId) +void QJS_Panic() { - // Get the QJS_PROXY_VALUE constructor from global object - JSValue global_obj = JS_GetGlobalObject(ctx); - JSValue ctor = JS_GetPropertyStr(ctx, global_obj, "QJS_PROXY_VALUE"); - JS_FreeValue(ctx, global_obj); - - if (JS_IsException(ctor) || JS_IsUndefined(ctor)) { - JS_FreeValue(ctx, ctor); - return JS_ThrowReferenceError(ctx, "QJS_PROXY_VALUE is not defined"); - } - - // Create argument for the constructor (proxyId) - JSValue arg = JS_NewInt64(ctx, proxyId); - JSValue args[1] = { arg }; - - // Call the constructor with 'new' - JSValue result = JS_CallConstructor(ctx, ctor, 1, args); - - // Clean up - JS_FreeValue(ctx, ctor); - JS_FreeValue(ctx, arg); - - return result; -} - -void QJS_Panic() { // Handle panic situation fprintf(stderr, "QJS Panic: Unrecoverable error occurred\n"); abort(); diff --git a/qjswasm/proxy.c b/qjswasm/proxy.c new file mode 100644 index 0000000..f692904 --- /dev/null +++ b/qjswasm/proxy.c @@ -0,0 +1,126 @@ +#include "qjs.h" + +// toString method for QJS_PROXY_VALUE class +static JSValue qjs_proxy_value_toString(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + JSValue proxy_id = JS_GetPropertyStr(ctx, this_val, "proxyId"); + if (JS_IsException(proxy_id)) + return proxy_id; + + const char *proxy_id_str = JS_ToCString(ctx, proxy_id); + JS_FreeValue(ctx, proxy_id); + + if (!proxy_id_str) + return JS_EXCEPTION; + + char buffer[256]; + snprintf(buffer, sizeof(buffer), "[object QJS_PROXY_VALUE(proxyId: %s)]", proxy_id_str); + JS_FreeCString(ctx, proxy_id_str); + + return JS_NewString(ctx, buffer); +} + +// Constructor function for QJS_PROXY_VALUE class +static JSValue qjs_proxy_value_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) +{ + JSValue obj; + JSValue proto; + + if (JS_IsUndefined(new_target)) + { + // Called as function, not constructor + return JS_ThrowTypeError(ctx, "QJS_PROXY_VALUE must be called with new"); + } + + // Get prototype from new_target + proto = JS_GetPropertyStr(ctx, new_target, "prototype"); + if (JS_IsException(proto)) + return proto; + + // Create object with proper prototype + obj = JS_NewObjectProto(ctx, proto); + JS_FreeValue(ctx, proto); + + if (JS_IsException(obj)) + return obj; + + // Set the proxyId property + if (argc > 0) + { + if (JS_SetPropertyStr(ctx, obj, "proxyId", JS_DupValue(ctx, argv[0])) < 0) + { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + } + else + { + if (JS_SetPropertyStr(ctx, obj, "proxyId", JS_UNDEFINED) < 0) + { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + } + + return obj; +} + +// Initialize QJS_PROXY_VALUE class and add it to global object +int init_qjs_proxy_value_class(JSContext *ctx) +{ + JSValue global_obj = JS_GetGlobalObject(ctx); + + // Create prototype object with toString method + JSValue proto = JS_NewObject(ctx); + JSValue toString_func = JS_NewCFunction(ctx, qjs_proxy_value_toString, "toString", 0); + if (JS_SetPropertyStr(ctx, proto, "toString", toString_func) < 0) + { + JS_FreeValue(ctx, proto); + JS_FreeValue(ctx, global_obj); + return -1; + } + + // Create the constructor function + JSValue ctor = JS_NewCFunction2(ctx, qjs_proxy_value_constructor, "QJS_PROXY_VALUE", 1, JS_CFUNC_constructor, 0); + + // Set proto.constructor and ctor.prototype using QuickJS helper + JS_SetConstructor(ctx, ctor, proto); + + // Add the constructor to the global object + if (JS_SetPropertyStr(ctx, global_obj, "QJS_PROXY_VALUE", ctor) < 0) + { + JS_FreeValue(ctx, global_obj); + return -1; + } + + JS_FreeValue(ctx, global_obj); + return 0; +} + +// Create a new QJS_PROXY_VALUE instance directly in C for better performance +JSValue QJS_NewProxyValue(JSContext *ctx, int64_t proxyId) +{ + // Get the QJS_PROXY_VALUE constructor from global object + JSValue global_obj = JS_GetGlobalObject(ctx); + JSValue ctor = JS_GetPropertyStr(ctx, global_obj, "QJS_PROXY_VALUE"); + JS_FreeValue(ctx, global_obj); + + if (JS_IsException(ctor) || JS_IsUndefined(ctor)) + { + JS_FreeValue(ctx, ctor); + return JS_ThrowReferenceError(ctx, "QJS_PROXY_VALUE is not defined"); + } + + // Create argument for the constructor (proxyId) + JSValue arg = JS_NewInt64(ctx, proxyId); + JSValue args[1] = {arg}; + + // Call the constructor with 'new' + JSValue result = JS_CallConstructor(ctx, ctor, 1, args); + + // Clean up + JS_FreeValue(ctx, ctor); + JS_FreeValue(ctx, arg); + + return result; +} diff --git a/qjswasm/qjs.c b/qjswasm/qjs.c index 9904e27..949168b 100644 --- a/qjswasm/qjs.c +++ b/qjswasm/qjs.c @@ -1,94 +1,5 @@ #include "qjs.h" -// toString method for QJS_PROXY_VALUE class -static JSValue qjs_proxy_value_toString(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) -{ - JSValue proxy_id = JS_GetPropertyStr(ctx, this_val, "proxyId"); - if (JS_IsException(proxy_id)) - return proxy_id; - - const char *proxy_id_str = JS_ToCString(ctx, proxy_id); - JS_FreeValue(ctx, proxy_id); - - if (!proxy_id_str) - return JS_EXCEPTION; - - char buffer[256]; - snprintf(buffer, sizeof(buffer), "[object QJS_PROXY_VALUE(proxyId: %s)]", proxy_id_str); - JS_FreeCString(ctx, proxy_id_str); - - return JS_NewString(ctx, buffer); -} - -// Constructor function for QJS_PROXY_VALUE class -static JSValue qjs_proxy_value_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) -{ - JSValue obj; - JSValue proto; - - if (JS_IsUndefined(new_target)) { - // Called as function, not constructor - return JS_ThrowTypeError(ctx, "QJS_PROXY_VALUE must be called with new"); - } - - // Get prototype from new_target - proto = JS_GetPropertyStr(ctx, new_target, "prototype"); - if (JS_IsException(proto)) - return proto; - - // Create object with proper prototype - obj = JS_NewObjectProto(ctx, proto); - JS_FreeValue(ctx, proto); - - if (JS_IsException(obj)) - return obj; - - // Set the proxyId property - if (argc > 0) { - if (JS_SetPropertyStr(ctx, obj, "proxyId", JS_DupValue(ctx, argv[0])) < 0) { - JS_FreeValue(ctx, obj); - return JS_EXCEPTION; - } - } else { - if (JS_SetPropertyStr(ctx, obj, "proxyId", JS_UNDEFINED) < 0) { - JS_FreeValue(ctx, obj); - return JS_EXCEPTION; - } - } - - return obj; -} - -// Initialize QJS_PROXY_VALUE class and add it to global object -static int init_qjs_proxy_value_class(JSContext *ctx) -{ - JSValue global_obj = JS_GetGlobalObject(ctx); - - // Create prototype object with toString method - JSValue proto = JS_NewObject(ctx); - JSValue toString_func = JS_NewCFunction(ctx, qjs_proxy_value_toString, "toString", 0); - if (JS_SetPropertyStr(ctx, proto, "toString", toString_func) < 0) { - JS_FreeValue(ctx, proto); - JS_FreeValue(ctx, global_obj); - return -1; - } - - // Create the constructor function - JSValue ctor = JS_NewCFunction2(ctx, qjs_proxy_value_constructor, "QJS_PROXY_VALUE", 1, JS_CFUNC_constructor, 0); - - // Set proto.constructor and ctor.prototype using QuickJS helper - JS_SetConstructor(ctx, ctor, proto); - - // Add the constructor to the global object - if (JS_SetPropertyStr(ctx, global_obj, "QJS_PROXY_VALUE", ctor) < 0) { - JS_FreeValue(ctx, global_obj); - return -1; - } - - JS_FreeValue(ctx, global_obj); - return 0; -} - JSContext *New_QJSContext(JSRuntime *rt) { JSContext *ctx; @@ -110,6 +21,10 @@ QJSRuntime *New_QJS( JSRuntime *runtime; JSContext *ctx; +#ifdef QJS_DEBUG_RUNTIME_ADDRESS + randomize_address_space(); +#endif + runtime = JS_NewRuntime(); if (!runtime) @@ -141,14 +56,16 @@ QJSRuntime *New_QJS( } // Initialize QJS_PROXY_VALUE class - if (init_qjs_proxy_value_class(ctx) < 0) { + if (init_qjs_proxy_value_class(ctx) < 0) + { JS_FreeContext(ctx); JS_FreeRuntime(runtime); return NULL; } QJSRuntime *qjs = (QJSRuntime *)malloc(sizeof(QJSRuntime)); - if (!qjs) { + if (!qjs) + { JS_FreeContext(ctx); JS_FreeRuntime(runtime); return NULL; diff --git a/qjswasm/qjs.h b/qjswasm/qjs.h index 1978f7d..d30e8e4 100644 --- a/qjswasm/qjs.h +++ b/qjswasm/qjs.h @@ -79,6 +79,12 @@ JSValue InvokeAsyncFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc // JSValue goAsyncFunctionProxy(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv); JSValue jsFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); +#ifdef QJS_DEBUG_RUNTIME_ADDRESS +void randomize_address_space(void); +#endif + +int init_qjs_proxy_value_class(JSContext *ctx); + JSContext *New_QJSContext(JSRuntime *rt); // QJSRuntime *New_QJS(QJSRuntimeOptions); QJSRuntime *New_QJS( diff --git a/qjswasm/qjswasm.cmake b/qjswasm/qjswasm.cmake index 153a823..8fd8607 100644 --- a/qjswasm/qjswasm.cmake +++ b/qjswasm/qjswasm.cmake @@ -23,6 +23,12 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") ) endif() +# Optional debug flag for runtime address randomization +option(QJS_DEBUG_RUNTIME_ADDRESS "Enable runtime address randomization debugging" OFF) +if(QJS_DEBUG_RUNTIME_ADDRESS) + add_compile_definitions(QJS_DEBUG_RUNTIME_ADDRESS) +endif() + if(NOT CMAKE_SYSTEM_NAME STREQUAL "WASI") list(APPEND qjs_libs ${CMAKE_THREAD_LIBS_INIT}) endif() @@ -33,6 +39,7 @@ add_executable(qjswasm ../eval.c ../function.c ../helpers.c + ../proxy.c ../qjs.c ) diff --git a/runtime.go b/runtime.go index d11e9e0..85615f0 100644 --- a/runtime.go +++ b/runtime.go @@ -30,7 +30,7 @@ type Runtime struct { malloc api.Function free api.Function mem *Mem - option *Option + option Option handle *Handle context *Context registry *ProxyRegistry @@ -76,7 +76,7 @@ func createGlobalCompiledModule( } // New creates a QuickJS runtime with optional configuration. -func New(options ...*Option) (runtime *Runtime, err error) { +func New(options ...Option) (runtime *Runtime, err error) { defer func() { rerr := AnyToError(recover()) if rerr != nil { @@ -147,6 +147,10 @@ func New(options ...*Option) (runtime *Runtime, err error) { return runtime, nil } +func (r *Runtime) Raw() uint64 { + return r.handle.raw +} + // FreeQJSRuntime frees the QJS runtime. func (r *Runtime) FreeQJSRuntime() { defer func() { @@ -320,21 +324,17 @@ func (r *Runtime) call(name string, args ...uint64) uint64 { type Pool struct { pools chan *Runtime size int - option *Option + option Option setupFuncs []func(*Runtime) error mu sync.Mutex } // NewPool creates a new runtime pool with the specified size and configuration. -func NewPool(size int, option *Option, setupFuncs ...func(*Runtime) error) *Pool { +func NewPool(size int, option Option, setupFuncs ...func(*Runtime) error) *Pool { if size <= 0 { panic("pool size must be greater than 0") } - if option == nil { - option = &Option{} - } - p := &Pool{ pools: make(chan *Runtime, size), size: size, diff --git a/runtime_test.go b/runtime_test.go index dca7941..f0c4160 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -54,7 +54,7 @@ func testConcurrentRuntimeExecution(t *testing.T, threadID int) { } func createTestPoolWithSetup() *qjs.Pool { - return qjs.NewPool(10, &qjs.Option{ + return qjs.NewPool(10, qjs.Option{ MaxStackSize: 1024 * 1024 * 10, }, func(rt *qjs.Runtime) error { _, err := rt.Context().Eval( @@ -93,7 +93,7 @@ func testPooledRuntimeExecution(t *testing.T, pool *qjs.Pool, workerID int) { } func createTestPool(size int, setupFuncs ...func(*qjs.Runtime) error) *qjs.Pool { - return qjs.NewPool(size, nil, setupFuncs...) + return qjs.NewPool(size, qjs.Option{}, setupFuncs...) } func verifyRuntimeExecution(t *testing.T, rt *qjs.Runtime, code string, expected any) { @@ -131,18 +131,19 @@ func TestRuntime(t *testing.T) { t.Run("RuntimeCreation", func(t *testing.T) { rt, _ := setupTestContext(t) assert.Contains(t, rt.String(), "QJSRuntime") + assert.NotZero(t, rt.Raw(), "Runtime raw pointer should not be zero") }) t.Run("RuntimeCreationWithInvalidProxyFunction", func(t *testing.T) { invalidFunc := "invalidFunction" - _, err := qjs.New(&qjs.Option{ProxyFunction: invalidFunc}) + _, err := qjs.New(qjs.Option{ProxyFunction: invalidFunc}) assert.Error(t, err, "Creating runtime with invalid proxy function should return error") }) t.Run("RuntimeCreationWithCanceledContext", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - _, err := qjs.New(&qjs.Option{ + _, err := qjs.New(qjs.Option{ Context: ctx, CloseOnContextDone: true, DisableBuildCache: true, @@ -154,7 +155,7 @@ func TestRuntime(t *testing.T) { // Too short to be valid WASM truncatedWasmBytes := []byte{0x00, 0x61, 0x73} - _, err := qjs.New(&qjs.Option{QuickJSWasmBytes: truncatedWasmBytes}) + _, err := qjs.New(qjs.Option{QuickJSWasmBytes: truncatedWasmBytes}) require.Error(t, err) assert.Contains(t, err.Error(), "failed to create global compiled module") assert.Contains(t, err.Error(), "failed to compile qjs module") @@ -163,7 +164,7 @@ func TestRuntime(t *testing.T) { t.Run("RuntimeCreationWithErrorStartFunction", func(t *testing.T) { invalidStartFunc := "QJS_Panic" - _, err := qjs.New(&qjs.Option{StartFunctionName: invalidStartFunc}) + _, err := qjs.New(qjs.Option{StartFunctionName: invalidStartFunc}) assert.Error(t, err, "Creating runtime with invalid start function should return error") }) @@ -204,7 +205,7 @@ func TestRuntime(t *testing.T) { t.Run("MallocError", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - rt := must(qjs.New(&qjs.Option{ + rt := must(qjs.New(qjs.Option{ Context: ctx, CloseOnContextDone: true, DisableBuildCache: true, @@ -227,7 +228,7 @@ func TestRuntime(t *testing.T) { t.Run("FreeHandleError", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - rt := must(qjs.New(&qjs.Option{Context: ctx})) + rt := must(qjs.New(qjs.Option{Context: ctx})) ptr := rt.Malloc(1024) cancel() // Cancel context to simulate error assert.Panics(t, func() { @@ -247,7 +248,7 @@ func TestRuntime(t *testing.T) { val.Free() invalidBytes := []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00} - _, err = qjs.New(&qjs.Option{QuickJSWasmBytes: invalidBytes}) + _, err = qjs.New(qjs.Option{QuickJSWasmBytes: invalidBytes}) assert.Error(t, err, "Creating runtime with invalid QuickJSWasmBytes should return error") rt2, err := qjs.New() @@ -464,7 +465,7 @@ func TestPoolSetupFunctionHandling(t *testing.T) { invalidBytes := []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00} setupFunc := createSetupFunction("setupValue", "test", true, fmt.Errorf("invalid WASM bytes")) - pool := qjs.NewPool(3, &qjs.Option{QuickJSWasmBytes: invalidBytes}, setupFunc) + pool := qjs.NewPool(3, qjs.Option{QuickJSWasmBytes: invalidBytes}, setupFunc) rt, err := pool.Get() assert.Nil(t, rt) assert.Error(t, err) @@ -549,3 +550,48 @@ func TestPoolConcurrentAccess(t *testing.T) { assert.Equal(t, numThreads, successCount, "All goroutines should complete successfully") }) } + +func TestPoolCallGoFuncFromJs(t *testing.T) { + pool := qjs.NewPool(5, qjs.Option{}, func(rt *qjs.Runtime) error { + result, err := rt.Context().Eval("
", qjs.Code(` + const hello = (i, getEntity) => getEntity(); + export default { hello }; + `), qjs.TypeModule()) + if err != nil { + return err + } + + rt.Context().Global().SetPropertyStr("defaultExports", result) + return nil + }) + + invokeJsFunc := func(jsFuncName string, args ...any) (*qjs.Value, error) { + rt, err := pool.Get() + if err != nil { + return nil, err + } + defer pool.Put(rt) + + defaultExports := rt.Context().Global().GetPropertyStr("defaultExports") + return defaultExports.Invoke(jsFuncName, args...) + } + + var wg sync.WaitGroup + concurrentRoutines := 10 + for i := range concurrentRoutines { + wg.Add(1) + go func(i int) { + defer wg.Done() + getEntity := func() map[string]any { + return map[string]any{ + "id": i, + "name": fmt.Sprintf("Entity %d", i), + } + } + _, err := invokeJsFunc("hello", i, getEntity) + assert.NoError(t, err) + }(i) + } + + wg.Wait() +} diff --git a/testutils_test.go b/testutils_test.go index 7f12548..13411e7 100644 --- a/testutils_test.go +++ b/testutils_test.go @@ -213,9 +213,7 @@ func genModeGlobalTests(t *testing.T) []modeGlobalTest { func runModeGlobalTests(t *testing.T, tests []modeGlobalTest, isScript bool) { for _, test := range tests { t.Run(test.file, func(t *testing.T) { - runtime := must(qjs.New(&qjs.Option{ - MaxStackSize: 512 * 1024, - })) + runtime := must(qjs.New(qjs.Option{MaxStackSize: 512 * 1024})) defer runtime.Close() var val *qjs.Value var err error