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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
coverage
.tool-versions
releases
.idea/
15 changes: 6 additions & 9 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"hash/fnv"
"math"
"reflect"
"slices"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -554,13 +555,9 @@ func NumericBoundsCheck(floatVal float64, targetKind reflect.Kind) error {

// IsTypedArray returns true if the input is TypedArray or DataView.
func IsTypedArray(input *Value) bool {
for _, typeName := range typedArrayTypes {
if input.IsGlobalInstanceOf(typeName) {
return true
}
}

return false
return slices.ContainsFunc(typedArrayTypes, func(typeName string) bool {
return input.IsGlobalInstanceOf(typeName)
})
}

// processTempValue validates if temp is a valid result for the given T type.
Expand Down Expand Up @@ -715,9 +712,9 @@ func createGoObjectTarget[T any](input ObjectOrMap, samples ...T) (
obj = obj.ToMap()
}

target = reflect.TypeOf(sample)
target = reflect.TypeFor[T]()
if target == nil {
target = reflect.TypeOf(map[string]any{})
target = reflect.TypeFor[map[string]any]()
}

temp = reflect.New(target).Interface()
Expand Down
9 changes: 9 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,15 @@ func (c *Context) Invoke(fn *Value, this *Value, args ...*Value) (*Value, error)
return normalizeJsValue(c, result)
}

func (c *Context) defaultModuleLoader(moduleName string) uint64 {
moduleNameHandle := c.NewStringHandle(moduleName)
defer moduleNameHandle.Free()

result := c.runtime.Call("QJS_ModuleLoader", c.Raw(), moduleNameHandle.Raw(), 0)

return result.raw
}

