diff --git a/README.md b/README.md index 8e3c784..04c7da9 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ func main() { // Precompile the script to bytecode byteCode := must(ctx.Compile("script.js", qjs.Code(script), qjs.TypeModule())) // Use a pool of runtimes for concurrent requests - pool := qjs.NewPool(3, &qjs.Option{}, func(r *qjs.Runtime) error { + pool := qjs.NewPool(3, qjs.Option{}, func(r *qjs.Runtime) error { results := must(r.Context().Eval("script.js", qjs.Bytecode(byteCode), qjs.TypeModule())) // Store the exported functions in the global object for easy access r.Context().Global().SetPropertyStr("handlers", results) @@ -591,7 +591,7 @@ func main() { return nil } // Create a pool with 3 runtimes - pool := qjs.NewPool(3, &qjs.Option{}, setupFunc) + pool := qjs.NewPool(3, qjs.Option{}, setupFunc) numWorkers := 5 numTasks := 3 var wg sync.WaitGroup @@ -714,15 +714,20 @@ qjs.ToJSValue(ctx, goValue) // Convert Go value to JS (auto-detects ```go type Option struct { - CWD string // Working directory - MaxStackSize int // Stack size limit - MemoryLimit int // Memory usage limit - MaxExecutionTime int // Execution timeout - GCThreshold int // GC trigger threshold - CacheDir string // Compilation cache directory + ModuleConfig wazero.ModuleConfig // Wazero module setup (FS, stdio, start functions) + MaxStackSize int // Stack size limit + MemoryLimit int // Memory usage limit + MaxExecutionTime int // Execution timeout + GCThreshold int // GC trigger threshold + CacheDir string // Compilation cache directory } ``` +When `ModuleConfig` is nil, `qjs` creates a default config that mounts the current working +directory at `/`, enables walltime/nanotime/nanosleep, and wires stdout/stderr to the host. +When you provide `ModuleConfig` yourself, include `WithStartFunctions()` to clear wazero's +default `_start` entry unless you explicitly want start functions to run at instantiation. + ## Performance & Security **Optimization Tips:** diff --git a/options.go b/options.go index df515c3..e0642e8 100644 --- a/options.go +++ b/options.go @@ -3,8 +3,9 @@ package qjs import ( "context" "fmt" - "io" "os" + + "github.com/tetratelabs/wazero" ) const ( @@ -31,9 +32,7 @@ const ( ) type Option struct { - CWD string - StartFunctionName string - Context context.Context + Context context.Context // Enabling this option significantly increases evaluation time // because every operation must check the done context, which introduces additional overhead. CloseOnContextDone bool @@ -45,8 +44,7 @@ type Option struct { GCThreshold int QuickJSWasmBytes []byte ProxyFunction any - Stdout io.Writer - Stderr io.Writer + ModuleConfig wazero.ModuleConfig } // EvalOption configures JavaScript evaluation behavior in QuickJS context. @@ -221,12 +219,6 @@ func getRuntimeOption(registry *ProxyRegistry, options ...Option) (option Option option = options[0] } - if option.CWD == "" { - if option.CWD, err = os.Getwd(); err != nil { - return Option{}, fmt.Errorf("cannot get current working directory: %w", err) - } - } - if option.Context == nil { option.Context = context.Background() } @@ -235,12 +227,24 @@ func getRuntimeOption(registry *ProxyRegistry, options ...Option) (option Option option.ProxyFunction = createFuncProxyWithRegistry(registry) } - if option.Stdout == nil { - option.Stdout = os.Stdout - } + if option.ModuleConfig == nil { + var cwd string + if cwd, err = os.Getwd(); err != nil { + return Option{}, fmt.Errorf("cannot get current working directory: %w", err) + } - if option.Stderr == nil { - option.Stderr = os.Stderr + fsConfig := wazero. + NewFSConfig(). + WithDirMount(cwd, "/") + + option.ModuleConfig = wazero.NewModuleConfig(). + WithStartFunctions(). + WithSysWalltime(). + WithSysNanotime(). + WithSysNanosleep(). + WithFSConfig(fsConfig). + WithStdout(os.Stdout). + WithStderr(os.Stderr) } return option, nil diff --git a/options_test.go b/options_test.go index edcf81e..1dccba5 100644 --- a/options_test.go +++ b/options_test.go @@ -8,6 +8,7 @@ import ( "github.com/fastschema/qjs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tetratelabs/wazero" ) func TestEvalOptions(t *testing.T) { @@ -22,7 +23,7 @@ func TestEvalOptions(t *testing.T) { val.Free() }) - t.Run("CWDOption", func(t *testing.T) { + t.Run("ModuleConfig", func(t *testing.T) { // Store original working directory to restore later originalCwd, err := os.Getwd() require.NoError(t, err) @@ -31,20 +32,29 @@ func TestEvalOptions(t *testing.T) { _ = os.Chdir(originalCwd) }) - t.Run("default_cwd_from_os_getwd", func(t *testing.T) { + t.Run("nil_uses_default_module_config", func(t *testing.T) { runtime, err := qjs.New() require.NoError(t, err) runtime.Close() }) - t.Run("explicit_cwd_provided", func(t *testing.T) { + t.Run("custom_fs_mount", func(t *testing.T) { tempDir := t.TempDir() - runtime, err := qjs.New(qjs.Option{CWD: tempDir}) + runtime, err := qjs.New(qjs.Option{ + ModuleConfig: wazero.NewModuleConfig(). + WithStartFunctions(). + WithSysWalltime(). + WithSysNanotime(). + WithSysNanosleep(). + WithFSConfig(wazero.NewFSConfig().WithDirMount(tempDir, "/")). + WithStdout(os.Stdout). + WithStderr(os.Stderr), + }) require.NoError(t, err) runtime.Close() }) - t.Run("deleted_working_directory", func(t *testing.T) { + t.Run("nil_default_after_deleted_working_directory", func(t *testing.T) { // Create a temporary directory and change to it tempDir := t.TempDir() subDir := filepath.Join(tempDir, "workdir") @@ -59,10 +69,17 @@ func TestEvalOptions(t *testing.T) { err = os.RemoveAll(subDir) require.NoError(t, err) + _, getwdErr := os.Getwd() _, err = qjs.New() _ = os.Chdir(originalCwd) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get runtime options") + + if getwdErr != nil { + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get runtime options") + return + } + + require.NoError(t, err) }) }) diff --git a/runtime.go b/runtime.go index f099bc3..efc2b63 100644 --- a/runtime.go +++ b/runtime.go @@ -132,20 +132,10 @@ func New(options ...Option) (runtime *Runtime, err error) { return nil, fmt.Errorf("failed to setup host module: %w", err) } - fsConfig := wazero. - NewFSConfig(). - WithDirMount(runtime.option.CWD, "/") if runtime.module, err = runtime.wrt.InstantiateModule( option.Context, compiledQJSModule, - wazero.NewModuleConfig(). - WithStartFunctions(option.StartFunctionName). - WithSysWalltime(). - WithSysNanotime(). - WithSysNanosleep(). - WithFSConfig(fsConfig). - WithStdout(option.Stdout). - WithStderr(option.Stderr), + option.ModuleConfig, ); err != nil { return nil, fmt.Errorf("failed to instantiate module: %w", err) } diff --git a/runtime_test.go b/runtime_test.go index 0c0a6bb..68fe353 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -11,6 +11,7 @@ import ( "github.com/fastschema/qjs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tetratelabs/wazero" ) func testConcurrentRuntimeExecution(t *testing.T, threadID int) { @@ -165,7 +166,19 @@ func TestRuntime(t *testing.T) { t.Run("RuntimeCreationWithErrorStartFunction", func(t *testing.T) { invalidStartFunc := "QJS_Panic" - _, err := qjs.New(qjs.Option{StartFunctionName: invalidStartFunc}) + cwd, getwdErr := os.Getwd() + require.NoError(t, getwdErr) + + _, err := qjs.New(qjs.Option{ + ModuleConfig: wazero.NewModuleConfig(). + WithStartFunctions(invalidStartFunc). + WithSysWalltime(). + WithSysNanotime(). + WithSysNanosleep(). + WithFSConfig(wazero.NewFSConfig().WithDirMount(cwd, "/")). + WithStdout(os.Stdout). + WithStderr(os.Stderr), + }) assert.Error(t, err, "Creating runtime with invalid start function should return error") })