Skip to content
Open
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
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:**
Expand Down
38 changes: 21 additions & 17 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package qjs
import (
"context"
"fmt"
"io"
"os"

"github.com/tetratelabs/wazero"
)

const (
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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()
}
Expand All @@ -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
Expand Down
31 changes: 24 additions & 7 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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)
})
})

Expand Down
12 changes: 1 addition & 11 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
15 changes: 14 additions & 1 deletion runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
})

Expand Down