// createJsCallArgs marshals Go Value arguments to WASM memory for JavaScript calls.
func createJsCallArgs(c *Context, args ...*Value) (uint64, uint64) {
var argvPtr uint64
Expand Down
10 changes: 6 additions & 4 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import (
"fmt"
"reflect"
"runtime/debug"
"strings"
)

var (
ErrRType = reflect.TypeOf((*error)(nil)).Elem()
ErrRType = reflect.TypeFor[error]()
ErrZeroRValue = reflect.Zero(ErrRType)
ErrCallFuncOnNonObject = errors.New("cannot call function on non-object")
ErrNotAnObject = errors.New("value is not an object")
Expand Down Expand Up @@ -40,15 +41,16 @@ func combineErrors(errs ...error) error {
return nil
}

var errStr string
var builder strings.Builder

for _, err := range errs {
if err != nil {
errStr += err.Error() + "\n"
builder.WriteString(err.Error())
builder.WriteString("\n")
}
}

return errors.New(errStr)
return errors.New(builder.String())
}

func newMaxLengthExceededErr(request uint, maxLen int64, index int) error {
Expand Down
4 changes: 2 additions & 2 deletions jstogo.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,9 +546,9 @@ func jsObjectToGo[T any](

_, sample := createTemp(samples...)

targetType := reflect.TypeOf(sample)
targetType := reflect.TypeFor[T]()
if targetType == nil {
targetType = reflect.TypeOf(map[string]any{})
targetType = reflect.TypeFor[map[string]any]()
}

if targetType.Kind() == reflect.Map {
Expand Down
2 changes: 1 addition & 1 deletion jstogo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2076,7 +2076,7 @@ func TestJsFuncToGo(t *testing.T) {
} else {
// Check if it's an error type
returnType := sampleFnType.Out(0)
if returnType.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if returnType.Implements(reflect.TypeFor[error]()) {
// Single error return
assert.Nil(t, results[0].Interface())
} else {
Expand Down
131 changes: 130 additions & 1 deletion proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (r *ProxyRegistry) Register(fn any) uint64 {
}

// Get retrieves a function by its ID.
// Returns the function and true if found, nil and false otherwise.
// Returns the function and true if found, nil, and false otherwise.
// This method is thread-safe and can be called concurrently.
func (r *ProxyRegistry) Get(id uint64) (any, bool) {
if id == 0 {
Expand Down Expand Up @@ -112,6 +112,46 @@ type JsFunctionProxy = func(
argv uint32,
) (rs uint64)

// ModuleLoaderFunc is a Go function that loads JavaScript modules.
// It receives the module name and should return the module's source code as a string,
// or an error if the module cannot be loaded.
//
// Return values:
// - (source, nil): The module source code will be compiled and loaded
// - ("", error): The error will be thrown as a JavaScript exception
// - ("", nil): Falls back to the default file system loader
//
// Example:
//
// func(ctx *Context, moduleName string) (string, error) {
// if source, ok := myModules[moduleName]; ok {
// return source, nil
// }
// return "", nil // Fall back to default loader
// }
type ModuleLoaderFunc func(ctx *Context, moduleName string) (string, error)

// JsModuleLoaderProxy is the Go host function for module loading that will be imported by the WASM module.
// It corresponds to the following C declaration:
//
// __attribute__((import_module("env"), import_name("jsModuleLoaderProxy")))
// extern uint64_t jsModuleLoaderProxy(uint32_t ctx, uint32_t module_name, uint64_t callback_id);
//
// Parameters:
// - jsCtx: JSContext pointer (as uint32)
// - moduleNamePtr: pointer to module name string in WASM memory
// - callbackID: ID of the registered Go callback function
//
// Returns:
// - JSModuleDef pointer as uint64 (or 0 on error)
type JsModuleLoaderProxy = func(
ctx context.Context,
module api.Module,
jsCtx uint32,
moduleNamePtr uint32,
callbackID uint64,
) uint64

// createFuncProxyWithRegistry creates a WASM function proxy that bridges JavaScript function calls to Go functions.
// It handles parameter extraction, error recovery, and result conversion between JS and Go.
func createFuncProxyWithRegistry(registry *ProxyRegistry) JsFunctionProxy {
Expand Down Expand Up @@ -255,3 +295,92 @@ func readArgsFromWasmMem(mem api.Memory, argc uint32, argv uint32) []uint64 {

return args
}

// createModuleLoaderProxyWithRegistry creates a WASM module loader proxy that bridges QuickJS module loading to Go
// functions.
func createModuleLoaderProxyWithRegistry(registry *ProxyRegistry, runtime *Runtime) JsModuleLoaderProxy {
return func(
_ context.Context,
module api.Module,
_ uint32,
moduleNamePtr uint32,
callbackID uint64,
) uint64 {
// Read the module name from WASM memory
moduleName := readStringFromWasmMem(module.Memory(), moduleNamePtr)

// Get the Go callback function
fn, ok := registry.Get(callbackID)
if !ok {
// No callback registered - return 0 (NULL)
return 0
}

moduleLoader, ok := fn.(ModuleLoaderFunc)
if !ok {
// Wrong type - return 0 (NULL)
return 0
}

// Get the context
ctx := runtime.Context()

// Call the Go module loader function
defer func() {
if r := recover(); r != nil {
// Handle panic by setting an exception
ctx.ThrowError(AnyToError(r))
}
}()

source, err := moduleLoader(ctx, moduleName)
if err != nil {
// Throw error as JavaScript exception
ctx.ThrowError(err)

return 0
}

if source == "" {
// Empty source - fall back to default file system loader
return ctx.defaultModuleLoader(moduleName)
}

// Compile and load the module source code
value, err := ctx.Load(moduleName, Code(source), TypeModule())
if err != nil {
// Compilation/loading failed - throw error
ctx.ThrowError(err)

return 0
}

// Return JSModuleDef pointer as uint64
return value.Raw()
}
}

// readStringFromWasmMem reads a null-terminated C string from WASM memory.
func readStringFromWasmMem(mem api.Memory, ptr uint32) string {
if ptr == 0 {
return ""
}

// Read bytes until we hit a null terminator
var bytes []byte

offset := ptr

for {
b, ok := mem.ReadByte(offset)
if !ok || b == 0 {
break
}

bytes = append(bytes, b)

offset++
}

return string(bytes)
}
Binary file modified qjs.wasm
Binary file not shown.
38 changes: 38 additions & 0 deletions qjswasm/module_loader.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "qjs.h"

#ifdef __wasm__
// When compiling for WASM, declare the imported host function.
// The function is imported from the "env" module under the name "jsModuleLoaderProxy".
__attribute__((import_module("env"), import_name("jsModuleLoaderProxy"))) extern uint64_t jsModuleLoaderProxy(uint32_t ctx, uint32_t module_name, uint64_t callback_id);
#endif

// The actual module loader function that will be called by QuickJS
// The callback_id is passed via the opaque pointer
JSModuleDef *GoModuleLoaderProxy(JSContext *ctx, const char *module_name, void *opaque)
{
#ifdef __wasm__
// Extract the callback_id from the opaque pointer
uint64_t callback_id = (uint64_t)(uintptr_t)opaque;

if (callback_id == 0)
{
JS_ThrowInternalError(ctx, "Module loader callback not set");
return NULL;
}

// Call the Go callback through the imported host function
// The Go function compiles and loads the module, returning JSModuleDef* as uint64
uint64_t result = jsModuleLoaderProxy((uint32_t)(uintptr_t)ctx, (uint32_t)(uintptr_t)module_name, callback_id);

return (JSModuleDef *)(uintptr_t)result;
#else
JS_ThrowInternalError(ctx, "Module loader proxy not implemented for native builds");
return NULL;
#endif
}

void QJS_SetModuleLoaderCallback(QJSRuntime *qjs, uint64_t callback_id)
{
// Pass the callback_id as the opaque pointer - this is the idiomatic C pattern
JS_SetModuleLoaderFunc(qjs->runtime, NULL, GoModuleLoaderProxy, (void *)(uintptr_t)callback_id);
}
1 change: 1 addition & 0 deletions qjswasm/qjs.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,6 @@ JSValue QJS_NewArrayBufferCopy(JSContext *ctx, uint64_t addr, uint64_t len);
JSValue QJS_Call(JSContext *ctx, JSValue func, JSValue this, int argc, uint64_t argv);
JSValue QJS_NewProxyValue(JSContext *ctx, int64_t proxyId);
QJSRuntime *QJS_GetRuntime();
void QJS_SetModuleLoaderCallback(QJSRuntime *qjs, uint64_t callback_id);

void initialize();
2 changes: 2 additions & 0 deletions qjswasm/qjswasm.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ add_executable(qjswasm
../helpers.c
../proxy.c
../qjs.c
../module_loader.c
)

add_qjs_libc_if_needed(qjswasm)
Expand Down Expand Up @@ -109,6 +110,7 @@ target_link_options(qjswasm PRIVATE
"LINKER:--export=QJS_ThrowInternalError"

"LINKER:--export=QJS_ModuleLoader"
"LINKER:--export=QJS_SetModuleLoaderCallback"
"LINKER:--export=QJS_Load"
"LINKER:--export=QJS_Eval"
"LINKER:--export=QJS_Compile"
Expand Down
51 changes: 51 additions & 0 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,16 @@ func New(options ...Option) (runtime *Runtime, err error) {
return nil, fmt.Errorf("failed to instantiate WASI: %w", err)
}

// Create module loader proxy with registry and runtime
moduleLoaderProxy := createModuleLoaderProxyWithRegistry(proxyRegistry, runtime)

if _, err := runtime.wrt.NewHostModuleBuilder("env").
NewFunctionBuilder().
WithFunc(option.ProxyFunction).
Export("jsFunctionProxy").
NewFunctionBuilder().
WithFunc(moduleLoaderProxy).
Export("jsModuleLoaderProxy").
Instantiate(option.Context); err != nil {
return nil, fmt.Errorf("failed to setup host module: %w", err)
}
Expand Down Expand Up @@ -235,6 +241,51 @@ func (r *Runtime) Context() *Context {
return r.context
}

// SetModuleLoaderFunc sets a custom module loader function for this runtime.
// The provided Go function will be called whenever JavaScript code imports a module.
//
// The module loader receives the module name and should return:
// - (source, nil): Module source code to compile and load
// - ("", error): Error to throw as JavaScript exception
// - ("", nil): Fall back to default file system loader
//
// Example - Load modules from memory:
//
// modules := map[string]string{
// "utils": "export const add = (a, b) => a + b;",
// "config": "export const API_URL = 'https://api.example.com';",
// }
//
// runtime.SetModuleLoaderFunc(func(ctx *qjs.Context, moduleName string) (string, error) {
// if source, ok := modules[moduleName]; ok {
// return source, nil // Return source to compile
// }
// return "", nil // Fall back to file system
// })
//
// Example - Logging with delegation:
//
// runtime.SetModuleLoaderFunc(func(ctx *qjs.Context, moduleName string) (string, error) {
// fmt.Printf("Loading: %s\n", moduleName)
// return "", nil // Delegate to default loader
// })
//
// Example - Access control:
//
// runtime.SetModuleLoaderFunc(func(ctx *qjs.Context, moduleName string) (string, error) {
// if !isAllowed(moduleName) {
// return "", fmt.Errorf("module '%s' not allowed", moduleName)
// }
// return "", nil // Load from file system
// })
func (r *Runtime) SetModuleLoaderFunc(loaderFunc ModuleLoaderFunc) {
// Register the Go function in the proxy registry
callbackID := r.registry.Register(loaderFunc)

// Set the module loader callback with the callback ID
r.call("QJS_SetModuleLoaderCallback", r.handle.raw, callbackID)
}

// Call invokes a WebAssembly function by name with the given arguments.
func (r *Runtime) Call(name string, args ...uint64) *Handle {
return NewHandle(r, r.call(name, args...))
Expand Down
Loading
Loading