├── testdata ├── 01_global │ ├── 11_empty_script.js │ ├── 07_result_null.js │ ├── 13_array.js │ ├── 04_result_number.js │ ├── 06_result_boolean.js │ ├── 01_invalid_syntax.js │ ├── 08_result_undefined.js │ ├── 05_result_string.js │ ├── 02_throw_error.js │ ├── 03_result_no_value.js │ ├── 10_top_level_await_not_supported.js │ ├── 12_function.js │ └── 09_result_object.js ├── 02_module │ ├── 08_export_empty │ │ └── 999_index.js │ ├── 06_import_export │ │ ├── 001_a.js │ │ ├── 002_b.js │ │ └── 999_index.js │ ├── 09_side_effect │ │ ├── 999_index.js │ │ └── 001_side.js │ ├── 01_invalid_syntax │ │ └── 999_index.js │ ├── 02_throw_error │ │ └── 999_index.js │ ├── 03_default_export_return_value │ │ └── 999_index.js │ ├── 05_import_not_exist │ │ └── 999_index.js │ ├── 04_without_default_export_return_undefined │ │ └── 999_index.js │ ├── 07_export_fn_call │ │ ├── 001_multiply.js │ │ └── 999_index.js │ └── 10_export │ │ └── index.js ├── 03_compile │ ├── mod_a.js │ ├── 02_module │ │ ├── lib_a.js │ │ └── index.js │ ├── index.js │ └── 01_global │ │ └── index.js ├── 00_loader │ ├── 04_autoextjs │ │ └── file.js │ ├── 05_autoextmjs │ │ └── file.mjs │ ├── 02_autoindexjs │ │ └── index.js │ ├── 03_autoindexmjs │ │ └── index.mjs │ └── 01_realname │ │ └── file.js └── 04_load │ ├── 03_mixed │ ├── mod_b.js │ ├── mod_e.js │ └── mod_c │ │ └── index.js │ └── 02_load_module_file.js ├── .gitignore ├── qjs.wasm ├── .gitmodules ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── docker.yml ├── go.mod ├── .goreleaser.yaml ├── test.sh ├── .codecov.yml ├── go.sum ├── LICENSE ├── Makefile ├── .golangci.yml ├── eval.go ├── qjswasm ├── qjs.c ├── function.c ├── proxy.c ├── qjs.h ├── qjswasm.cmake └── eval.c ├── errors.go ├── utils.go ├── handle.go ├── options.go ├── options_test.go ├── functojs.go ├── proxy.go ├── collection.go ├── errors_test.go ├── mem.go ├── utils_test.go ├── testutils_test.go ├── handle_test.go ├── runtime.go ├── context.go ├── eval_test.go └── gotojs.go /testdata/01_global/11_empty_script.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/01_global/07_result_null.js: -------------------------------------------------------------------------------- 1 | null; 2 | -------------------------------------------------------------------------------- /testdata/01_global/13_array.js: -------------------------------------------------------------------------------- 1 | [1, 2, 3]; 2 | -------------------------------------------------------------------------------- /testdata/01_global/04_result_number.js: -------------------------------------------------------------------------------- 1 | 1 + 2; 2 | -------------------------------------------------------------------------------- /testdata/01_global/06_result_boolean.js: -------------------------------------------------------------------------------- 1 | true; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .tool-versions 3 | releases 4 | -------------------------------------------------------------------------------- /testdata/01_global/01_invalid_syntax.js: -------------------------------------------------------------------------------- 1 | const a = ; 2 | -------------------------------------------------------------------------------- /testdata/01_global/08_result_undefined.js: -------------------------------------------------------------------------------- 1 | undefined; 2 | -------------------------------------------------------------------------------- /testdata/01_global/05_result_string.js: -------------------------------------------------------------------------------- 1 | 'Hello, World!'; 2 | -------------------------------------------------------------------------------- /testdata/02_module/08_export_empty/999_index.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /testdata/03_compile/mod_a.js: -------------------------------------------------------------------------------- 1 | export const getA = () => 'A'; 2 | -------------------------------------------------------------------------------- /qjs.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastschema/qjs/HEAD/qjs.wasm -------------------------------------------------------------------------------- /testdata/00_loader/04_autoextjs/file.js: -------------------------------------------------------------------------------- 1 | export default 'file.js'; 2 | -------------------------------------------------------------------------------- /testdata/00_loader/05_autoextmjs/file.mjs: -------------------------------------------------------------------------------- 1 | export default 'file.mjs'; 2 | -------------------------------------------------------------------------------- /testdata/01_global/02_throw_error.js: -------------------------------------------------------------------------------- 1 | throw new Error("script error"); 2 | -------------------------------------------------------------------------------- /testdata/02_module/06_import_export/001_a.js: -------------------------------------------------------------------------------- 1 | export const a = 1 + 2; 2 | -------------------------------------------------------------------------------- /testdata/02_module/09_side_effect/999_index.js: -------------------------------------------------------------------------------- 1 | import './001_side'; 2 | -------------------------------------------------------------------------------- /testdata/04_load/03_mixed/mod_b.js: -------------------------------------------------------------------------------- 1 | export const getB = () => 'B'; 2 | -------------------------------------------------------------------------------- /testdata/04_load/03_mixed/mod_e.js: -------------------------------------------------------------------------------- 1 | export const getE = () => 'E'; 2 | -------------------------------------------------------------------------------- /testdata/00_loader/02_autoindexjs/index.js: -------------------------------------------------------------------------------- 1 | export default 'autoindexjs'; 2 | -------------------------------------------------------------------------------- /testdata/01_global/03_result_no_value.js: -------------------------------------------------------------------------------- 1 | console.log('Hello, World!'); 2 | -------------------------------------------------------------------------------- /testdata/02_module/01_invalid_syntax/999_index.js: -------------------------------------------------------------------------------- 1 | export const a = 1 + (; 2 | -------------------------------------------------------------------------------- /testdata/04_load/03_mixed/mod_c/index.js: -------------------------------------------------------------------------------- 1 | export const getC = () => 'C'; 2 | -------------------------------------------------------------------------------- /testdata/00_loader/03_autoindexmjs/index.mjs: -------------------------------------------------------------------------------- 1 | export default 'autoindexmjs'; 2 | -------------------------------------------------------------------------------- /testdata/02_module/02_throw_error/999_index.js: -------------------------------------------------------------------------------- 1 | throw new Error("module error"); 2 | -------------------------------------------------------------------------------- /testdata/01_global/10_top_level_await_not_supported.js: -------------------------------------------------------------------------------- 1 | await Promise.resolve(1); 2 | -------------------------------------------------------------------------------- /testdata/01_global/12_function.js: -------------------------------------------------------------------------------- 1 | (function add(a, b) { 2 | return a + b; 3 | }); 4 | -------------------------------------------------------------------------------- /testdata/02_module/03_default_export_return_value/999_index.js: -------------------------------------------------------------------------------- 1 | export default 1+2; 2 | -------------------------------------------------------------------------------- /testdata/02_module/05_import_not_exist/999_index.js: -------------------------------------------------------------------------------- 1 | import { a } from 'non-exist'; 2 | -------------------------------------------------------------------------------- /testdata/01_global/09_result_object.js: -------------------------------------------------------------------------------- 1 | const obj = { a: 1, b: '2', c: true }; 2 | obj; 3 | -------------------------------------------------------------------------------- /testdata/03_compile/02_module/lib_a.js: -------------------------------------------------------------------------------- 1 | export const getMessageA = () => 'Hello from lib_a'; 2 | -------------------------------------------------------------------------------- /testdata/03_compile/index.js: -------------------------------------------------------------------------------- 1 | import { getA } from './mod_a'; 2 | 3 | export default getA(); 4 | -------------------------------------------------------------------------------- /testdata/00_loader/01_realname/file.js: -------------------------------------------------------------------------------- 1 | const message = 'Hello, World!'; 2 | export default message; 3 | -------------------------------------------------------------------------------- /testdata/02_module/04_without_default_export_return_undefined/999_index.js: -------------------------------------------------------------------------------- 1 | export const a = 1+2; 2 | -------------------------------------------------------------------------------- /testdata/03_compile/01_global/index.js: -------------------------------------------------------------------------------- 1 | const getMessage = () => 'Hello, World!'; 2 | getMessage(); 3 | -------------------------------------------------------------------------------- /testdata/04_load/02_load_module_file.js: -------------------------------------------------------------------------------- 1 | export const moduleExported = 'exported from module file'; 2 | -------------------------------------------------------------------------------- /testdata/02_module/06_import_export/002_b.js: -------------------------------------------------------------------------------- 1 | import { a } from './001_a.js'; 2 | export const b = a + 3; 3 | -------------------------------------------------------------------------------- /testdata/02_module/06_import_export/999_index.js: -------------------------------------------------------------------------------- 1 | import { b } from './002_b.js'; 2 | export default b; 3 | -------------------------------------------------------------------------------- /testdata/02_module/09_side_effect/001_side.js: -------------------------------------------------------------------------------- 1 | console.log('side effect executed'); 2 | export default 55; 3 | -------------------------------------------------------------------------------- /testdata/02_module/07_export_fn_call/001_multiply.js: -------------------------------------------------------------------------------- 1 | export function multiply(a, b) { 2 | return a * b; 3 | } 4 | -------------------------------------------------------------------------------- /testdata/03_compile/02_module/index.js: -------------------------------------------------------------------------------- 1 | import { getMessageA } from './lib_a'; 2 | 3 | export default getMessageA(); 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "qjswasm/quickjs"] 2 | path = qjswasm/quickjs 3 | url = https://github.com/quickjs-ng/quickjs.git 4 | -------------------------------------------------------------------------------- /testdata/02_module/07_export_fn_call/999_index.js: -------------------------------------------------------------------------------- 1 | import { multiply } from './001_multiply.js'; 2 | export default multiply(4, 5); 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /testdata/02_module/10_export/index.js: -------------------------------------------------------------------------------- 1 | export const sayHello = msg => { 2 | console.log(`[${msg}] JS says hello`); 3 | } 4 | 5 | export const sayHi = msg => { 6 | console.log(`[${msg}] JS says hi`); 7 | } 8 | 9 | export const init = () => { 10 | registerJsFunction('sayHello'); 11 | registerJsFunction('sayHi'); 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fastschema/qjs 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/stretchr/testify v1.11.1 7 | github.com/tetratelabs/wazero v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | 16 | exclude github.com/stretchr/testify v1.8.4 17 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: qjs 2 | dist: releases 3 | 4 | version: 1 5 | 6 | before: 7 | hooks: 8 | - go mod tidy 9 | 10 | builds: 11 | - skip: true 12 | 13 | archives: 14 | - id: build_main 15 | format: zip 16 | 17 | snapshot: 18 | name_template: "{{ incpatch .Version }}-next" 19 | 20 | changelog: 21 | sort: asc 22 | filters: 23 | exclude: 24 | - "^docs:" 25 | - "^test:" 26 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # go test -failfast -race -v -p 1 3 | scriptDir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | workingDir=$PWD 5 | mkdir -p $scriptDir/coverage 6 | coverageFile=$scriptDir/coverage/coverage 7 | gotestsum -f testname \ 8 | --debug \ 9 | -- \ 10 | ./... \ 11 | -v \ 12 | -failfast \ 13 | -race \ 14 | -count=1 \ 15 | -coverprofile=$coverageFile.out \ 16 | -covermode=atomic 17 | go tool cover -html=$coverageFile.out -o $coverageFile.html 18 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | # For more configuration details: 2 | # https://docs.codecov.io/docs/codecov-yaml 3 | 4 | # Validate 5 | # curl -X POST --data-binary @.codecov.yml https://codecov.io/validate 6 | 7 | coverage: 8 | status: 9 | project: off 10 | patch: off 11 | 12 | range: 70..90 13 | round: down 14 | precision: 2 15 | 16 | # Ignoring Paths 17 | ignore: 18 | - examples 19 | 20 | # Pull request comments: 21 | # ---------------------- 22 | # Diff is the Coverage Diff of the pull request. 23 | # Files are the files impacted by the pull request 24 | comment: 25 | layout: diff, files # accepted in any order: reach, diff, flags, and/or files 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= 8 | github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Nguyen Ngoc Phuong and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build build-debug clean 2 | 3 | build: 4 | @echo "Configuring and building qjs..." 5 | cd qjswasm/quickjs && \ 6 | rm -rf build && \ 7 | cmake -B build \ 8 | -DQJS_BUILD_LIBC=ON \ 9 | -DQJS_BUILD_CLI_WITH_MIMALLOC=OFF \ 10 | -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake \ 11 | -DCMAKE_PROJECT_INCLUDE=../qjswasm.cmake 12 | @echo "Building qjs target..." 13 | make -C qjswasm/quickjs/build qjswasm -j$(nproc) 14 | @echo "Copying build/qjswasm to top-level as qjs.wasm..." 15 | cp qjswasm/quickjs/build/qjswasm qjs.wasm 16 | 17 | wasm-opt -O3 qjs.wasm -o qjs.wasm 18 | 19 | build-debug: 20 | @echo "Configuring and building qjs with runtime address debug..." 21 | cd qjswasm/quickjs && \ 22 | rm -rf build && \ 23 | cmake -B build \ 24 | -DQJS_BUILD_LIBC=ON \ 25 | -DQJS_BUILD_CLI_WITH_MIMALLOC=OFF \ 26 | -DQJS_DEBUG_RUNTIME_ADDRESS=ON \ 27 | -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake \ 28 | -DCMAKE_PROJECT_INCLUDE=../qjswasm.cmake 29 | @echo "Building qjs target..." 30 | make -C qjswasm/quickjs/build qjswasm -j$(nproc) 31 | @echo "Copying build/qjswasm to top-level as qjs.wasm..." 32 | cp qjswasm/quickjs/build/qjswasm qjs.wasm 33 | 34 | wasm-opt -O3 qjs.wasm -o qjs.wasm 35 | 36 | clean: 37 | @echo "Cleaning build directory..." 38 | cd quickjs && rm -rf build 39 | 40 | test: 41 | ./test.sh 42 | 43 | lint: 44 | golangci-lint run 45 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | run: 4 | concurrency: 4 5 | go: '1.22.0' 6 | 7 | linters: 8 | default: all 9 | disable: 10 | - containedctx 11 | - contextcheck 12 | - cyclop 13 | - depguard 14 | - err113 15 | - exhaustive 16 | - exhaustruct 17 | - gochecknoglobals 18 | - gosmopolitan 19 | - ireturn 20 | - mnd 21 | - nestif 22 | - nilnil 23 | - noinlineerr 24 | - nonamedreturns 25 | - unparam 26 | - varnamelen 27 | - wsl 28 | settings: 29 | dupl: 30 | threshold: 100 31 | funlen: 32 | lines: 150 33 | statements: 140 34 | gosec: 35 | excludes: 36 | - G115 37 | - G402 38 | staticcheck: 39 | checks: 40 | - all 41 | - -SA1029 42 | - -ST1003 43 | exclusions: 44 | generated: lax 45 | presets: 46 | - comments 47 | - common-false-positives 48 | - legacy 49 | - std-error-handling 50 | rules: 51 | - linters: 52 | - dupl 53 | - funlen 54 | - gocritic 55 | - gosec 56 | path: _test\.go 57 | - linters: 58 | - gocognit 59 | - gocyclo 60 | path: common.go 61 | paths: 62 | - examples$ 63 | - _test\.go 64 | 65 | formatters: 66 | enable: 67 | - gofmt 68 | exclusions: 69 | generated: lax 70 | paths: 71 | - third_party$ 72 | - builtin$ 73 | - examples$ 74 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | paths-ignore: 5 | - 'examples/**' 6 | tags-ignore: 7 | - '*.*' 8 | pull_request: 9 | paths-ignore: 10 | - 'examples/**' 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: '1.24' 20 | - name: Run linters 21 | uses: golangci/golangci-lint-action@v8 22 | with: 23 | args: --verbose 24 | 25 | test: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | go: ['1.22.x', '1.23.x', '1.24.x', '1.25.x'] 30 | platform: [ubuntu-latest, windows-latest, macos-latest, macos-14] 31 | steps: 32 | - uses: actions/checkout@v5 33 | - uses: actions/setup-go@v5 34 | with: 35 | go-version: ${{ matrix.go }} 36 | - uses: actions/cache@v4 37 | with: 38 | path: ~/go/pkg/mod 39 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 40 | restore-keys: | 41 | ${{ runner.os }}-go- 42 | - name: Install gotestsum 43 | run: go install gotest.tools/gotestsum@v1.12.3 44 | - name: Test 45 | run: gotestsum -f testname -- ./... -race -count=1 -coverprofile=coverage.txt -covermode=atomic 46 | - name: Upload coverage reports to Codecov 47 | if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go == '1.24.x' }} 48 | uses: codecov/codecov-action@v5.5.1 49 | with: 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | file: ./coverage.txt 52 | flags: unittests 53 | slug: fastschema/qjs 54 | -------------------------------------------------------------------------------- /eval.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | func load(c *Context, file string, flags ...EvalOptionFunc) (*Value, error) { 4 | if file == "" { 5 | return nil, ErrInvalidFileName 6 | } 7 | 8 | // Module: Force TypeModule() since load only works with modules 9 | flags = append(flags, TypeModule()) 10 | option := createEvalOption(c, file, flags...) 11 | 12 | evalOptions := option.Handle() 13 | 14 | defer option.Free() 15 | 16 | result := c.Call("QJS_Load", c.Raw(), evalOptions) 17 | 18 | return normalizeJsValue(c, result) 19 | } 20 | 21 | func eval(c *Context, file string, flags ...EvalOptionFunc) (*Value, error) { 22 | if file == "" { 23 | return nil, ErrInvalidFileName 24 | } 25 | 26 | option := createEvalOption(c, file, flags...) 27 | 28 | evalOptions := option.Handle() 29 | defer option.Free() 30 | 31 | result := c.Call("QJS_Eval", c.Raw(), evalOptions) 32 | 33 | return normalizeJsValue(c, result) 34 | } 35 | 36 | func compile(c *Context, file string, flags ...EvalOptionFunc) (_ []byte, err error) { 37 | option := createEvalOption(c, file, flags...) 38 | 39 | evalOptions := option.Handle() 40 | defer option.Free() 41 | 42 | result := c.Call("QJS_Compile2", c.Raw(), evalOptions) 43 | if result, err = normalizeJsValue(c, result); err != nil { 44 | return nil, err 45 | } 46 | 47 | defer result.Free() 48 | 49 | bytecodeBytes := result.Bytes() 50 | 51 | // Bytecode: Create independent copy to avoid memory corruption 52 | bytes := make([]byte, len(bytecodeBytes)) 53 | copy(bytes, bytecodeBytes) 54 | 55 | return bytes, nil 56 | } 57 | 58 | func normalizeJsValue(c *Context, value *Value) (*Value, error) { 59 | hasException := c.HasException() 60 | if hasException { 61 | value.Free() 62 | 63 | return nil, c.Exception() 64 | } 65 | 66 | if value.IsError() { 67 | defer value.Free() 68 | 69 | return nil, value.Exception() 70 | } 71 | 72 | return value, nil 73 | } 74 | -------------------------------------------------------------------------------- /qjswasm/qjs.c: -------------------------------------------------------------------------------- 1 | #include "qjs.h" 2 | 3 | JSContext *New_QJSContext(JSRuntime *rt) 4 | { 5 | JSContext *ctx; 6 | ctx = JS_NewContext(rt); 7 | js_init_module_std(ctx, "qjs:std"); 8 | js_init_module_os(ctx, "qjs:os"); 9 | js_init_module_bjson(ctx, "qjs:bjson"); 10 | js_set_global_objs(ctx); 11 | 12 | return ctx; 13 | } 14 | 15 | QJSRuntime *New_QJS( 16 | size_t memory_limit, 17 | size_t max_stack_size, 18 | size_t max_execution_time, 19 | size_t gc_threshold) 20 | { 21 | JSRuntime *runtime; 22 | JSContext *ctx; 23 | 24 | #ifdef QJS_DEBUG_RUNTIME_ADDRESS 25 | randomize_address_space(); 26 | #endif 27 | 28 | runtime = JS_NewRuntime(); 29 | 30 | if (!runtime) 31 | return NULL; 32 | 33 | if (memory_limit > 0) 34 | JS_SetMemoryLimit(runtime, memory_limit); 35 | 36 | if (gc_threshold > 0) 37 | JS_SetGCThreshold(runtime, gc_threshold); 38 | 39 | if (max_stack_size > 0) 40 | JS_SetMaxStackSize(runtime, max_stack_size); 41 | 42 | /* setup the the worker context */ 43 | js_std_set_worker_new_context_func(New_QJSContext); 44 | /* initialize the standard objects */ 45 | js_std_init_handlers(runtime); 46 | /* loader for ES6 modules */ 47 | JS_SetModuleLoaderFunc(runtime, NULL, QJS_ModuleLoader, NULL); 48 | /* exit on unhandled promise rejections */ 49 | // JS_SetHostPromiseRejectionTracker(runtime, js_std_promise_rejection_tracker, NULL); 50 | 51 | ctx = New_QJSContext(runtime); 52 | if (!ctx) 53 | { 54 | JS_FreeRuntime(runtime); 55 | return NULL; 56 | } 57 | 58 | // Initialize QJS_PROXY_VALUE class 59 | if (init_qjs_proxy_value_class(ctx) < 0) 60 | { 61 | JS_FreeContext(ctx); 62 | JS_FreeRuntime(runtime); 63 | return NULL; 64 | } 65 | 66 | QJSRuntime *qjs = (QJSRuntime *)malloc(sizeof(QJSRuntime)); 67 | if (!qjs) 68 | { 69 | JS_FreeContext(ctx); 70 | JS_FreeRuntime(runtime); 71 | return NULL; 72 | } 73 | 74 | qjs->runtime = runtime; 75 | qjs->context = ctx; 76 | 77 | return qjs; 78 | } 79 | 80 | void QJS_FreeValue(JSContext *ctx, JSValue val) 81 | { 82 | JS_FreeValue(ctx, val); 83 | } 84 | 85 | void QJS_Free(QJSRuntime *qjs) 86 | { 87 | JS_FreeContext(qjs->context); 88 | JS_FreeRuntime(qjs->runtime); 89 | free(qjs); 90 | } 91 | 92 | JSValue QJS_CloneValue(JSContext *ctx, JSValue val) 93 | { 94 | return JS_DupValue(ctx, val); 95 | } 96 | 97 | JSContext *QJS_GetContext(QJSRuntime *qjs) 98 | { 99 | return qjs->context; 100 | } 101 | 102 | void QJS_UpdateStackTop(QJSRuntime *qjs) 103 | { 104 | JS_UpdateStackTop(qjs->runtime); 105 | } 106 | 107 | QJSRuntime *qjs = NULL; 108 | 109 | QJSRuntime *QJS_GetRuntime() 110 | { 111 | return qjs; 112 | } 113 | 114 | void initialize() 115 | { 116 | if (qjs != NULL) 117 | return; 118 | size_t memory_limit = 0; 119 | size_t gc_threshold = 0; 120 | size_t max_stack_size = 0; 121 | qjs = New_QJS(memory_limit, max_stack_size, 0, gc_threshold); 122 | } 123 | -------------------------------------------------------------------------------- /qjswasm/function.c: -------------------------------------------------------------------------------- 1 | #include "qjs.h" 2 | 3 | #ifdef __wasm__ 4 | // When compiling for WASM, declare the imported host function. 5 | // The function is imported from the "env" module under the name "jsFunctionProxy". 6 | __attribute__((import_module("env"), import_name("jsFunctionProxy"))) extern JSValue jsFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 7 | #endif 8 | 9 | JSValue InvokeFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) 10 | { 11 | #ifdef __wasm__ 12 | // In WASM, call the imported host function. 13 | // Validate that we have the expected number of arguments 14 | if (argc < 4) 15 | { 16 | return JS_ThrowInternalError(ctx, "InvokeFunctionProxy: insufficient arguments"); 17 | } 18 | return jsFunctionProxy(ctx, this_val, argc, argv); 19 | #else 20 | // For native builds, throw an internal error. 21 | return JS_ThrowInternalError(ctx, "proxy not implemented"); 22 | #endif 23 | } 24 | 25 | JSValue QJS_CreateFunctionProxy(JSContext *ctx, uint64_t func_id, uint64_t ctx_id, uint64_t is_async) 26 | { 27 | // Create the C function binding that will be our proxy handler 28 | JSValue proxy = JS_NewCFunction( 29 | ctx, 30 | InvokeFunctionProxy, 31 | "proxyHandler", // for debugging purposes 32 | 0); 33 | 34 | if (JS_IsException(proxy)) 35 | return proxy; 36 | 37 | // Verify that the proxy is actually a function 38 | if (!JS_IsFunction(ctx, proxy)) 39 | { 40 | JS_FreeValue(ctx, proxy); 41 | return JS_ThrowInternalError(ctx, "failed to create proxy function"); 42 | } 43 | 44 | // This JavaScript creates a function that captures our proxy handler and context/handler IDs 45 | // When called, it invokes the proxy handler with the original 'this' context and arguments 46 | const char *proxy_content = ""; 47 | 48 | if (is_async == 0) 49 | { 50 | proxy_content = "(proxy, fnHandler, ctx, is_async) => function QJS_FunctionProxy (...args) { " 51 | " if (typeof proxy !== 'function') throw new TypeError('proxy is not a function'); " 52 | " return proxy.call(this, fnHandler, ctx, is_async, undefined, ...args); " 53 | "}"; 54 | } 55 | else 56 | { 57 | proxy_content = "(proxy, fnHandler, ctx, is_async) => async function QJS_AsyncFunctionProxy (...args) {" 58 | " if (typeof proxy !== 'function') throw new TypeError('proxy is not a function'); " 59 | " let resolve, reject;" 60 | " const promise = new Promise((resolve_, reject_) => {" 61 | " resolve = resolve_;" 62 | " reject = reject_;" 63 | " });" 64 | " promise.resolve = resolve;" 65 | " promise.reject = reject;" 66 | "" 67 | " proxy.call(this, fnHandler, ctx, is_async, promise, ...args);" 68 | " return await promise;" 69 | "}"; 70 | } 71 | 72 | JSValue proxy_func = JS_Eval( 73 | ctx, 74 | proxy_content, 75 | strlen(proxy_content), 76 | "", 77 | JS_EVAL_TYPE_GLOBAL); 78 | if (JS_IsException(proxy_func)) 79 | { 80 | JS_FreeValue(ctx, proxy); 81 | return proxy_func; 82 | } 83 | 84 | // Create arguments array for the call 85 | JSValue arg1 = JS_NewInt64(ctx, func_id); 86 | JSValue arg2 = JS_NewInt64(ctx, ctx_id); 87 | JSValue arg3 = JS_NewInt64(ctx, is_async); 88 | JSValueConst args[4] = {proxy, arg1, arg2, arg3}; 89 | 90 | // Use JS_UNDEFINED for the 'this' context 91 | JSValue result = JS_Call(ctx, proxy_func, JS_UNDEFINED, 4, args); 92 | 93 | // Free all created values 94 | JS_FreeValue(ctx, arg1); 95 | JS_FreeValue(ctx, arg2); 96 | JS_FreeValue(ctx, arg3); 97 | JS_FreeValue(ctx, proxy_func); 98 | JS_FreeValue(ctx, proxy); 99 | 100 | return result; 101 | } 102 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build docker image on release 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: 'Version to release' 9 | required: true 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | buildVersion: ${{ steps.getBuildVersion.outputs.result }} 16 | lastPackageVersion: ${{ steps.getLastPackageVersion.outputs.result }} 17 | steps: 18 | - name: Get latest fastschema release 19 | id: getBuildVersion 20 | uses: actions/github-script@v8 21 | with: 22 | result-encoding: string 23 | script: | 24 | try { 25 | const excludes = ['draft', 'prerelease']; 26 | const { data } = await github.rest.repos.listReleases({ 27 | owner: 'fastschema', 28 | repo: 'fastschema' 29 | }); 30 | 31 | const releases = data.filter(release => excludes.every(e => !release[e])) 32 | const latest = releases[0]; 33 | 34 | return latest.tag_name.replace('v',''); 35 | } catch (e) { 36 | return '0.0.0'; 37 | } 38 | 39 | - name: Get latest package version 40 | id: getLastPackageVersion 41 | uses: actions/github-script@v8 42 | with: 43 | result-encoding: string 44 | script: | 45 | try { 46 | const { data } = await github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg({ 47 | org: context.repo.owner, 48 | package_type: 'container', 49 | package_name: 'fastschema', 50 | }); 51 | 52 | const latest = data.filter(p => p.metadata.container.tags.includes("latest"))[0]; 53 | 54 | return latest.metadata.container.tags.find(v => /^v?\d+\.\d+\.\d+$/.test(v)) || '0.0.0' 55 | } catch (e) { 56 | return '0.0.0'; 57 | } 58 | 59 | build: 60 | runs-on: ubuntu-latest 61 | needs: check 62 | if: (needs.check.outputs.buildVersion != needs.check.outputs.lastPackageVersion) || github.event_name == 'workflow_dispatch' 63 | steps: 64 | - name: Checkout code 65 | uses: actions/checkout@v5 66 | 67 | - name: Set up Docker Buildx 68 | uses: docker/setup-buildx-action@v3 69 | 70 | - name: Set up QEMU 71 | uses: docker/setup-qemu-action@v3 72 | 73 | - name: Login to Docker registry 74 | uses: docker/login-action@v3 75 | with: 76 | registry: ghcr.io 77 | username: ${{ github.repository_owner }} 78 | password: ${{ secrets.GITHUB_TOKEN }} 79 | 80 | - name: Get build version 81 | id: getBuildVersion 82 | run: echo "value=${{ github.event.inputs.version || needs.check.outputs.buildVersion }}" >> $GITHUB_OUTPUT 83 | 84 | - name: Docker meta 85 | id: meta 86 | uses: docker/metadata-action@v5 87 | with: 88 | images: ghcr.io/fastschema/fastschema 89 | tags: | 90 | type=semver,pattern={{ version }},value=${{ steps.getBuildVersion.outputs.value }} 91 | type=semver,pattern={{ major }}.{{ minor }},value=${{ steps.getBuildVersion.outputs.value }} 92 | type=semver,pattern={{ major }},value=${{ steps.getBuildVersion.outputs.value }} 93 | 94 | - name: Build and push Docker image 95 | uses: docker/build-push-action@v6 96 | with: 97 | context: . 98 | push: true 99 | provenance: true 100 | tags: ${{ steps.meta.outputs.tags }} 101 | labels: ${{ steps.meta.outputs.labels }} 102 | build-args: VERSION=${{ steps.getBuildVersion.outputs.value }} 103 | platforms: linux/amd64,linux/arm64,linux/arm/v7 104 | cache-from: type=gha 105 | cache-to: type=gha,mode=max 106 | -------------------------------------------------------------------------------- /qjswasm/proxy.c: -------------------------------------------------------------------------------- 1 | #include "qjs.h" 2 | 3 | // toString method for QJS_PROXY_VALUE class 4 | static JSValue qjs_proxy_value_toString(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) 5 | { 6 | JSValue proxy_id = JS_GetPropertyStr(ctx, this_val, "proxyId"); 7 | if (JS_IsException(proxy_id)) 8 | return proxy_id; 9 | 10 | const char *proxy_id_str = JS_ToCString(ctx, proxy_id); 11 | JS_FreeValue(ctx, proxy_id); 12 | 13 | if (!proxy_id_str) 14 | return JS_EXCEPTION; 15 | 16 | char buffer[256]; 17 | snprintf(buffer, sizeof(buffer), "[object QJS_PROXY_VALUE(proxyId: %s)]", proxy_id_str); 18 | JS_FreeCString(ctx, proxy_id_str); 19 | 20 | return JS_NewString(ctx, buffer); 21 | } 22 | 23 | // Constructor function for QJS_PROXY_VALUE class 24 | static JSValue qjs_proxy_value_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) 25 | { 26 | JSValue obj; 27 | JSValue proto; 28 | 29 | if (JS_IsUndefined(new_target)) 30 | { 31 | // Called as function, not constructor 32 | return JS_ThrowTypeError(ctx, "QJS_PROXY_VALUE must be called with new"); 33 | } 34 | 35 | // Get prototype from new_target 36 | proto = JS_GetPropertyStr(ctx, new_target, "prototype"); 37 | if (JS_IsException(proto)) 38 | return proto; 39 | 40 | // Create object with proper prototype 41 | obj = JS_NewObjectProto(ctx, proto); 42 | JS_FreeValue(ctx, proto); 43 | 44 | if (JS_IsException(obj)) 45 | return obj; 46 | 47 | // Set the proxyId property 48 | if (argc > 0) 49 | { 50 | if (JS_SetPropertyStr(ctx, obj, "proxyId", JS_DupValue(ctx, argv[0])) < 0) 51 | { 52 | JS_FreeValue(ctx, obj); 53 | return JS_EXCEPTION; 54 | } 55 | } 56 | else 57 | { 58 | if (JS_SetPropertyStr(ctx, obj, "proxyId", JS_UNDEFINED) < 0) 59 | { 60 | JS_FreeValue(ctx, obj); 61 | return JS_EXCEPTION; 62 | } 63 | } 64 | 65 | return obj; 66 | } 67 | 68 | // Initialize QJS_PROXY_VALUE class and add it to global object 69 | int init_qjs_proxy_value_class(JSContext *ctx) 70 | { 71 | JSValue global_obj = JS_GetGlobalObject(ctx); 72 | 73 | // Create prototype object with toString method 74 | JSValue proto = JS_NewObject(ctx); 75 | JSValue toString_func = JS_NewCFunction(ctx, qjs_proxy_value_toString, "toString", 0); 76 | if (JS_SetPropertyStr(ctx, proto, "toString", toString_func) < 0) 77 | { 78 | JS_FreeValue(ctx, proto); 79 | JS_FreeValue(ctx, global_obj); 80 | return -1; 81 | } 82 | 83 | // Create the constructor function 84 | JSValue ctor = JS_NewCFunction2(ctx, qjs_proxy_value_constructor, "QJS_PROXY_VALUE", 1, JS_CFUNC_constructor, 0); 85 | 86 | // Set proto.constructor and ctor.prototype using QuickJS helper 87 | JS_SetConstructor(ctx, ctor, proto); 88 | 89 | // Add the constructor to the global object 90 | if (JS_SetPropertyStr(ctx, global_obj, "QJS_PROXY_VALUE", ctor) < 0) 91 | { 92 | JS_FreeValue(ctx, global_obj); 93 | return -1; 94 | } 95 | 96 | JS_FreeValue(ctx, global_obj); 97 | return 0; 98 | } 99 | 100 | // Create a new QJS_PROXY_VALUE instance directly in C for better performance 101 | JSValue QJS_NewProxyValue(JSContext *ctx, int64_t proxyId) 102 | { 103 | // Get the QJS_PROXY_VALUE constructor from global object 104 | JSValue global_obj = JS_GetGlobalObject(ctx); 105 | JSValue ctor = JS_GetPropertyStr(ctx, global_obj, "QJS_PROXY_VALUE"); 106 | JS_FreeValue(ctx, global_obj); 107 | 108 | if (JS_IsException(ctor) || JS_IsUndefined(ctor)) 109 | { 110 | JS_FreeValue(ctx, ctor); 111 | return JS_ThrowReferenceError(ctx, "QJS_PROXY_VALUE is not defined"); 112 | } 113 | 114 | // Create argument for the constructor (proxyId) 115 | JSValue arg = JS_NewInt64(ctx, proxyId); 116 | JSValue args[1] = {arg}; 117 | 118 | // Call the constructor with 'new' 119 | JSValue result = JS_CallConstructor(ctx, ctor, 1, args); 120 | 121 | // Clean up 122 | JS_FreeValue(ctx, ctor); 123 | JS_FreeValue(ctx, arg); 124 | 125 | return result; 126 | } 127 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "runtime/debug" 8 | ) 9 | 10 | var ( 11 | ErrRType = reflect.TypeOf((*error)(nil)).Elem() 12 | ErrZeroRValue = reflect.Zero(ErrRType) 13 | ErrCallFuncOnNonObject = errors.New("cannot call function on non-object") 14 | ErrNotAnObject = errors.New("value is not an object") 15 | ErrObjectNotAConstructor = errors.New("object not a constructor") 16 | ErrInvalidFileName = errors.New("file name is required") 17 | ErrMissingProperties = errors.New("value has no properties") 18 | ErrInvalidPointer = errors.New("null pointer dereference") 19 | ErrIndexOutOfRange = errors.New("index out of range") 20 | ErrNoNullTerminator = errors.New("no NUL terminator") 21 | ErrInvalidContext = errors.New("invalid context") 22 | ErrNotANumber = errors.New("js value is not a number") 23 | ErrAsyncFuncRequirePromise = errors.New("jsFunctionProxy: async function requires a promise") 24 | ErrEmptyStringToNumber = errors.New("empty string cannot be converted to number") 25 | ErrJsFuncDeallocated = errors.New("js function context has been deallocated") 26 | ErrNotByteArray = errors.New("invalid TypedArray: buffer is not a byte array") 27 | ErrNotArrayBuffer = errors.New("input is not an ArrayBuffer") 28 | ErrMissingBufferProperty = errors.New("invalid TypedArray: missing buffer property") 29 | ErrRuntimeClosed = errors.New("runtime is closed") 30 | ErrNilModule = errors.New("WASM module is nil") 31 | ErrNilHandle = errors.New("handle is nil") 32 | ErrChanClosed = errors.New("channel is closed") 33 | ErrChanSend = errors.New("channel send would block: buffer full or no receiver ready") 34 | ErrChanReceive = errors.New("channel receive would block: buffer empty or no sender ready") 35 | ErrChanCloseReceiveOnly = errors.New("cannot close receive-only channel") 36 | ) 37 | 38 | func combineErrors(errs ...error) error { 39 | if len(errs) == 0 { 40 | return nil 41 | } 42 | 43 | var errStr string 44 | 45 | for _, err := range errs { 46 | if err != nil { 47 | errStr += err.Error() + "\n" 48 | } 49 | } 50 | 51 | return errors.New(errStr) 52 | } 53 | 54 | func newMaxLengthExceededErr(request uint, maxLen int64, index int) error { 55 | return fmt.Errorf("length %d exceeds max %d at index %d", request, maxLen, index) 56 | } 57 | 58 | func newOverflowErr(value any, targetType string) error { 59 | return fmt.Errorf("value %v overflows %s", value, targetType) 60 | } 61 | 62 | func newGoToJsErr(kind string, err error, details ...string) error { 63 | detail := "" 64 | if len(details) > 0 { 65 | detail = " " + details[0] 66 | } 67 | 68 | if err == nil { 69 | return fmt.Errorf("cannot convert Go%s '%s' to JS", detail, kind) 70 | } 71 | 72 | return fmt.Errorf("cannot convert Go%s '%s' to JS: %w", detail, kind, err) 73 | } 74 | 75 | func newJsToGoErr(kind *Value, err error, details ...string) error { 76 | detail := "" 77 | if len(details) > 0 { 78 | detail = " " + details[0] 79 | } 80 | 81 | kindStr := "" 82 | 83 | var kindErr error 84 | 85 | if kind != nil { 86 | kindStr, kindErr = kind.JSONStringify() 87 | if kindErr != nil { 88 | kindStr = fmt.Errorf("(%w), %s", kindErr, kind.String()).Error() 89 | } 90 | } 91 | 92 | if kindStr == "undefined" || kindStr == "null" { 93 | kindStr = kind.Type() 94 | } 95 | 96 | if kindStr != "" { 97 | kindStr = " " + kindStr 98 | } 99 | 100 | if err == nil { 101 | return fmt.Errorf("cannot convert JS%s%s to Go", detail, kindStr) 102 | } 103 | 104 | return fmt.Errorf("cannot convert JS%s%s to Go: %w", detail, kindStr, err) 105 | } 106 | 107 | func newArgConversionErr(index int, err error) error { 108 | return fmt.Errorf("cannot convert JS function argument at index %d: %w", index, err) 109 | } 110 | 111 | func newInvalidGoTypeErr(expect string, got any) error { 112 | return fmt.Errorf("expected GO type %s, got %T", expect, got) 113 | } 114 | 115 | func newInvalidJsInputErr(kind string, input *Value) (err error) { 116 | var detail string 117 | if detail, err = input.JSONStringify(); err != nil { 118 | detail = fmt.Sprintf("(JSONStringify failed: %v), (.String()) %s", err, input.String()) 119 | } 120 | 121 | return fmt.Errorf("expected JS %s, got %s=%s", kind, input.Type(), detail) 122 | } 123 | 124 | func newJsStringifyErr(kind string, err error) error { 125 | return fmt.Errorf("js %s: %w", kind, err) 126 | } 127 | 128 | func newProxyErr(id uint64, r any) error { 129 | if err, ok := r.(error); ok { 130 | return fmt.Errorf("functionProxy [%d]: %w\n%s", id, err, debug.Stack()) 131 | } 132 | 133 | if str, ok := r.(string); ok { 134 | return fmt.Errorf("functionProxy [%d]: %s\n%s", id, str, debug.Stack()) 135 | } 136 | 137 | return fmt.Errorf("functionProxy [%d]: %v\n%s", id, r, debug.Stack()) 138 | } 139 | 140 | func newInvokeErr(input *Value, err error) error { 141 | return fmt.Errorf("cannot call getTime on JS value '%s', err=%w", input.String(), err) 142 | } 143 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | func Min(a, b int) int { 11 | if a < b { 12 | return a 13 | } 14 | 15 | return b 16 | } 17 | 18 | func IsImplementError(rtype reflect.Type) bool { 19 | return rtype.Implements(reflect.TypeOf((*error)(nil)).Elem()) 20 | } 21 | 22 | // IsImplementsJSONUnmarshaler checks if a type implements json.Unmarshaler. 23 | func IsImplementsJSONUnmarshaler(t reflect.Type) bool { 24 | unmarshalerType := reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() 25 | 26 | return t.Implements(unmarshalerType) || reflect.PointerTo(t).Implements(unmarshalerType) 27 | } 28 | 29 | // GetGoTypeName creates a descriptive string for complex types. 30 | func GetGoTypeName(input any) string { 31 | var t reflect.Type 32 | switch v := input.(type) { 33 | case reflect.Type: 34 | t = v 35 | default: 36 | t = reflect.TypeOf(v) 37 | } 38 | 39 | switch t.Kind() { 40 | case reflect.Ptr: 41 | return "*" + GetGoTypeName(t.Elem()) 42 | case reflect.Slice: 43 | return "[]" + GetGoTypeName(t.Elem()) 44 | case reflect.Array: 45 | return fmt.Sprintf("[%d]%s", t.Len(), GetGoTypeName(t.Elem())) 46 | case reflect.Map: 47 | return fmt.Sprintf("map[%s]%s", GetGoTypeName(t.Key()), GetGoTypeName(t.Elem())) 48 | case reflect.Chan: 49 | return "chan " + GetGoTypeName(t.Elem()) 50 | case reflect.Func: 51 | return CreateGoFuncSignature(t) 52 | default: 53 | return t.String() 54 | } 55 | } 56 | 57 | // CreateGoFuncSignature creates a readable string for function types. 58 | func CreateGoFuncSignature(fnType reflect.Type) string { 59 | parts := []string{"func("} 60 | params := []string{} 61 | 62 | for i := range fnType.NumIn() { 63 | paramType := fnType.In(i) 64 | if fnType.IsVariadic() && i == fnType.NumIn()-1 { 65 | params = append(params, "..."+GetGoTypeName(paramType.Elem())) 66 | } else { 67 | params = append(params, GetGoTypeName(paramType)) 68 | } 69 | } 70 | 71 | parts = append(parts, strings.Join(params, ", ")) 72 | parts = append(parts, ")") 73 | 74 | if fnType.NumOut() > 0 { 75 | parts = append(parts, " ") 76 | if fnType.NumOut() > 1 { 77 | parts = append(parts, "(") 78 | } 79 | 80 | returns := []string{} 81 | for i := range fnType.NumOut() { 82 | returns = append(returns, GetGoTypeName(fnType.Out(i))) 83 | } 84 | 85 | parts = append(parts, strings.Join(returns, ", ")) 86 | if fnType.NumOut() > 1 { 87 | parts = append(parts, ")") 88 | } 89 | } 90 | 91 | return strings.Join(parts, "") 92 | } 93 | 94 | // IsConvertibleToJs checks if a Go type can be converted to a JavaScript type. 95 | func IsConvertibleToJs(rType reflect.Type, visited map[reflect.Type]bool, detail string) (err error) { 96 | // Prevent infinite recursion for recursive types 97 | if visited[rType] { 98 | return nil 99 | } 100 | 101 | visited[rType] = true 102 | if rType.Kind() == reflect.Ptr { 103 | return IsConvertibleToJs(rType.Elem(), visited, detail) 104 | } 105 | 106 | switch rType.Kind() { 107 | case reflect.Chan: 108 | return nil 109 | case reflect.UnsafePointer: 110 | return newGoToJsErr("unsafe.Pointer", nil, detail) 111 | case reflect.Func: 112 | return nil 113 | case reflect.Slice: 114 | err := IsConvertibleToJs(rType.Elem(), visited, detail) 115 | if err != nil { 116 | return newGoToJsErr("slice: "+GetGoTypeName(rType.Elem()), nil, detail) 117 | } 118 | 119 | return nil 120 | case reflect.Array: 121 | err := IsConvertibleToJs(rType.Elem(), visited, detail) 122 | if err != nil { 123 | return newGoToJsErr("array: "+GetGoTypeName(rType.Elem()), nil, detail) 124 | } 125 | 126 | return nil 127 | case reflect.Map: 128 | keyType := rType.Key() 129 | 130 | err := IsConvertibleToJs(keyType, visited, detail) 131 | if err != nil { 132 | return newGoToJsErr("map key: "+GetGoTypeName(keyType), nil, detail) 133 | } 134 | 135 | valueType := rType.Elem() 136 | if err := IsConvertibleToJs(valueType, visited, detail); err != nil { 137 | return newGoToJsErr("map value: "+GetGoTypeName(valueType), nil, detail) 138 | } 139 | 140 | return nil 141 | case reflect.Struct: 142 | for i := range rType.NumField() { 143 | field := rType.Field(i) 144 | jsonTagName, _, _ := strings.Cut(field.Tag.Get("json"), ",") 145 | 146 | if !field.IsExported() || jsonTagName == "-" { 147 | continue 148 | } 149 | 150 | if err := IsConvertibleToJs(field.Type, visited, detail); err != nil { 151 | return newGoToJsErr(GetGoTypeName(rType)+"."+field.Name, err) 152 | } 153 | } 154 | 155 | return nil 156 | case reflect.Interface: 157 | return nil 158 | } 159 | 160 | return nil 161 | } 162 | 163 | // IsNumericType checks if a reflect.Type represents a numeric type. 164 | func IsNumericType(t reflect.Type) bool { 165 | if t.Kind() == reflect.Ptr { 166 | t = t.Elem() 167 | } 168 | 169 | switch t.Kind() { 170 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 171 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, 172 | reflect.Float32, reflect.Float64, 173 | reflect.Complex64, reflect.Complex128: 174 | return true 175 | default: 176 | return false 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /qjswasm/qjs.h: -------------------------------------------------------------------------------- 1 | #include "./quickjs/quickjs.h" 2 | #include "./quickjs/quickjs-libc.h" 3 | #include "./quickjs/cutils.h" 4 | #include 5 | 6 | JSValue JS_NewNull(); 7 | JSValue JS_NewUndefined(); 8 | JSValue JS_NewUninitialized(); 9 | JSValue JS_GetException(JSContext *ctx); 10 | JSValue QJS_ThrowSyntaxError(JSContext *ctx, const char *fmt); 11 | JSValue QJS_ThrowTypeError(JSContext *ctx, const char *fmt); 12 | JSValue QJS_ThrowReferenceError(JSContext *ctx, const char *fmt); 13 | JSValue QJS_ThrowRangeError(JSContext *ctx, const char *fmt); 14 | JSValue QJS_ThrowInternalError(JSContext *ctx, const char *fmt); 15 | int JS_DeletePropertyInt64(JSContext *ctx, JSValueConst obj, int64_t idx, int flags); 16 | 17 | /* Minimum and maximum values a `signed long int' can hold. (Same as `int'). */ 18 | #undef LONG_MIN 19 | #define LONG_MIN (-LONG_MAX - 1L) 20 | #undef LONG_MAX 21 | #define LONG_MAX __LONG_MAX__ 22 | 23 | #ifndef QJS_NATIVE_MODULE_SUFFIX 24 | #ifdef _WIN32 25 | #define QJS_NATIVE_MODULE_SUFFIX ".dll" 26 | #else 27 | #define QJS_NATIVE_MODULE_SUFFIX ".so" 28 | #endif 29 | #endif 30 | 31 | typedef struct TimeoutArgs 32 | { 33 | time_t start; 34 | time_t timeout; 35 | } TimeoutArgs; 36 | 37 | typedef struct 38 | { 39 | uintptr_t fn; 40 | } InterruptHandler; 41 | 42 | typedef struct QJSEvalOptions 43 | { 44 | const void *buf; 45 | const uint8_t *bytecode_buf; 46 | size_t bytecode_len; 47 | const char *filename; 48 | int eval_flags; 49 | } QJSEvalOptions; 50 | 51 | typedef struct QJSRuntime 52 | { 53 | JSRuntime *runtime; 54 | JSContext *context; 55 | } QJSRuntime; 56 | 57 | bool file_exists(const char *path); 58 | void remove_trailing_slashes(char *path); 59 | int js__has_suffix(const char *str, const char *suffix); 60 | char *append_suffix(const char *base, const char *suffix); 61 | bool input_is_file(QJSEvalOptions opts); 62 | void js_set_global_objs(JSContext *ctx); 63 | char *detect_entry_point(char *module_name); 64 | JSValue js_std_await(JSContext *ctx, JSValue obj); 65 | void QJS_Free(QJSRuntime *qjs); 66 | void QJS_FreeValue(JSContext *ctx, JSValue val); 67 | JSValue QJS_CloneValue(JSContext *ctx, JSValue val); 68 | JSContext *QJS_GetContext(QJSRuntime *qjs); 69 | JSRuntime *JS_GetRuntime(JSContext *ctx); 70 | 71 | JSValue InvokeFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 72 | JSValue InvokeAsyncFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 73 | // void SetInterruptHandler(JSRuntime *rt, void *handlerArgs); 74 | // void SetExecuteTimeout(JSRuntime *rt, time_t timeout); 75 | 76 | // Headers for GO exported functions 77 | // int goInterruptHandler(JSRuntime *rt, void *handler); 78 | // JSValue goFunctionProxy(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv); 79 | // JSValue goAsyncFunctionProxy(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv); 80 | 81 | JSValue jsFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 82 | #ifdef QJS_DEBUG_RUNTIME_ADDRESS 83 | void randomize_address_space(void); 84 | #endif 85 | 86 | int init_qjs_proxy_value_class(JSContext *ctx); 87 | 88 | JSContext *New_QJSContext(JSRuntime *rt); 89 | // QJSRuntime *New_QJS(QJSRuntimeOptions); 90 | QJSRuntime *New_QJS( 91 | size_t memory_limit, 92 | size_t max_stack_size, 93 | size_t max_execution_time, 94 | size_t gc_threshold); 95 | void QJS_UpdateStackTop(QJSRuntime *qjs); 96 | JSModuleDef *QJS_ModuleLoader(JSContext *ctx, const char *module_name, void *opaque); 97 | JSValue QJS_Load(JSContext *ctx, QJSEvalOptions opts); 98 | JSValue QJS_Eval(JSContext *ctx, QJSEvalOptions opts); 99 | unsigned char *QJS_Compile(JSContext *c, QJSEvalOptions opts, size_t *outSize); 100 | uint64_t *QJS_Compile2(JSContext *ctx, QJSEvalOptions opts); 101 | QJSEvalOptions *QJS_CreateEvalOption(void *buf, uint8_t *bytecode_buf, size_t bytecode_len, char *filename, int eval_flags); 102 | 103 | uint64_t *QJS_ToCString(JSContext *ctx, JSValueConst val); 104 | int64_t QJS_ToInt64(JSContext *ctx, JSValue val); 105 | int32_t QJS_ToInt32(JSContext *ctx, JSValue val); 106 | uint32_t QJS_ToUint32(JSContext *ctx, JSValue val); 107 | double QJS_ToFloat64(JSContext *ctx, JSValue val); 108 | double QJS_ToEpochTime(JSContext *ctx, JSValue val); 109 | bool QJS_IsUndefined(JSValue val); 110 | bool QJS_IsException(JSValue val); 111 | bool QJS_IsError(JSContext *ctx, JSValue val); 112 | bool QJS_IsPromise(JSContext *ctx, JSValue v); 113 | bool QJS_IsFunction(JSContext *ctx, JSValue v); 114 | bool QJS_IsObject(JSValue v); 115 | bool QJS_IsNumber(JSValue v); 116 | bool QJS_IsBigInt(JSValue v); 117 | bool QJS_IsBool(JSValue v); 118 | bool QJS_IsNull(JSValue v); 119 | bool QJS_IsUninitialized(JSValue val); 120 | bool QJS_IsString(JSValue val); 121 | bool QJS_IsSymbol(JSValue val); 122 | bool QJS_IsArray(JSValue v); 123 | bool QJS_IsConstructor(JSContext *ctx, JSValue v); 124 | bool QJS_IsInstanceOf(JSContext *ctx, JSValue v, JSValue obj); 125 | JSValue QJS_NewString(JSContext *ctx, const char *str); 126 | JSValue QJS_CreateFunctionProxy(JSContext *ctx, uint64_t handle_id, uint64_t ctx_id, uint64_t is_async); 127 | JSValue QJS_NewInt64(JSContext *ctx, int64_t val); 128 | JSValue QJS_NewUint32(JSContext *ctx, uint32_t val); 129 | JSValue QJS_NewInt32(JSContext *ctx, int32_t val); 130 | JSValue QJS_NewBigInt64(JSContext *ctx, int64_t val); 131 | JSValue QJS_NewBigUint64(JSContext *ctx, uint64_t val); 132 | JSValue QJS_NewFloat64(JSContext *ctx, uint64_t bits); 133 | uint64_t *QJS_AtomToCString(JSContext *ctx, JSAtom atom); 134 | 135 | uint64_t *QJS_GetOwnPropertyNames(JSContext *ctx, JSValue v); 136 | JSValue QJS_ParseJSON(JSContext *ctx, const char *buf); 137 | JSValue QJS_NewBool(JSContext *ctx, int val); 138 | uint64_t *QJS_GetArrayBuffer(JSContext *ctx, JSValue obj); 139 | uint64_t *QJS_JSONStringify(JSContext *ctx, JSValue v); 140 | int QJS_SetPropertyUint32(JSContext *ctx, JSValue this_obj, uint64_t idx, JSValue val); 141 | JSValue QJS_GetPropertyUint32(JSContext *ctx, JSValue this_obj, uint32_t idx); 142 | int QJS_NewAtomUInt32(JSContext *ctx, uint64_t n); 143 | JSValue QJS_NewArrayBufferCopy(JSContext *ctx, uint64_t addr, uint64_t len); 144 | JSValue QJS_Call(JSContext *ctx, JSValue func, JSValue this, int argc, uint64_t argv); 145 | JSValue QJS_NewProxyValue(JSContext *ctx, int64_t proxyId); 146 | QJSRuntime *QJS_GetRuntime(); 147 | 148 | void initialize(); 149 | -------------------------------------------------------------------------------- /qjswasm/qjswasm.cmake: -------------------------------------------------------------------------------- 1 | macro(add_qjs_libc_if_needed target) 2 | if(NOT QJS_BUILD_LIBC) 3 | target_sources(${target} PRIVATE quickjs-libc.c) 4 | endif() 5 | endmacro() 6 | macro(add_static_if_needed target) 7 | if(QJS_BUILD_CLI_STATIC OR MINGW) 8 | target_link_options(${target} PRIVATE -static) 9 | if(MINGW) 10 | target_link_options(${target} PRIVATE -static-libgcc) 11 | endif() 12 | endif() 13 | endmacro() 14 | 15 | if(CMAKE_SYSTEM_NAME STREQUAL "WASI") 16 | add_compile_definitions( 17 | _WASI_EMULATED_PROCESS_CLOCKS 18 | _WASI_EMULATED_SIGNAL 19 | ) 20 | add_link_options( 21 | -lwasi-emulated-process-clocks 22 | -lwasi-emulated-signal 23 | ) 24 | endif() 25 | 26 | # Optional debug flag for runtime address randomization 27 | option(QJS_DEBUG_RUNTIME_ADDRESS "Enable runtime address randomization debugging" OFF) 28 | if(QJS_DEBUG_RUNTIME_ADDRESS) 29 | add_compile_definitions(QJS_DEBUG_RUNTIME_ADDRESS) 30 | endif() 31 | 32 | if(NOT CMAKE_SYSTEM_NAME STREQUAL "WASI") 33 | list(APPEND qjs_libs ${CMAKE_THREAD_LIBS_INIT}) 34 | endif() 35 | 36 | add_executable(qjswasm 37 | # gen/repl.c 38 | # gen/standalone.c 39 | ../eval.c 40 | ../function.c 41 | ../helpers.c 42 | ../proxy.c 43 | ../qjs.c 44 | ) 45 | 46 | add_qjs_libc_if_needed(qjswasm) 47 | add_static_if_needed(qjswasm) 48 | 49 | set_target_properties(qjswasm PROPERTIES 50 | OUTPUT_NAME "qjswasm" 51 | ) 52 | 53 | target_link_options(qjswasm PRIVATE 54 | "LINKER:--export=js_std_await" 55 | "LINKER:--export=New_QJS" 56 | "LINKER:--export=New_QJSContext" 57 | "LINKER:--export=QJS_FreeValue" 58 | "LINKER:--export=QJS_Free" 59 | "LINKER:--export=QJS_CloneValue" 60 | "LINKER:--export=QJS_GetContext" 61 | "LINKER:--export=JS_FreeContext" 62 | "LINKER:--export=JS_NewDate" 63 | "LINKER:--export=JS_NewNull" 64 | "LINKER:--export=JS_NewUndefined" 65 | "LINKER:--export=JS_NewUninitialized" 66 | "LINKER:--export=QJS_NewString" 67 | "LINKER:--export=QJS_ToCString" 68 | "LINKER:--export=JS_FreeCString" 69 | "LINKER:--export=QJS_ToInt64" 70 | "LINKER:--export=QJS_ToInt32" 71 | "LINKER:--export=QJS_ToUint32" 72 | "LINKER:--export=QJS_ToFloat64" 73 | "LINKER:--export=JS_GetGlobalObject" 74 | "LINKER:--export=JS_HasException" 75 | "LINKER:--export=JS_GetException" 76 | "LINKER:--export=JS_GetPropertyStr" 77 | "LINKER:--export=QJS_IsUndefined" 78 | "LINKER:--export=QJS_IsException" 79 | "LINKER:--export=QJS_UpdateStackTop" 80 | "LINKER:--export=JS_SetPropertyStr" 81 | # "LINKER:--export=JS_Call" 82 | "LINKER:--export=JS_CallConstructor" 83 | "LINKER:--export=QJS_CreateFunctionProxy" 84 | "LINKER:--export=QJS_NewInt64" 85 | "LINKER:--export=QJS_NewUint32" 86 | "LINKER:--export=QJS_NewInt32" 87 | "LINKER:--export=JS_NewError" 88 | "LINKER:--export=JS_Throw" 89 | "LINKER:--export=JS_FreeValue" 90 | "LINKER:--export=JS_NewAtom" 91 | "LINKER:--export=JS_AtomToValue" 92 | "LINKER:--export=JS_FreeAtom" 93 | "LINKER:--export=JS_NewObject" 94 | "LINKER:--export=JS_ValueToAtom" 95 | "LINKER:--export=JS_GetProperty" 96 | "LINKER:--export=JS_SetProperty" 97 | "LINKER:--export=JS_HasProperty" 98 | "LINKER:--export=JS_NewAtomUInt32" 99 | "LINKER:--export=JS_DeleteProperty" 100 | "LINKER:--export=JS_ToObject" 101 | "LINKER:--export=JS_ToBool" 102 | "LINKER:--export=JS_NewArrayBufferCopy" 103 | "LINKER:--export=JS_NewArray" 104 | "LINKER:--export=JS_SetPropertyUint32" 105 | "LINKER:--export=QJS_ThrowSyntaxError" 106 | "LINKER:--export=QJS_ThrowTypeError" 107 | "LINKER:--export=QJS_ThrowReferenceError" 108 | "LINKER:--export=QJS_ThrowRangeError" 109 | "LINKER:--export=QJS_ThrowInternalError" 110 | 111 | "LINKER:--export=QJS_ModuleLoader" 112 | "LINKER:--export=QJS_Load" 113 | "LINKER:--export=QJS_Eval" 114 | "LINKER:--export=QJS_Compile" 115 | "LINKER:--export=QJS_Compile2" 116 | "LINKER:--export=QJS_CreateEvalOption" 117 | "LINKER:--export=QJS_CloneValue" 118 | "LINKER:--export=QJS_AtomToCString" 119 | "LINKER:--export=QJS_IsPromise" 120 | "LINKER:--export=QJS_IsObject" 121 | "LINKER:--export=JS_IsDate" 122 | "LINKER:--export=QJS_IsFunction" 123 | "LINKER:--export=QJS_IsError" 124 | "LINKER:--export=QJS_IsNumber" 125 | "LINKER:--export=QJS_IsBigInt" 126 | "LINKER:--export=QJS_IsBool" 127 | "LINKER:--export=QJS_IsNull" 128 | "LINKER:--export=QJS_IsUninitialized" 129 | "LINKER:--export=QJS_IsString" 130 | "LINKER:--export=QJS_IsSymbol" 131 | "LINKER:--export=QJS_IsArray" 132 | "LINKER:--export=QJS_IsConstructor" 133 | "LINKER:--export=QJS_IsInstanceOf" 134 | "LINKER:--export=QJS_GetOwnPropertyNames" 135 | "LINKER:--export=QJS_ParseJSON" 136 | "LINKER:--export=QJS_NewBool" 137 | "LINKER:--export=QJS_NewBigInt64" 138 | "LINKER:--export=QJS_NewBigUint64" 139 | "LINKER:--export=QJS_NewFloat64" 140 | "LINKER:--export=QJS_NewProxyValue" 141 | "LINKER:--export=QJS_GetArrayBuffer" 142 | "LINKER:--export=QJS_JSONStringify" 143 | "LINKER:--export=QJS_ToInt32" 144 | "LINKER:--export=QJS_ToEpochTime" 145 | "LINKER:--export=QJS_SetPropertyUint32" 146 | "LINKER:--export=QJS_GetPropertyUint32" 147 | "LINKER:--export=QJS_NewAtomUInt32" 148 | "LINKER:--export=QJS_NewArrayBufferCopy" 149 | "LINKER:--export=QJS_Call" 150 | "LINKER:--export=QJS_GetRuntime" 151 | "LINKER:--export=QJS_Panic" 152 | 153 | "LINKER:--export=malloc" 154 | "LINKER:--export=free" 155 | "LINKER:--export=initialize" 156 | ) 157 | 158 | target_compile_options(qjswasm PRIVATE "-fvisibility=default") 159 | 160 | target_compile_definitions(qjswasm PRIVATE ${qjs_defines}) 161 | 162 | target_link_libraries(qjswasm qjs) 163 | 164 | if(NOT WIN32) 165 | set_target_properties(qjswasm PROPERTIES ENABLE_EXPORTS TRUE) 166 | endif() 167 | 168 | if(QJS_BUILD_CLI_WITH_MIMALLOC OR QJS_BUILD_CLI_WITH_STATIC_MIMALLOC) 169 | find_package(mimalloc REQUIRED) 170 | if(QJS_BUILD_CLI_WITH_STATIC_MIMALLOC) 171 | target_link_libraries(qjswasm mimalloc-static) 172 | else() 173 | target_link_libraries(qjswasm mimalloc) 174 | endif() 175 | endif() 176 | 177 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) 178 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) # enables -flto for clang/wasm-ld 179 | 180 | add_compile_options(-O3 -DNDEBUG) 181 | add_link_options(-O3) 182 | 183 | add_link_options("LINKER:--stack-first") 184 | add_link_options("LINKER:--initial-memory=1310720") # 20 pages 185 | -------------------------------------------------------------------------------- /handle.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sync/atomic" 7 | ) 8 | 9 | // Handle represents a reference to a QuickJS value. 10 | // It manages raw pointer values from WebAssembly memory and provides safe 11 | // type conversion methods with proper resource management. 12 | type Handle struct { 13 | raw uint64 14 | runtime *Runtime 15 | freed int32 // atomic flag to prevent double-free 16 | } 17 | 18 | // NewHandle creates a new Handle wrapping the given pointer value. 19 | // The handle maintains a reference to the runtime for proper memory management. 20 | func NewHandle(runtime *Runtime, ptr uint64) *Handle { 21 | if runtime == nil { 22 | panic("handle: runtime cannot be nil") 23 | } 24 | 25 | return &Handle{ 26 | raw: ptr, 27 | runtime: runtime, 28 | freed: 0, 29 | } 30 | } 31 | 32 | // Free releases the memory associated with this handle. 33 | // Only used with C values such as: QJS_ToCString, QJS_JSONStringify. 34 | // Do not use this method for JsValue. 35 | func (h *Handle) Free() { 36 | if h == nil || h.runtime == nil { 37 | return 38 | } 39 | 40 | // Use atomic compare-and-swap to ensure single free 41 | if atomic.CompareAndSwapInt32(&h.freed, 0, 1) && h.raw != 0 { 42 | h.runtime.FreeHandle(h.raw) 43 | } 44 | } 45 | 46 | // IsFreed returns true if the handle has been freed. 47 | func (h *Handle) IsFreed() bool { 48 | return h == nil || atomic.LoadInt32(&h.freed) != 0 49 | } 50 | 51 | // Raw returns the underlying raw pointer or 0 if the handle is nil or freed. 52 | func (h *Handle) Raw() uint64 { 53 | if h == nil || h.IsFreed() { 54 | return 0 55 | } 56 | 57 | return h.raw 58 | } 59 | 60 | // Bool converts the handle value to bool using zero/non-zero semantics. 61 | func (h *Handle) Bool() bool { 62 | if h == nil || h.IsFreed() { 63 | return false 64 | } 65 | 66 | return int32(h.raw) != 0 67 | } 68 | 69 | // Signed integer conversion methods with bounds checking. 70 | type Signed interface { 71 | ~int | ~int8 | ~int16 | ~int32 | ~int64 72 | } 73 | 74 | type Unsigned interface { 75 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 76 | } 77 | 78 | type Integer interface { 79 | Signed | Unsigned 80 | } 81 | 82 | type Float interface { 83 | ~float32 | ~float64 84 | } 85 | 86 | // ConvertToSigned performs safe conversion to signed integer types with bounds checking. 87 | func ConvertToSigned[T Signed](h *Handle) T { 88 | if h == nil || h.IsFreed() { 89 | return T(0) 90 | } 91 | 92 | // For signed integers, we need to handle sign extension properly 93 | // WebAssembly returns values as uint64, but they may represent signed values 94 | var result T 95 | 96 | switch any(result).(type) { 97 | case int8: 98 | // Extract lower 8 bits and sign extend 99 | val := int8(uint8(h.raw)) 100 | result = T(val) 101 | case int16: 102 | // Extract lower 16 bits and sign extend 103 | val := int16(uint16(h.raw)) 104 | result = T(val) 105 | case int32: 106 | // Extract lower 32 bits and sign extend 107 | val := int32(uint32(h.raw)) 108 | result = T(val) 109 | case int64: 110 | // For int64, direct cast is appropriate 111 | result = T(int64(h.raw)) 112 | case int: 113 | // For int (platform dependent), handle like int32 or int64 based on size 114 | if Is32BitPlatform() { 115 | result = T(int32(uint32(h.raw))) 116 | } else { 117 | result = T(int64(h.raw)) 118 | } 119 | default: 120 | // Fallback for any other signed integer types 121 | result = T(h.raw) 122 | } 123 | 124 | return result 125 | } 126 | 127 | // ConvertToUnsigned performs safe conversion to unsigned integer types with bounds checking. 128 | func ConvertToUnsigned[T Unsigned](h *Handle) T { 129 | if h == nil || h.IsFreed() { 130 | return T(0) 131 | } 132 | 133 | value := h.raw 134 | 135 | var result = T(value) 136 | 137 | // Check for overflow by converting back and comparing 138 | if uint64(result) != value { 139 | panic(fmt.Sprintf("handle: overflow error - value %d exceeds range for %T", value, result)) 140 | } 141 | 142 | return result 143 | } 144 | 145 | func (h *Handle) Int() int { return ConvertToSigned[int](h) } 146 | func (h *Handle) Int8() int8 { return ConvertToSigned[int8](h) } 147 | func (h *Handle) Int16() int16 { return ConvertToSigned[int16](h) } 148 | func (h *Handle) Int32() int32 { return ConvertToSigned[int32](h) } 149 | func (h *Handle) Int64() int64 { return ConvertToSigned[int64](h) } 150 | func (h *Handle) Uint() uint { return ConvertToUnsigned[uint](h) } 151 | func (h *Handle) Uint8() uint8 { return ConvertToUnsigned[uint8](h) } 152 | func (h *Handle) Uint16() uint16 { return ConvertToUnsigned[uint16](h) } 153 | func (h *Handle) Uint32() uint32 { return ConvertToUnsigned[uint32](h) } 154 | func (h *Handle) Uint64() uint64 { return ConvertToUnsigned[uint64](h) } 155 | func (h *Handle) Uintptr() uintptr { return ConvertToUnsigned[uintptr](h) } 156 | 157 | // Float32 converts the handle value to float32 by interpreting the lower 32 bits 158 | // as IEEE 754 single-precision floating point representation. 159 | // Returns 0.0 if the handle is nil or freed. 160 | func (h *Handle) Float32() float32 { 161 | if h == nil || h.IsFreed() { 162 | return 0.0 163 | } 164 | 165 | return math.Float32frombits(uint32(h.raw)) 166 | } 167 | 168 | // Float64 converts the handle value to float64 by interpreting the raw bits 169 | // as IEEE 754 double-precision floating point representation. 170 | // Returns 0.0 if the handle is nil or freed. 171 | func (h *Handle) Float64() float64 { 172 | if h == nil || h.IsFreed() { 173 | return 0.0 174 | } 175 | 176 | return math.Float64frombits(h.raw) 177 | } 178 | 179 | // String converts the handle value to string by unpacking a pointer 180 | // to string data in QuickJS memory. Returns empty string if handle is nil or freed. 181 | // If there's a JavaScript exception in the context, it will panic with the exception. 182 | func (h *Handle) String() string { 183 | if h == nil || h.IsFreed() { 184 | return "" 185 | } 186 | 187 | // Check for exceptions in the JavaScript context 188 | if h.raw == 0 { 189 | if h.runtime != nil && h.runtime.context != nil && h.runtime.context.HasException() { 190 | panic(h.runtime.context.Exception()) 191 | } 192 | 193 | return "" 194 | } 195 | 196 | return h.runtime.mem.StringFromPackedPtr(h.raw) 197 | } 198 | 199 | // Bytes converts the handle value to []byte by reading from QuickJS memory. 200 | // Returns empty slice for zero handles or if the handle is freed. 201 | // The returned bytes are a copy and safe to modify. 202 | func (h *Handle) Bytes() []byte { 203 | if h == nil || h.IsFreed() || h.raw == 0 { 204 | return nil 205 | } 206 | 207 | addr, size := h.runtime.mem.UnpackPtr(h.raw) 208 | if addr == 0 || size == 0 { 209 | return nil 210 | } 211 | 212 | // Ensure we free the address after reading 213 | defer h.runtime.FreeHandle(uint64(addr)) 214 | 215 | // Read from WebAssembly memory 216 | data := h.runtime.mem.MustRead(addr, uint64(size)) 217 | 218 | // Create a copy to ensure the returned slice is safe to use 219 | result := make([]byte, size) 220 | copy(result, data) 221 | 222 | return result 223 | } 224 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | const ( 11 | // JsEvalTypeGlobal evaluates code in global scope (default). 12 | JsEvalTypeGlobal = (0 << 0) 13 | // JsEvalTypeModule evaluates code as ES6 module. 14 | JsEvalTypeModule = (1 << 0) 15 | // JsEvalTypeDirect performs direct call (internal use). 16 | JsEvalTypeDirect = (2 << 0) 17 | // JsEvalTypeInDirect performs indirect call (internal use). 18 | JsEvalTypeInDirect = (3 << 0) 19 | // JsEvalTypeMask masks the eval type bits. 20 | JsEvalTypeMask = (3 << 0) 21 | // JsEvalFlagStrict forces strict mode execution. 22 | JsEvalFlagStrict = (1 << 3) 23 | // JsEvalFlagUnUsed is reserved for future use. 24 | JsEvalFlagUnUsed = (1 << 4) 25 | // JsEvalFlagCompileOnly returns a JS bytecode/module for JS_EvalFunction(). 26 | JsEvalFlagCompileOnly = (1 << 5) 27 | // JsEvalFlagBackTraceBarrier prevents the stack frames before this eval in the Error() backtraces. 28 | JsEvalFlagBackTraceBarrier = (1 << 6) 29 | // JsEvalFlagAsync enables top-level await (global scope only). 30 | JsEvalFlagAsync = (1 << 7) 31 | ) 32 | 33 | type Option struct { 34 | CWD string 35 | StartFunctionName string 36 | Context context.Context 37 | // Enabling this option significantly increases evaluation time 38 | // because every operation must check the done context, which introduces additional overhead. 39 | CloseOnContextDone bool 40 | DisableBuildCache bool 41 | CacheDir string 42 | MemoryLimit int 43 | MaxStackSize int 44 | MaxExecutionTime int 45 | GCThreshold int 46 | QuickJSWasmBytes []byte 47 | ProxyFunction any 48 | Stdout io.Writer 49 | Stderr io.Writer 50 | } 51 | 52 | // EvalOption configures JavaScript evaluation behavior in QuickJS context. 53 | type EvalOption struct { 54 | c *Context 55 | file string 56 | code string 57 | bytecode []byte 58 | bytecodeLen int 59 | flags uint64 60 | 61 | // QuickJS value handles for memory management 62 | fileValue *Value 63 | codeValue *Value 64 | byteCodeValue *Value 65 | } 66 | 67 | // EvalOptionFunc configures evaluation behavior using functional option pattern. 68 | type EvalOptionFunc func(*EvalOption) 69 | 70 | // createEvalOption initializes default option with global scope and strict mode. 71 | func createEvalOption(c *Context, file string, flags ...EvalOptionFunc) *EvalOption { 72 | evalOption := &EvalOption{ 73 | c: c, 74 | file: file, 75 | flags: JsEvalTypeGlobal | JsEvalFlagStrict, 76 | } 77 | 78 | for _, flag := range flags { 79 | flag(evalOption) 80 | } 81 | 82 | return evalOption 83 | } 84 | 85 | // Code sets the JavaScript source code to evaluate. 86 | func Code(code string) EvalOptionFunc { 87 | return func(o *EvalOption) { 88 | o.code = code 89 | } 90 | } 91 | 92 | // Bytecode sets precompiled JavaScript bytecode to execute. 93 | func Bytecode(buf []byte) EvalOptionFunc { 94 | return func(o *EvalOption) { 95 | o.bytecode = buf 96 | o.bytecodeLen = len(buf) 97 | } 98 | } 99 | 100 | // TypeGlobal sets evaluation to run in global scope (default behavior). 101 | func TypeGlobal() EvalOptionFunc { 102 | return func(o *EvalOption) { 103 | o.flags |= JsEvalTypeGlobal 104 | } 105 | } 106 | 107 | // TypeModule sets evaluation to run as ES6 module. 108 | func TypeModule() EvalOptionFunc { 109 | return func(o *EvalOption) { 110 | o.flags |= JsEvalTypeModule 111 | } 112 | } 113 | 114 | // FlagAsync enables top-level await in global scripts. 115 | // Returns a promise from JS_Eval(). Only valid with TypeGlobal. 116 | func FlagAsync() EvalOptionFunc { 117 | return func(o *EvalOption) { 118 | o.flags |= JsEvalFlagAsync 119 | } 120 | } 121 | 122 | // FlagStrict forces strict mode execution. 123 | func FlagStrict() EvalOptionFunc { 124 | return func(o *EvalOption) { 125 | o.flags |= JsEvalFlagStrict 126 | } 127 | } 128 | 129 | // FlagCompileOnly compiles code without execution. 130 | // Returns bytecode object for later execution with JS_EvalFunction(). 131 | func FlagCompileOnly() EvalOptionFunc { 132 | return func(o *EvalOption) { 133 | o.flags |= JsEvalFlagCompileOnly 134 | } 135 | } 136 | 137 | // TypeDirect sets direct call mode (internal QuickJS use). 138 | func TypeDirect() EvalOptionFunc { 139 | return func(o *EvalOption) { 140 | o.flags |= JsEvalTypeDirect 141 | } 142 | } 143 | 144 | // TypeIndirect sets indirect call mode (internal QuickJS use). 145 | func TypeIndirect() EvalOptionFunc { 146 | return func(o *EvalOption) { 147 | o.flags |= JsEvalTypeInDirect 148 | } 149 | } 150 | 151 | // TypeMask applies eval type mask (internal QuickJS use). 152 | func TypeMask() EvalOptionFunc { 153 | return func(o *EvalOption) { 154 | o.flags |= JsEvalTypeMask 155 | } 156 | } 157 | 158 | // FlagUnused is reserved for future QuickJS features. 159 | func FlagUnused() EvalOptionFunc { 160 | return func(o *EvalOption) { 161 | o.flags |= JsEvalFlagUnUsed 162 | } 163 | } 164 | 165 | // FlagBacktraceBarrier excludes stack frames before this eval from error backtraces. 166 | func FlagBacktraceBarrier() EvalOptionFunc { 167 | return func(o *EvalOption) { 168 | o.flags |= JsEvalFlagBackTraceBarrier 169 | } 170 | } 171 | 172 | // Handle creates QuickJS evaluation option handle for WASM function calls. 173 | func (o *EvalOption) Handle() (handle uint64) { 174 | codeHandle := uint64(0) 175 | byteCodeHandle := uint64(0) 176 | o.fileValue = o.c.NewStringHandle(o.file) 177 | 178 | if o.code != "" { 179 | o.codeValue = o.c.NewStringHandle(o.code) 180 | codeHandle = o.codeValue.Raw() 181 | } 182 | 183 | if o.bytecode != nil { 184 | o.byteCodeValue = o.c.NewBytes(o.bytecode) 185 | byteCodeHandle = o.byteCodeValue.Raw() 186 | } 187 | 188 | // Create QuickJS option struct via WASM call 189 | option := o.c.Call( 190 | "QJS_CreateEvalOption", 191 | codeHandle, 192 | byteCodeHandle, 193 | uint64(o.bytecodeLen), 194 | o.fileValue.Raw(), 195 | o.flags, 196 | ) 197 | 198 | return option.Raw() 199 | } 200 | 201 | // Free releases QuickJS value handles to prevent memory leaks. 202 | // Must be called after Handle() to clean up WASM memory. 203 | func (o *EvalOption) Free() { 204 | if o.fileValue.Raw() != 0 { 205 | o.c.Call("JS_FreeValue", o.c.Raw(), o.fileValue.Raw()) 206 | } 207 | 208 | if o.codeValue != nil && o.codeValue.Raw() != 0 { 209 | o.c.Call("JS_FreeValue", o.c.Raw(), o.codeValue.Raw()) 210 | } 211 | 212 | if o.byteCodeValue != nil && o.byteCodeValue.Raw() != 0 { 213 | o.c.Call("JS_FreeValue", o.c.Raw(), o.byteCodeValue.Raw()) 214 | } 215 | } 216 | 217 | func getRuntimeOption(registry *ProxyRegistry, options ...Option) (option Option, err error) { 218 | if len(options) == 0 { 219 | option = Option{} 220 | } else { 221 | option = options[0] 222 | } 223 | 224 | if option.CWD == "" { 225 | if option.CWD, err = os.Getwd(); err != nil { 226 | return Option{}, fmt.Errorf("cannot get current working directory: %w", err) 227 | } 228 | } 229 | 230 | if option.Context == nil { 231 | option.Context = context.Background() 232 | } 233 | 234 | if option.ProxyFunction == nil { 235 | option.ProxyFunction = createFuncProxyWithRegistry(registry) 236 | } 237 | 238 | if option.Stdout == nil { 239 | option.Stdout = os.Stdout 240 | } 241 | 242 | if option.Stderr == nil { 243 | option.Stderr = os.Stderr 244 | } 245 | 246 | return option, nil 247 | } 248 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package qjs_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/fastschema/qjs" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestEvalOptions(t *testing.T) { 14 | t.Run("DefaultOptions", func(t *testing.T) { 15 | runtime := must(qjs.New()) 16 | defer runtime.Close() 17 | 18 | // Use Eval to test the default options (should be global with strict mode) 19 | val, err := runtime.Eval("test.js", qjs.Code("'use strict'; 'test'")) 20 | assert.NoError(t, err) 21 | assert.Equal(t, "test", val.String()) 22 | val.Free() 23 | }) 24 | 25 | t.Run("CWDOption", func(t *testing.T) { 26 | // Store original working directory to restore later 27 | originalCwd, err := os.Getwd() 28 | require.NoError(t, err) 29 | 30 | t.Cleanup(func() { 31 | _ = os.Chdir(originalCwd) 32 | }) 33 | 34 | t.Run("default_cwd_from_os_getwd", func(t *testing.T) { 35 | runtime, err := qjs.New() 36 | require.NoError(t, err) 37 | runtime.Close() 38 | }) 39 | 40 | t.Run("explicit_cwd_provided", func(t *testing.T) { 41 | tempDir := t.TempDir() 42 | runtime, err := qjs.New(qjs.Option{CWD: tempDir}) 43 | require.NoError(t, err) 44 | runtime.Close() 45 | }) 46 | 47 | t.Run("deleted_working_directory", func(t *testing.T) { 48 | // Create a temporary directory and change to it 49 | tempDir := t.TempDir() 50 | subDir := filepath.Join(tempDir, "workdir") 51 | err := os.Mkdir(subDir, 0755) 52 | require.NoError(t, err, "Failed to create subdirectory") 53 | 54 | // Change to the subdirectory 55 | err = os.Chdir(subDir) 56 | require.NoError(t, err) 57 | 58 | // Remove the current working directory while we're in it 59 | err = os.RemoveAll(subDir) 60 | require.NoError(t, err) 61 | 62 | _, err = qjs.New() 63 | _ = os.Chdir(originalCwd) 64 | assert.Error(t, err) 65 | assert.Contains(t, err.Error(), "failed to get runtime options") 66 | }) 67 | }) 68 | 69 | t.Run("CodeOption", func(t *testing.T) { 70 | runtime := must(qjs.New()) 71 | defer runtime.Close() 72 | 73 | val, err := runtime.Eval("test.js", qjs.Code("42")) 74 | assert.NoError(t, err) 75 | assert.Equal(t, int32(42), val.Int32()) 76 | val.Free() 77 | }) 78 | 79 | t.Run("TypeModuleOption", func(t *testing.T) { 80 | runtime := must(qjs.New()) 81 | defer runtime.Close() 82 | 83 | val, err := runtime.Eval("test.js", 84 | qjs.Code("export default 'module'"), 85 | qjs.TypeModule()) 86 | assert.NoError(t, err) 87 | assert.Equal(t, "module", val.String()) 88 | val.Free() 89 | }) 90 | 91 | t.Run("BytecodeCompileAndEval", func(t *testing.T) { 92 | runtime := must(qjs.New()) 93 | defer runtime.Close() 94 | 95 | bytecode, err := runtime.Compile("test.js", qjs.Code("123 + 456")) 96 | assert.NoError(t, err) 97 | assert.NotEmpty(t, bytecode) 98 | 99 | val, err := runtime.Eval("test.js", qjs.Bytecode(bytecode)) 100 | assert.NoError(t, err) 101 | assert.Equal(t, int32(579), val.Int32()) 102 | val.Free() 103 | }) 104 | 105 | t.Run("ModuleBytecodeCompileAndEval", func(t *testing.T) { 106 | runtime := must(qjs.New()) 107 | defer runtime.Close() 108 | 109 | bytecode, err := runtime.Compile( 110 | "test.js", 111 | qjs.Code("export default 'module bytecode'"), 112 | qjs.TypeModule()) 113 | assert.NoError(t, err) 114 | assert.NotEmpty(t, bytecode) 115 | 116 | val, err := runtime.Eval("test.js", 117 | qjs.Bytecode(bytecode), 118 | qjs.TypeModule()) 119 | assert.NoError(t, err) 120 | assert.Equal(t, "module bytecode", val.String()) 121 | val.Free() 122 | }) 123 | 124 | t.Run("FlagAsyncOption", func(t *testing.T) { 125 | runtime := must(qjs.New()) 126 | defer runtime.Close() 127 | 128 | // Test async flag with top-level await 129 | val, err := runtime.Eval("test.js", 130 | qjs.Code("await Promise.resolve(100)"), 131 | qjs.FlagAsync()) 132 | assert.NoError(t, err) 133 | assert.Equal(t, int32(100), val.Int32()) 134 | val.Free() 135 | }) 136 | 137 | t.Run("FlagStrictOption", func(t *testing.T) { 138 | runtime := must(qjs.New()) 139 | defer runtime.Close() 140 | 141 | // In strict mode, using undeclared variables throws an error 142 | _, err := runtime.Eval("test.js", 143 | qjs.Code("undeclaredVar = 10"), 144 | qjs.FlagStrict()) 145 | assert.Error(t, err) 146 | assert.Contains(t, err.Error(), "ReferenceError") 147 | }) 148 | 149 | t.Run("FlagCompileOnlyOption", func(t *testing.T) { 150 | runtime := must(qjs.New()) 151 | defer runtime.Close() 152 | ctx := runtime.Context() 153 | 154 | // We need to use Go API directly because runtime.Compile 155 | // already applies FlagCompileOnly 156 | val, err := ctx.Eval("test.js", 157 | qjs.Code("42"), 158 | qjs.FlagCompileOnly()) 159 | assert.NoError(t, err) 160 | 161 | // The result should be bytecode, not the executed result 162 | assert.False(t, val.IsNumber()) 163 | val.Free() 164 | }) 165 | 166 | t.Run("FlagBacktraceBarrierOption", func(t *testing.T) { 167 | runtime := must(qjs.New()) 168 | defer runtime.Close() 169 | 170 | _, err := runtime.Eval("outer.js", qjs.Code(` 171 | function outer() { 172 | throw new Error("test error"); 173 | } 174 | `)) 175 | assert.NoError(t, err) 176 | 177 | _, err = runtime.Eval("without-barrier.js", qjs.Code(`outer()`)) 178 | assert.Error(t, err) 179 | assert.Contains(t, err.Error(), "outer.js") 180 | 181 | _, err = runtime.Eval("with-barrier.js", 182 | qjs.Code(`outer()`), 183 | qjs.FlagBacktraceBarrier()) 184 | assert.Error(t, err) 185 | }) 186 | 187 | t.Run("MultipleOptions", func(t *testing.T) { 188 | runtime := must(qjs.New()) 189 | defer runtime.Close() 190 | 191 | val, err := runtime.Eval("test.js", 192 | qjs.Code("export default await Promise.resolve('async module')"), 193 | qjs.TypeModule(), 194 | qjs.FlagStrict()) 195 | assert.NoError(t, err) 196 | assert.Equal(t, "async module", val.String()) 197 | val.Free() 198 | }) 199 | 200 | t.Run("ExplicitTypeGlobal", func(t *testing.T) { 201 | runtime := must(qjs.New()) 202 | defer runtime.Close() 203 | 204 | val, err := runtime.Eval("test.js", 205 | qjs.Code("var x = 100; x;"), 206 | qjs.TypeGlobal()) 207 | assert.NoError(t, err) 208 | assert.Equal(t, int32(100), val.Int32()) 209 | val.Free() 210 | }) 211 | 212 | t.Run("BytecodeWithModuleAndAsync", func(t *testing.T) { 213 | runtime := must(qjs.New()) 214 | defer runtime.Close() 215 | 216 | bytecode, err := runtime.Compile( 217 | "test.js", 218 | qjs.Code("export default await Promise.resolve(42)"), 219 | qjs.TypeModule()) 220 | assert.NoError(t, err) 221 | assert.NotEmpty(t, bytecode) 222 | 223 | val, err := runtime.Eval("test.js", 224 | qjs.Bytecode(bytecode), 225 | qjs.TypeModule()) 226 | assert.NoError(t, err) 227 | assert.Equal(t, int32(42), val.Int32()) 228 | val.Free() 229 | }) 230 | 231 | t.Run("ModuleWithStrictMode", func(t *testing.T) { 232 | runtime := must(qjs.New()) 233 | defer runtime.Close() 234 | 235 | val, err := runtime.Eval("test.js", 236 | qjs.Code("export default 'strict module'"), 237 | qjs.TypeModule(), 238 | qjs.FlagStrict()) 239 | assert.NoError(t, err) 240 | assert.Equal(t, "strict module", val.String()) 241 | val.Free() 242 | 243 | _, err = runtime.Eval("test.js", 244 | qjs.Code("export default (function() { with({}) { return 'test'; } })()"), 245 | qjs.TypeModule(), 246 | qjs.FlagStrict()) 247 | assert.Error(t, err) 248 | assert.Contains(t, err.Error(), "SyntaxError") 249 | }) 250 | } 251 | 252 | func TestInternalOptions(t *testing.T) { 253 | runtime := must(qjs.New()) 254 | defer runtime.Close() 255 | 256 | _, err := runtime.Compile("test.js", 257 | qjs.Code("42"), 258 | qjs.TypeDirect()) 259 | assert.NoError(t, err) 260 | 261 | _, err = runtime.Compile("test.js", 262 | qjs.Code("42"), 263 | qjs.TypeIndirect()) 264 | assert.NoError(t, err) 265 | 266 | _, err = runtime.Compile("test.js", 267 | qjs.Code("42"), 268 | qjs.TypeMask()) 269 | assert.NoError(t, err) 270 | 271 | _, err = runtime.Compile("test.js", 272 | qjs.Code("42"), 273 | qjs.FlagUnused()) 274 | assert.NoError(t, err) 275 | } 276 | -------------------------------------------------------------------------------- /functojs.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // FuncToJS converts a Go function to a JavaScript function. 9 | func FuncToJS(c *Context, v any) (_ *Value, err error) { 10 | if v == nil { 11 | return c.NewNull(), nil 12 | } 13 | 14 | defer func() { 15 | if err != nil { 16 | err = fmt.Errorf("[FuncToJS] %w", err) 17 | } 18 | }() 19 | 20 | rval := reflect.ValueOf(v) 21 | rtype := reflect.TypeOf(v) 22 | 23 | if rtype.Kind() == reflect.Ptr { 24 | if rval.IsNil() { 25 | return c.NewNull(), nil 26 | } 27 | 28 | rval = rval.Elem() 29 | rtype = rtype.Elem() 30 | } 31 | 32 | if rtype.Kind() != reflect.Func { 33 | return nil, newInvalidGoTypeErr("function", v) 34 | } 35 | 36 | if rval.IsNil() { 37 | return c.NewNull(), nil 38 | } 39 | 40 | fnType := rval.Type() 41 | if err := VerifyGoFunc(fnType, v); err != nil { 42 | return nil, err 43 | } 44 | 45 | return c.Function(func(this *This) (*Value, error) { 46 | goArgs, err := JsFuncArgsToGo(this.Args(), fnType) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | var results []reflect.Value 52 | if fnType.IsVariadic() { 53 | results = rval.CallSlice(goArgs) 54 | } else { 55 | results = rval.Call(goArgs) 56 | } 57 | 58 | return GoFuncResultToJs(c, results) 59 | }), nil 60 | } 61 | 62 | // JsFuncArgsToGo converts JS arguments to Go arguments in both variadic and non-variadic functions, 63 | // filling missing arguments with zero values. 64 | func JsFuncArgsToGo(jsArgs []*Value, fnType reflect.Type) ([]reflect.Value, error) { 65 | numGoArgs := fnType.NumIn() 66 | goArgs := make([]reflect.Value, 0, numGoArgs) 67 | numJSArgs := len(jsArgs) 68 | 69 | if fnType.IsVariadic() { 70 | fixedArgs := Min(numGoArgs-1, numJSArgs) 71 | for i := range fixedArgs { 72 | goVal, err := JsArgToGo(jsArgs[i], fnType.In(i)) 73 | if err != nil { 74 | return nil, newArgConversionErr(i, err) 75 | } 76 | 77 | goArgs = append(goArgs, goVal) 78 | } 79 | 80 | // Handle variadic slice 81 | variadicSlice, err := CreateVariadicSlice(jsArgs[fixedArgs:], fnType.In(numGoArgs-1), fixedArgs) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return append(goArgs, variadicSlice), nil 87 | } 88 | 89 | // Limit JS args to Go args count to avoid index out of bounds 90 | argsToProcess := Min(numJSArgs, numGoArgs) 91 | for i := range argsToProcess { 92 | goVal, err := JsArgToGo(jsArgs[i], fnType.In(i)) 93 | if err != nil { 94 | return nil, newArgConversionErr(i, err) 95 | } 96 | 97 | goArgs = append(goArgs, goVal) 98 | } 99 | 100 | // Fill missing arguments with zero values 101 | for i := argsToProcess; i < numGoArgs; i++ { 102 | goArgs = append(goArgs, reflect.Zero(fnType.In(i))) 103 | } 104 | 105 | return goArgs, nil 106 | } 107 | 108 | // handlePointerArgument processes JS arguments for pointer types. 109 | func handlePointerArgument(jsArg *Value, argType reflect.Type) (reflect.Value, error) { 110 | if jsArg.IsNull() || jsArg.IsUndefined() { 111 | return reflect.Zero(argType), nil 112 | } 113 | 114 | underlyingType := argType.Elem() 115 | zeroVal := reflect.New(underlyingType).Elem() 116 | 117 | goVal, err := ToGoValue(jsArg, zeroVal.Interface()) 118 | if err != nil { 119 | return reflect.Value{}, newJsToGoErr(jsArg, err, "function param pointer to "+jsArg.Type()) 120 | } 121 | 122 | ptrVal := reflect.New(underlyingType) 123 | ptrVal.Elem().Set(reflect.ValueOf(goVal)) 124 | 125 | return ptrVal, nil 126 | } 127 | 128 | // CreateNonNilSample creates appropriate non-nil samples for types that have nil zero values. 129 | func CreateNonNilSample(argType reflect.Type) any { 130 | switch argType.Kind() { 131 | case reflect.Interface: 132 | // return nil to let the conversion 133 | // use the default logic which can handle dynamic type inference 134 | return nil 135 | 136 | case reflect.Ptr: 137 | elemType := argType.Elem() 138 | elemZero := reflect.Zero(elemType) 139 | ptr := reflect.New(elemType) 140 | ptr.Elem().Set(elemZero) 141 | 142 | return ptr.Interface() 143 | 144 | case reflect.Array: 145 | return reflect.New(argType).Elem().Interface() 146 | 147 | case reflect.Slice: 148 | return reflect.MakeSlice(argType, 0, 0).Interface() 149 | 150 | case reflect.Map: 151 | return reflect.MakeMap(argType).Interface() 152 | 153 | case reflect.Chan: 154 | return reflect.MakeChan(argType, 1).Interface() // size 1 buffer to avoid blocking 155 | 156 | case reflect.Func: 157 | return createDummyFunction(argType) 158 | 159 | default: 160 | // For other types (shouldn't happen), return zero value 161 | return reflect.Zero(argType).Interface() 162 | } 163 | } 164 | 165 | // createDummyFunction creates a dummy function with the specified signature for type inference. 166 | func createDummyFunction(funcType reflect.Type) any { 167 | fn := reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value { 168 | results := make([]reflect.Value, funcType.NumOut()) 169 | for i := range funcType.NumOut() { 170 | results[i] = reflect.Zero(funcType.Out(i)) 171 | } 172 | 173 | return results 174 | }) 175 | 176 | return fn.Interface() 177 | } 178 | 179 | // JsArgToGo converts a single JS argument to a Go value with enhanced type handling. 180 | func JsArgToGo(jsArg *Value, argType reflect.Type) (reflect.Value, error) { 181 | if argType.Kind() == reflect.Ptr { 182 | return handlePointerArgument(jsArg, argType) 183 | } 184 | 185 | goZeroVal := CreateNonNilSample(argType) 186 | 187 | goVal, err := ToGoValue(jsArg, goZeroVal) 188 | if err != nil { 189 | return reflect.Value{}, newJsToGoErr(jsArg, err, "function param "+jsArg.Type()) 190 | } 191 | 192 | return reflect.ValueOf(goVal), nil 193 | } 194 | 195 | // CreateVariadicSlice creates a reflect.Value slice for variadic arguments. 196 | // Converts remaining JS arguments to the slice element type and returns as a slice value. 197 | func CreateVariadicSlice(jsArgs []*Value, sliceType reflect.Type, fixedArgsCount int) (reflect.Value, error) { 198 | varArgType := sliceType.Elem() 199 | numVarArgs := len(jsArgs) 200 | variadicSlice := reflect.MakeSlice(sliceType, numVarArgs, numVarArgs) 201 | 202 | for i, jsArg := range jsArgs { 203 | goVal, err := JsArgToGo(jsArg, varArgType) 204 | if err != nil { 205 | return reflect.Value{}, newArgConversionErr(fixedArgsCount+i, err) 206 | } 207 | 208 | variadicSlice.Index(i).Set(goVal) 209 | } 210 | 211 | return variadicSlice, nil 212 | } 213 | 214 | // GoFuncResultToJs processes Go function call results and converts them to JS values. 215 | // If last return value is a non-nil error, it's thrown in JS context. 216 | // The remaining return values are converted to JS value or JS array if there are multiple. 217 | func GoFuncResultToJs(c *Context, results []reflect.Value) (*Value, error) { 218 | if len(results) == 0 { 219 | return nil, nil 220 | } 221 | 222 | // Check if last return value is an error 223 | lastIdx := len(results) - 1 224 | lastResult := results[lastIdx] 225 | 226 | // Last return is error 227 | if IsImplementError(lastResult.Type()) { 228 | // Last return is non-nil error -> throw in JS context 229 | if !lastResult.IsNil() { 230 | resultErr, _ := lastResult.Interface().(error) 231 | 232 | return nil, resultErr 233 | } 234 | 235 | // Error is nil, handle remaining return values 236 | remaining := results[:lastIdx] 237 | 238 | if len(remaining) == 0 { 239 | return nil, nil 240 | } 241 | 242 | // Single remaining value -> return that value 243 | if len(remaining) == 1 { 244 | return ToJsValue(c, remaining[0].Interface()) 245 | } 246 | 247 | // Multiple remaining values -> return as JS array 248 | jsValues := make([]any, len(remaining)) 249 | for i, result := range remaining { 250 | jsValues[i] = result.Interface() 251 | } 252 | 253 | return ToJsValue(c, jsValues) 254 | } 255 | 256 | // Single return value -> return that value 257 | if len(results) == 1 { 258 | return ToJsValue(c, results[0].Interface()) 259 | } 260 | 261 | // Multiple return values -> return as JS array 262 | jsValues := make([]any, len(results)) 263 | for i, result := range results { 264 | jsValues[i] = result.Interface() 265 | } 266 | 267 | return ToJsValue(c, jsValues) 268 | } 269 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/tetratelabs/wazero/api" 10 | ) 11 | 12 | // ProxyRegistry stores Go functions that can be called from JavaScript. 13 | // It provides thread-safe registration and retrieval of functions with automatic ID generation. 14 | type ProxyRegistry struct { 15 | nextID uint64 16 | mu sync.RWMutex 17 | proxies map[uint64]any 18 | } 19 | 20 | // NewProxyRegistry creates a new thread-safe proxy registry. 21 | func NewProxyRegistry() *ProxyRegistry { 22 | return &ProxyRegistry{ 23 | proxies: make(map[uint64]any), 24 | } 25 | } 26 | 27 | // Register adds a function to the registry and returns its unique ID. 28 | // This method is thread-safe and can be called concurrently. 29 | func (r *ProxyRegistry) Register(fn any) uint64 { 30 | if fn == nil { 31 | return 0 32 | } 33 | 34 | id := atomic.AddUint64(&r.nextID, 1) 35 | r.mu.Lock() 36 | r.proxies[id] = fn 37 | r.mu.Unlock() 38 | 39 | return id 40 | } 41 | 42 | // Get retrieves a function by its ID. 43 | // Returns the function and true if found, nil and false otherwise. 44 | // This method is thread-safe and can be called concurrently. 45 | func (r *ProxyRegistry) Get(id uint64) (any, bool) { 46 | if id == 0 { 47 | return nil, false 48 | } 49 | 50 | r.mu.RLock() 51 | fn, ok := r.proxies[id] 52 | r.mu.RUnlock() 53 | 54 | return fn, ok 55 | } 56 | 57 | // Unregister removes a function from the registry by its ID. 58 | // Returns true if the function was found and removed, false otherwise. 59 | // This method is thread-safe and can be called concurrently. 60 | func (r *ProxyRegistry) Unregister(id uint64) bool { 61 | if id == 0 { 62 | return false 63 | } 64 | 65 | r.mu.Lock() 66 | 67 | _, exists := r.proxies[id] 68 | if exists { 69 | delete(r.proxies, id) 70 | } 71 | 72 | r.mu.Unlock() 73 | 74 | return exists 75 | } 76 | 77 | // Len returns the number of registered functions. 78 | // This method is thread-safe. 79 | func (r *ProxyRegistry) Len() int { 80 | r.mu.RLock() 81 | count := len(r.proxies) 82 | r.mu.RUnlock() 83 | 84 | return count 85 | } 86 | 87 | // Clear removes all registered functions from the registry. 88 | // This method is thread-safe. 89 | func (r *ProxyRegistry) Clear() { 90 | r.mu.Lock() 91 | r.proxies = make(map[uint64]any) 92 | r.mu.Unlock() 93 | } 94 | 95 | // JsFunctionProxy is the Go host function that will be imported by the WASM module. 96 | // It corresponds to the following C declaration: 97 | // 98 | // __attribute__((import_module("env"), import_name("jsFunctionProxy"))) 99 | // extern JSValue jsFunctionProxy(JSContext *ctx, JSValueConst this, int argc, JSValueConst *argv); 100 | // 101 | // Parameters: 102 | // - ctx: JSContext pointer 103 | // - this: JSValueConst this (the "this" value) 104 | // - argc: int argc (number of arguments) 105 | // - argv: pointer to argv (an array of JSValueConst/JSValue, each 8 bytes - uint64) 106 | type JsFunctionProxy = func( 107 | ctx context.Context, 108 | module api.Module, 109 | jsCtx uint32, 110 | thisVal uint64, 111 | argc uint32, 112 | argv uint32, 113 | ) (rs uint64) 114 | 115 | // createFuncProxyWithRegistry creates a WASM function proxy that bridges JavaScript function calls to Go functions. 116 | // It handles parameter extraction, error recovery, and result conversion between JS and Go. 117 | func createFuncProxyWithRegistry(registry *ProxyRegistry) JsFunctionProxy { 118 | return func( 119 | _ context.Context, 120 | module api.Module, 121 | _ uint32, 122 | thisVal uint64, 123 | argc uint32, 124 | argv uint32, 125 | ) (rs uint64) { 126 | goFunc, this := getProxyFuncParams(registry, module.Memory(), thisVal, argc, argv) 127 | 128 | defer func() { 129 | if r := recover(); r != nil { 130 | rs = handlePanicRecovery(this, r) 131 | } 132 | }() 133 | 134 | result, err := goFunc(this) 135 | if err != nil { 136 | return this.context.ThrowError(err).Raw() 137 | } 138 | 139 | return validateAndReturnResult(this, result) 140 | } 141 | } 142 | 143 | // handlePanicRecovery handles panics during Go function execution. 144 | func handlePanicRecovery(this *This, r any) uint64 { 145 | recoveredErr := newProxyErr(0, r) 146 | if this.isAsync && this.promise != nil { 147 | errVal := this.context.NewError(recoveredErr) 148 | rejectErr := this.promise.Reject(errVal) 149 | 150 | return this.context.ThrowError(combineErrors(recoveredErr, rejectErr)).Raw() 151 | } 152 | 153 | return this.context.ThrowError(recoveredErr).Raw() 154 | } 155 | 156 | // validateAndReturnResult validates the function result and handles JavaScript exceptions. 157 | func validateAndReturnResult(this *This, result *Value) uint64 { 158 | if this.context.HasException() { 159 | panic(this.context.Exception()) 160 | } 161 | 162 | if result == nil { 163 | return this.context.NewUndefined().Raw() 164 | } 165 | 166 | return result.Raw() 167 | } 168 | 169 | // getProxyFuncParams extracts and validates parameters for proxy function calls. 170 | // It parses WASM memory to extract function ID, context, arguments, and async information. 171 | func getProxyFuncParams( 172 | registry *ProxyRegistry, 173 | mem api.Memory, 174 | thisRef uint64, 175 | argc uint32, 176 | argv uint32, 177 | ) (Function, *This) { 178 | args := readArgsFromWasmMem(mem, argc, argv) 179 | functionHandle := args[0] // The first argument is the function ID 180 | jsContextHandle := args[1] // The second argument is the context ID 181 | isAsync := args[2] // The third argument is the async flag 182 | 183 | goFunc, goContext := retrieveGoResources(registry, functionHandle, jsContextHandle) 184 | promise := extractPromiseIfAsync(goContext, args, isAsync) 185 | fnArgs := extractFunctionArguments(goContext, args) 186 | this := createThisContext(goContext, thisRef, fnArgs, promise, isAsync) 187 | 188 | return goFunc, this 189 | } 190 | 191 | // extractPromiseIfAsync extracts the promise handle for async functions. 192 | func extractPromiseIfAsync(context *Context, args []uint64, isAsync uint64) *Value { 193 | if isAsync == 0 { 194 | return context.NewUndefined() 195 | } 196 | 197 | promiseHandle := args[3] 198 | 199 | return context.NewValue(NewHandle(context.runtime, promiseHandle)) 200 | } 201 | 202 | // extractFunctionArguments extracts and clones function arguments from WASM memory. 203 | func extractFunctionArguments(context *Context, args []uint64) []*Value { 204 | fnArgs := make([]*Value, len(args)-4) 205 | for i := range fnArgs { 206 | argHandle := args[4+i] 207 | arg := context.NewValue(NewHandle(context.runtime, argHandle)) 208 | fnArgs[i] = arg.Clone() 209 | } 210 | 211 | return fnArgs 212 | } 213 | 214 | // createThisContext creates the This context for function execution. 215 | func createThisContext(context *Context, thisRef uint64, args []*Value, promise *Value, isAsync uint64) *This { 216 | thisValue := context.NewValue(NewHandle(context.runtime, thisRef)) 217 | this := &This{ 218 | context: context, 219 | Value: thisValue, 220 | args: args, 221 | promise: promise, 222 | isAsync: isAsync != 0, 223 | } 224 | 225 | return this 226 | } 227 | 228 | // retrieveGoResources retrieves and validates Go function and context from the registry. 229 | func retrieveGoResources( 230 | registry *ProxyRegistry, 231 | functionHandle uint64, 232 | jsContextHandle uint64, 233 | ) (Function, *Context) { 234 | // Retrieve and validate function 235 | fn, _ := registry.Get(functionHandle) 236 | goFunc, _ := fn.(Function) 237 | jsContext, _ := registry.Get(jsContextHandle) 238 | goContext, _ := jsContext.(*Context) 239 | 240 | return goFunc, goContext 241 | } 242 | 243 | // readArgsFromWasmMem reads the argument array from WASM memory with proper validation. 244 | // Each argument is 8 bytes (uint64) in little-endian format. 245 | func readArgsFromWasmMem(mem api.Memory, argc uint32, argv uint32) []uint64 { 246 | args := make([]uint64, argc) 247 | 248 | for i := range argc { 249 | // Calculate the offset for the i-th argument 250 | offset := argv + (i * 8) 251 | data, _ := mem.Read(offset, 8) 252 | // Convert the 8 bytes to a uint64 (little-endian encoding) 253 | args[i] = binary.LittleEndian.Uint64(data) 254 | } 255 | 256 | return args 257 | } 258 | -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import "fmt" 4 | 5 | // Array provides a wrapper around JavaScript arrays with Go-like methods. 6 | type Array struct { 7 | *Value 8 | } 9 | 10 | // NewArray wraps a JavaScript array value in a Go Array type. 11 | func NewArray(value *Value) *Array { 12 | if value == nil || !value.IsArray() { 13 | return nil 14 | } 15 | 16 | return &Array{Value: value} 17 | } 18 | 19 | func (a *Array) ForEach(forFn func(key, value *Value)) { 20 | if a == nil || a.Value == nil || forFn == nil { 21 | return 22 | } 23 | 24 | a.Value.ForEach(forFn) 25 | } 26 | 27 | // HasIndex returns true if the given index exists in the array. 28 | func (a *Array) HasIndex(i int64) bool { 29 | return a.HasPropertyIndex(i) 30 | } 31 | 32 | // Get returns the element at the given index. 33 | func (a *Array) Get(index int64) *Value { 34 | if a == nil || a.Value == nil { 35 | return nil 36 | } 37 | 38 | return a.GetPropertyIndex(index) 39 | } 40 | 41 | // Push appends elements to the array and returns the new length. 42 | func (a *Array) Push(elements ...*Value) int64 { 43 | ret, err := a.InvokeJS("push", elements...) 44 | if err != nil { 45 | panic(fmt.Errorf("failed to invoke push on Array: %w", err)) 46 | } 47 | 48 | defer ret.Free() 49 | 50 | return ret.Int64() 51 | } 52 | 53 | // Set updates the element at the given index. 54 | func (a *Array) Set(index int64, value *Value) { 55 | if a == nil || a.Value == nil { 56 | return 57 | } 58 | 59 | a.SetPropertyIndex(index, value) 60 | } 61 | 62 | // Delete removes the element at the given index. 63 | func (a *Array) Delete(index int64) bool { 64 | if a == nil || a.Value == nil { 65 | return false 66 | } 67 | 68 | indexValue := a.context.NewInt64(index) 69 | defer indexValue.Free() 70 | 71 | countValue := a.context.NewInt64(1) 72 | defer countValue.Free() 73 | 74 | removeList, err := a.InvokeJS("splice", indexValue, countValue) 75 | if err != nil { 76 | panic(fmt.Errorf("failed to invoke splice on Array: %w", err)) 77 | } 78 | 79 | defer removeList.Free() 80 | 81 | return removeList.IsArray() && removeList.Len() > 0 82 | } 83 | 84 | // Map provides a wrapper around JavaScript Map objects with Go-like methods. 85 | type Map struct { 86 | *Value 87 | } 88 | 89 | // NewMap wraps a JavaScript Map value in a Go Map type. 90 | func NewMap(value *Value) *Map { 91 | if value == nil || !value.IsMap() { 92 | return nil 93 | } 94 | 95 | return &Map{Value: value} 96 | } 97 | 98 | // JSONStringify returns the JSON representation of the Map as an object. 99 | func (m *Map) JSONStringify() (string, error) { 100 | object := m.CreateObject() 101 | defer object.Free() 102 | 103 | return object.JSONStringify() 104 | } 105 | 106 | // IsMap returns true if this is a valid Map. 107 | // Mainly used to satisfy ObjectOrMap. 108 | func (m *Map) IsMap() bool { 109 | return m != nil && m.Value != nil 110 | } 111 | 112 | // IsObject returns true if this is a valid Map, mainly used to satisfy ObjectOrMap. 113 | func (m *Map) IsObject() bool { 114 | return m != nil && m.Value != nil 115 | } 116 | 117 | // ToMap returns this Map instance, mainly used to satisfy ObjectOrMap. 118 | func (m *Map) ToMap() *Map { 119 | return m 120 | } 121 | 122 | // Get retrieves the value for the given key. 123 | func (m *Map) Get(key *Value) *Value { 124 | if m == nil || m.Value == nil || key == nil { 125 | return nil 126 | } 127 | 128 | v, err := m.InvokeJS("get", key) 129 | if err != nil { 130 | panic(fmt.Errorf("failed to invoke get on Map: %w", err)) 131 | } 132 | 133 | return v 134 | } 135 | 136 | // Set sets the value for the given key. 137 | func (m *Map) Set(key, value *Value) { 138 | if m == nil || m.Value == nil || key == nil || value == nil { 139 | return 140 | } 141 | 142 | result, err := m.InvokeJS("set", key, value) 143 | if err != nil { 144 | panic(fmt.Errorf("failed to invoke set on Map: %w", err)) 145 | } 146 | 147 | result.Free() 148 | } 149 | 150 | // Delete removes the key-value pair. 151 | func (m *Map) Delete(key *Value) { 152 | if m == nil || m.Value == nil || key == nil { 153 | return 154 | } 155 | 156 | result, err := m.InvokeJS("delete", key) 157 | if err != nil { 158 | panic(fmt.Errorf("failed to invoke delete on Map: %w", err)) 159 | } 160 | 161 | result.Free() 162 | } 163 | 164 | // Has returns true if the key exists. 165 | func (m *Map) Has(key *Value) bool { 166 | if m == nil || m.Value == nil || key == nil { 167 | return false 168 | } 169 | 170 | boolValue, err := m.InvokeJS("has", key) 171 | if err != nil { 172 | panic(fmt.Errorf("failed to invoke has on Map: %w", err)) 173 | } 174 | 175 | defer boolValue.Free() 176 | 177 | return boolValue.Bool() 178 | } 179 | 180 | // ForEach calls forFn for each key-value pair. 181 | func (m *Map) ForEach(forFn func(key, value *Value)) { 182 | if m == nil || m.Value == nil || forFn == nil { 183 | return 184 | } 185 | 186 | forEachFn := m.context.Function(func(t *This) (*Value, error) { 187 | args := t.Args() 188 | if len(args) >= MinMapForeachArgs { 189 | forFn(args[1], args[0]) 190 | } 191 | 192 | return nil, nil 193 | }) 194 | defer forEachFn.Free() 195 | 196 | val, err := m.InvokeJS("forEach", forEachFn) 197 | if err != nil { 198 | panic(fmt.Errorf("failed to invoke forEach on Map: %w", err)) 199 | } 200 | 201 | val.Free() 202 | } 203 | 204 | // CreateObject converts the Map to a JavaScript object. 205 | func (m *Map) CreateObject() *Value { 206 | if m == nil || m.Value == nil { 207 | return nil 208 | } 209 | 210 | object := m.context.NewObject() 211 | m.ForEach(func(key, value *Value) { 212 | object.SetProperty(key, value) 213 | }) 214 | 215 | return object 216 | } 217 | 218 | // Set provides a wrapper around JavaScript Set objects with Go-like methods. 219 | type Set struct { 220 | *Value 221 | } 222 | 223 | // NewSet wraps a JavaScript Set value in a Go Set type. 224 | func NewSet(value *Value) *Set { 225 | if value == nil || !value.IsSet() { 226 | return nil 227 | } 228 | 229 | return &Set{Value: value} 230 | } 231 | 232 | // JSONStringify returns the JSON representation of the Set as an array. 233 | func (s *Set) JSONStringify() (string, error) { 234 | arr := s.ToArray() 235 | 236 | defer arr.Free() 237 | 238 | return arr.JSONStringify() 239 | } 240 | 241 | // Add adds a value to the Set. 242 | func (s *Set) Add(value *Value) { 243 | if s == nil || s.Value == nil || value == nil { 244 | return 245 | } 246 | 247 | v, err := s.InvokeJS("add", value) 248 | if err != nil { 249 | panic(fmt.Errorf("failed to invoke add on Set: %w", err)) 250 | } 251 | 252 | defer v.Free() 253 | } 254 | 255 | // Delete removes the value from the Set. 256 | func (s *Set) Delete(value *Value) { 257 | if s == nil || s.Value == nil || value == nil { 258 | return 259 | } 260 | 261 | v, err := s.InvokeJS("delete", value) 262 | if err != nil { 263 | panic(fmt.Errorf("failed to invoke delete on Set: %w", err)) 264 | } 265 | 266 | defer v.Free() 267 | } 268 | 269 | // Has returns true if the value exists. 270 | func (s *Set) Has(value *Value) bool { 271 | if s == nil || s.Value == nil || value == nil { 272 | return false 273 | } 274 | 275 | v, err := s.InvokeJS("has", value) 276 | if err != nil { 277 | panic(fmt.Errorf("failed to invoke has on Set: %w", err)) 278 | } 279 | 280 | defer v.Free() 281 | 282 | return v.Bool() 283 | } 284 | 285 | // ToArray converts the Set to an Array. 286 | func (s *Set) ToArray() *Array { 287 | if s == nil || s.Value == nil { 288 | return nil 289 | } 290 | 291 | array := s.context.NewArray() 292 | s.ForEach(func(value *Value) { 293 | array.Push(value.Clone()) 294 | }) 295 | 296 | return array 297 | } 298 | 299 | // ForEach calls forFn for each value. 300 | func (s *Set) ForEach(forFn func(value *Value)) { 301 | if s == nil || s.Value == nil || forFn == nil { 302 | return 303 | } 304 | 305 | forEachFn := s.context.Function(func(t *This) (*Value, error) { 306 | args := t.Args() 307 | if len(args) > 0 { 308 | forFn(args[0]) 309 | } 310 | 311 | return t.NewUndefined(), nil 312 | }) 313 | defer forEachFn.Free() 314 | 315 | value, err := s.InvokeJS("forEach", forEachFn) 316 | if err != nil { 317 | panic(fmt.Errorf("failed to invoke forEach on Set: %w", err)) 318 | } 319 | 320 | defer value.Free() 321 | } 322 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestCombineErrors(t *testing.T) { 13 | t.Run("empty slice returns nil", func(t *testing.T) { 14 | result := combineErrors() 15 | assert.Nil(t, result) 16 | }) 17 | 18 | t.Run("single error", func(t *testing.T) { 19 | err := errors.New("test error") 20 | result := combineErrors(err) 21 | assert.Error(t, result) 22 | assert.Contains(t, result.Error(), "test error") 23 | }) 24 | 25 | t.Run("multiple errors", func(t *testing.T) { 26 | err1 := errors.New("error 1") 27 | err2 := errors.New("error 2") 28 | result := combineErrors(err1, err2) 29 | assert.Error(t, result) 30 | assert.Contains(t, result.Error(), "error 1") 31 | assert.Contains(t, result.Error(), "error 2") 32 | }) 33 | 34 | t.Run("mixed nil and non-nil errors", func(t *testing.T) { 35 | err1 := errors.New("error 1") 36 | var err2 error // nil 37 | err3 := errors.New("error 3") 38 | result := combineErrors(err1, err2, err3) 39 | assert.Error(t, result) 40 | assert.Contains(t, result.Error(), "error 1") 41 | assert.Contains(t, result.Error(), "error 3") 42 | }) 43 | 44 | t.Run("all nil errors", func(t *testing.T) { 45 | var err1, err2, err3 error // all nil 46 | result := combineErrors(err1, err2, err3) 47 | assert.Error(t, result) 48 | assert.Equal(t, "", result.Error()) // When all errors are nil, empty string 49 | }) 50 | } 51 | 52 | func TestNewJsStringifyErr(t *testing.T) { 53 | t.Run("basic error wrapping", func(t *testing.T) { 54 | originalErr := errors.New("stringify failed") 55 | result := newJsStringifyErr("test", originalErr) 56 | 57 | assert.Error(t, result) 58 | assert.Contains(t, result.Error(), "js test:") 59 | assert.Contains(t, result.Error(), "stringify failed") 60 | assert.True(t, errors.Is(result, originalErr)) 61 | }) 62 | 63 | t.Run("different kinds", func(t *testing.T) { 64 | originalErr := errors.New("conversion error") 65 | 66 | result1 := newJsStringifyErr("array", originalErr) 67 | assert.Contains(t, result1.Error(), "js array:") 68 | 69 | result2 := newJsStringifyErr("object", originalErr) 70 | assert.Contains(t, result2.Error(), "js object:") 71 | }) 72 | } 73 | 74 | func TestNewProxyErr(t *testing.T) { 75 | t.Run("error type input", func(t *testing.T) { 76 | originalErr := errors.New("original error") 77 | result := newProxyErr(123, originalErr) 78 | 79 | assert.Error(t, result) 80 | assert.Contains(t, result.Error(), "functionProxy [123]:") 81 | assert.Contains(t, result.Error(), "original error") 82 | assert.True(t, errors.Is(result, originalErr)) 83 | // Should contain stack trace 84 | assert.Contains(t, result.Error(), "TestNewProxyErr") 85 | }) 86 | 87 | t.Run("string type input", func(t *testing.T) { 88 | result := newProxyErr(456, "string error") 89 | 90 | assert.Error(t, result) 91 | assert.Contains(t, result.Error(), "functionProxy [456]: string error") 92 | // Should contain stack trace 93 | assert.Contains(t, result.Error(), "TestNewProxyErr") 94 | }) 95 | 96 | t.Run("other type input", func(t *testing.T) { 97 | result := newProxyErr(789, 42) 98 | 99 | assert.Error(t, result) 100 | assert.Contains(t, result.Error(), "functionProxy [789]: 42") 101 | // Should contain stack trace 102 | assert.Contains(t, result.Error(), "TestNewProxyErr") 103 | }) 104 | 105 | t.Run("contains debug stack", func(t *testing.T) { 106 | result := newProxyErr(999, "test") 107 | 108 | // Verify the stack trace is included 109 | stackLines := strings.Split(result.Error(), "\n") 110 | found := false 111 | for _, line := range stackLines { 112 | if strings.Contains(line, "TestNewProxyErr") { 113 | found = true 114 | break 115 | } 116 | } 117 | assert.True(t, found, "Stack trace should contain test function name") 118 | }) 119 | } 120 | 121 | func TestNewJsToGoErr_JSONStringifyFailure(t *testing.T) { 122 | // This test requires a runtime to create a Value that fails JSONStringify 123 | rt, err := New() 124 | require.NoError(t, err) 125 | defer rt.Close() 126 | 127 | // Create a Value that will fail JSONStringify (circular reference) 128 | result, err := rt.Eval("test.js", Code(` 129 | const obj = {}; 130 | obj.self = obj; // circular reference 131 | obj 132 | `)) 133 | require.NoError(t, err) 134 | defer result.Free() 135 | 136 | // This should trigger the JSONStringify failure path 137 | jsErr := newJsToGoErr(result, errors.New("conversion failed"), "test details") 138 | 139 | assert.Error(t, jsErr) 140 | assert.Contains(t, jsErr.Error(), "conversion failed") 141 | assert.Contains(t, jsErr.Error(), "test details") 142 | // Should contain fallback string representation 143 | assert.Contains(t, jsErr.Error(), "[object Object]") 144 | } 145 | 146 | func TestNewJsToGoErr_EmptyErrorConditions(t *testing.T) { 147 | rt, err := New() 148 | require.NoError(t, err) 149 | defer rt.Close() 150 | 151 | ctx := rt.Context() 152 | 153 | t.Run("with nil error", func(t *testing.T) { 154 | value := ctx.NewString("test") 155 | defer value.Free() 156 | 157 | result := newJsToGoErr(value, nil, "detail") 158 | assert.Error(t, result) 159 | assert.Contains(t, result.Error(), "cannot convert JS detail") 160 | assert.NotContains(t, result.Error(), ":") 161 | }) 162 | 163 | t.Run("with undefined value", func(t *testing.T) { 164 | value := ctx.NewUndefined() 165 | defer value.Free() 166 | 167 | result := newJsToGoErr(value, errors.New("test"), "detail") 168 | assert.Error(t, result) 169 | assert.Contains(t, result.Error(), "Undefined") 170 | }) 171 | 172 | t.Run("with null value", func(t *testing.T) { 173 | value := ctx.NewNull() 174 | defer value.Free() 175 | 176 | result := newJsToGoErr(value, errors.New("test"), "detail") 177 | assert.Error(t, result) 178 | assert.Contains(t, result.Error(), "Null") 179 | }) 180 | } 181 | 182 | func createCircularValue(ctx *Context) *Value { 183 | result, err := ctx.Eval("test.js", Code(` 184 | const obj = {}; 185 | obj.self = obj; 186 | obj 187 | `)) 188 | if err != nil { 189 | panic(err) 190 | } 191 | return result 192 | } 193 | 194 | func TestNewJsToGoErr(t *testing.T) { 195 | rt, err := New() 196 | require.NoError(t, err) 197 | defer rt.Close() 198 | 199 | ctx := rt.Context() 200 | 201 | t.Run("JSONStringifyFailure", func(t *testing.T) { 202 | // Create a value that will cause JSONStringify to fail 203 | circularValue := createCircularValue(ctx) 204 | defer circularValue.Free() 205 | 206 | // This should hit the JSONStringify error path and use fallback 207 | result := newJsToGoErr(circularValue, errors.New("test error")) 208 | assert.Error(t, result) 209 | assert.Contains(t, result.Error(), "test error") 210 | // Should use String() as fallback when JSONStringify fails 211 | assert.Contains(t, result.Error(), "[object Object]") 212 | }) 213 | 214 | t.Run("JSONStringifySuccess", func(t *testing.T) { 215 | value := ctx.NewString("hello") 216 | defer value.Free() 217 | 218 | result := newJsToGoErr(value, errors.New("test error")) 219 | assert.Error(t, result) 220 | assert.Contains(t, result.Error(), `"hello"`) 221 | assert.Contains(t, result.Error(), "test error") 222 | }) 223 | } 224 | 225 | func TestNewInvalidJsInputErr_JSONStringifyFailure(t *testing.T) { 226 | rt, err := New() 227 | require.NoError(t, err) 228 | defer rt.Close() 229 | 230 | // Create a Value that will fail JSONStringify (circular reference) 231 | circularValue, err := rt.Eval("test.js", Code(` 232 | const obj = {}; 233 | obj.self = obj; // circular reference 234 | obj 235 | `)) 236 | require.NoError(t, err) 237 | defer circularValue.Free() 238 | 239 | // This should trigger the JSONStringify failure path in newInvalidJsInputErr 240 | result := newInvalidJsInputErr("array", circularValue) 241 | 242 | assert.Error(t, result) 243 | assert.Contains(t, result.Error(), "expected JS array") 244 | assert.Contains(t, result.Error(), "JSONStringify failed:") 245 | assert.Contains(t, result.Error(), "[object Object]") // Fallback String() representation 246 | } 247 | 248 | func TestNewInvalidJsInputErr_SuccessfulJSONStringify(t *testing.T) { 249 | rt, err := New() 250 | require.NoError(t, err) 251 | defer rt.Close() 252 | 253 | ctx := rt.Context() 254 | 255 | // Create a simple value that JSONStringify will work on 256 | value := ctx.NewString("test string") 257 | defer value.Free() 258 | 259 | result := newInvalidJsInputErr("number", value) 260 | 261 | assert.Error(t, result) 262 | assert.Contains(t, result.Error(), "expected JS number") 263 | assert.Contains(t, result.Error(), `"test string"`) // Successful JSONStringify 264 | assert.NotContains(t, result.Error(), "JSONStringify failed") 265 | } 266 | -------------------------------------------------------------------------------- /mem.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "sync" 7 | 8 | "github.com/tetratelabs/wazero/api" 9 | ) 10 | 11 | // Mem provides a safe interface for WebAssembly memory operations. 12 | // It wraps the underlying wazero api.Memory with bounds checking and error handling. 13 | type Mem struct { 14 | mu sync.Mutex 15 | mem api.Memory 16 | } 17 | 18 | // Size returns the current size of the WebAssembly memory in bytes. 19 | func (m *Mem) Size() uint32 { 20 | return m.mem.Size() 21 | } 22 | 23 | // UnpackPtr extracts address and size from a packed 64-bit value in memory. 24 | // It reads 8 bytes from the memory address specified by packedPtr, reconstructs 25 | // the original uint64 value, and then extracts the 32-bit address from the high bits 26 | // and the 32-bit size from the low bits. 27 | // 28 | // Maintains original signature for backward compatibility - panics on error. 29 | func (m *Mem) UnpackPtr(packedPtr uint64) (uint32, uint32) { 30 | if packedPtr == 0 { 31 | return 0, 0 32 | } 33 | 34 | m.mu.Lock() 35 | defer m.mu.Unlock() 36 | 37 | packedBytes, err := m.Read(uint32(packedPtr), PackedPtrSize) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // Reconstruct the packed value from little-endian bytes 43 | packed := uint64(0) 44 | for i := range PackedPtrSize { 45 | packed |= uint64(packedBytes[i]) << (i * 8) 46 | } 47 | 48 | // Extract address (high 32 bits) and size (low 32 bits) 49 | addr := uint32(packed >> 32) 50 | size := uint32(packed) 51 | 52 | return addr, size 53 | } 54 | 55 | // Read extracts bytes from WebAssembly memory at the specified address. 56 | // Performs comprehensive validation and bounds checking. 57 | func (m *Mem) Read(addr uint32, size uint64) ([]byte, error) { 58 | err := m.validateMemoryAccess(addr, size) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if size == 0 { 64 | return []byte{}, nil 65 | } 66 | 67 | sizeU32 := uint32(size) 68 | 69 | buf, ok := m.mem.Read(addr, sizeU32) 70 | if !ok { 71 | return nil, ErrIndexOutOfRange 72 | } 73 | 74 | return buf, nil 75 | } 76 | 77 | // MustRead is like Read but panics on error. 78 | func (m *Mem) MustRead(addr uint32, size uint64) []byte { 79 | buf, err := m.Read(addr, size) 80 | if err != nil { 81 | panic(err) 82 | } 83 | 84 | return buf 85 | } 86 | 87 | // Write copies the contents of byte slice b to WebAssembly memory starting at the given address. 88 | // Uses Read to validate address and bounds before writing. 89 | func (m *Mem) Write(addr uint32, b []byte) error { 90 | m.mu.Lock() 91 | defer m.mu.Unlock() 92 | // Validates address and bounds through Read call 93 | buf, err := m.Read(addr, uint64(len(b))) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | copy(buf, b) 99 | 100 | return nil 101 | } 102 | 103 | // MustWrite is like Write but panics on error. 104 | func (m *Mem) MustWrite(addr uint32, b []byte) { 105 | err := m.Write(addr, b) 106 | if err != nil { 107 | panic(err) 108 | } 109 | } 110 | 111 | // ReadUint8 reads a single byte from WebAssembly memory at the specified address. 112 | func (m *Mem) ReadUint8(ptr uint32) (uint8, error) { 113 | err := m.validatePointer(ptr) 114 | if err != nil { 115 | return 0, err 116 | } 117 | 118 | v, ok := m.mem.ReadByte(ptr) 119 | if !ok { 120 | return 0, ErrIndexOutOfRange 121 | } 122 | 123 | return v, nil 124 | } 125 | 126 | // ReadUint32 reads a 32-bit unsigned integer from WebAssembly memory at the specified address. 127 | func (m *Mem) ReadUint32(ptr uint32) (uint32, error) { 128 | err := m.validatePointer(ptr) 129 | if err != nil { 130 | return 0, err 131 | } 132 | 133 | v, ok := m.mem.ReadUint32Le(ptr) 134 | if !ok { 135 | return 0, ErrIndexOutOfRange 136 | } 137 | 138 | return v, nil 139 | } 140 | 141 | // ReadUint64 reads a 64-bit unsigned integer from WebAssembly memory at the specified address. 142 | func (m *Mem) ReadUint64(ptr uint32) (uint64, error) { 143 | err := m.validatePointer(ptr) 144 | if err != nil { 145 | return 0, err 146 | } 147 | 148 | v, ok := m.mem.ReadUint64Le(ptr) 149 | if !ok { 150 | return 0, ErrIndexOutOfRange 151 | } 152 | 153 | return v, nil 154 | } 155 | 156 | // ReadFloat64 reads a 64-bit floating point number from WebAssembly memory at the specified address. 157 | func (m *Mem) ReadFloat64(ptr uint32) (float64, error) { 158 | uint64Val, err := m.ReadUint64(ptr) 159 | if err != nil { 160 | return 0, err 161 | } 162 | 163 | return math.Float64frombits(uint64Val), nil 164 | } 165 | 166 | // WriteUint8 writes a single byte to WebAssembly memory at the specified address. 167 | func (m *Mem) WriteUint8(ptr uint32, v uint8) error { 168 | err := m.validatePointer(ptr) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | if ok := m.mem.WriteByte(ptr, v); !ok { 174 | return ErrIndexOutOfRange 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // WriteUint32 writes a 32-bit unsigned integer to WebAssembly memory at the specified address. 181 | func (m *Mem) WriteUint32(ptr uint32, v uint32) error { 182 | err := m.validatePointer(ptr) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | if ok := m.mem.WriteUint32Le(ptr, v); !ok { 188 | return ErrIndexOutOfRange 189 | } 190 | 191 | return nil 192 | } 193 | 194 | // WriteUint64 writes a 64-bit unsigned integer to WebAssembly memory at the specified address. 195 | func (m *Mem) WriteUint64(ptr uint32, v uint64) error { 196 | err := m.validatePointer(ptr) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | if ok := m.mem.WriteUint64Le(ptr, v); !ok { 202 | return ErrIndexOutOfRange 203 | } 204 | 205 | return nil 206 | } 207 | 208 | // WriteFloat64 writes a 64-bit floating point number to WebAssembly memory at the specified address. 209 | func (m *Mem) WriteFloat64(ptr uint32, v float64) error { 210 | return m.WriteUint64(ptr, math.Float64bits(v)) 211 | } 212 | 213 | // ReadString reads a null-terminated string from WebAssembly memory starting at the given address. 214 | // It reads up to maxlen bytes and returns the string without the null terminator. 215 | func (m *Mem) ReadString(addr, maxlen uint32) (string, error) { 216 | err := m.validatePointer(addr) 217 | if err != nil { 218 | return "", err 219 | } 220 | 221 | if maxlen == 0 { 222 | return "", nil 223 | } 224 | 225 | readLen := m.calculateSafeReadLength(addr, maxlen) 226 | 227 | buf, ok := m.mem.Read(addr, readLen) 228 | if !ok { 229 | return "", ErrIndexOutOfRange 230 | } 231 | 232 | // Find null terminator 233 | nullIndex := bytes.IndexByte(buf, StringTerminator) 234 | if nullIndex < 0 { 235 | return "", ErrNoNullTerminator 236 | } 237 | 238 | return string(buf[:nullIndex]), nil 239 | } 240 | 241 | // WriteString writes a null-terminated string to WebAssembly memory. 242 | // It copies the string content to the specified memory address and appends a null terminator. 243 | func (m *Mem) WriteString(ptr uint32, s string) error { 244 | size := uint64(len(s) + 1) // +1 for null terminator 245 | 246 | // Validates address and bounds through Read call 247 | buf, err := m.Read(ptr, size) 248 | if err != nil { 249 | return err 250 | } 251 | 252 | copy(buf, s) 253 | buf[len(s)] = StringTerminator 254 | 255 | return nil 256 | } 257 | 258 | // StringFromPackedPtr reads a string from a packed pointer containing address and size. 259 | // Maintains original signature for backward compatibility - panics on error. 260 | func (m *Mem) StringFromPackedPtr(ptr uint64) string { 261 | addr, size := m.UnpackPtr(ptr) 262 | 263 | str, err := m.ReadString(addr, size) 264 | if err != nil { 265 | panic(err) 266 | } 267 | 268 | return str 269 | } 270 | 271 | // validatePointer checks if a pointer is valid (non-null). 272 | func (m *Mem) validatePointer(ptr uint32) error { 273 | if ptr == NullPtr { 274 | return ErrInvalidPointer 275 | } 276 | 277 | return nil 278 | } 279 | 280 | // validateSize checks if a size is within valid bounds. 281 | func (m *Mem) validateSize(size uint64) error { 282 | if size > math.MaxUint32 { 283 | return ErrIndexOutOfRange 284 | } 285 | 286 | return nil 287 | } 288 | 289 | // validateMemoryAccess performs comprehensive validation for memory operations. 290 | func (m *Mem) validateMemoryAccess(ptr uint32, size uint64) error { 291 | err := m.validatePointer(ptr) 292 | if err != nil { 293 | return err 294 | } 295 | 296 | if err = m.validateSize(size); err != nil { 297 | return err 298 | } 299 | 300 | return nil 301 | } 302 | 303 | // calculateSafeReadLength calculates a safe read length for string operations. 304 | func (m *Mem) calculateSafeReadLength(addr, maxlen uint32) uint32 { 305 | memSize := m.mem.Size() 306 | 307 | if maxlen == math.MaxUint32 { 308 | return memSize - addr 309 | } 310 | 311 | // Calculate safe read length including null terminator space 312 | available := memSize - addr 313 | if available <= maxlen { 314 | return available 315 | } 316 | 317 | return maxlen + 1 318 | } 319 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package qjs_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "unsafe" 7 | 8 | "github.com/fastschema/qjs" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestIsConvertibleToJs(t *testing.T) { 13 | t.Run("BasicTypes", func(t *testing.T) { 14 | basicTypes := []struct { 15 | name string 16 | value any 17 | expected bool 18 | }{ 19 | {"int", 42, true}, 20 | {"string", "hello", true}, 21 | {"bool", true, true}, 22 | {"float64", 3.14, true}, 23 | {"[]int", []int{1, 2, 3}, true}, 24 | {"[]unsafe.Pointer", []unsafe.Pointer{}, false}, 25 | {"map[string]int", map[string]int{"key": 1}, true}, 26 | {"map[unsafe.Pointer]string", map[unsafe.Pointer]string{}, false}, 27 | {"map[string]unsafe.Pointer", map[string]unsafe.Pointer{}, false}, 28 | {"chan int", make(chan int), true}, 29 | } 30 | 31 | for _, tt := range basicTypes { 32 | t.Run(tt.name, func(t *testing.T) { 33 | err := qjs.IsConvertibleToJs(reflect.TypeOf(tt.value), make(map[reflect.Type]bool), "test") 34 | if tt.expected { 35 | assert.NoError(t, err, "Expected %s to be convertible", tt.name) 36 | } else { 37 | assert.Error(t, err, "Expected %s to not be convertible", tt.name) 38 | } 39 | }) 40 | } 41 | }) 42 | 43 | t.Run("UnsupportedTypes", func(t *testing.T) { 44 | t.Run("unsafe_pointer", func(t *testing.T) { 45 | unsafePtr := unsafe.Pointer(&[]byte{1, 2, 3}[0]) 46 | err := qjs.IsConvertibleToJs(reflect.TypeOf(unsafePtr), make(map[reflect.Type]bool), "test") 47 | assert.Error(t, err, "unsafe.Pointer should not be convertible") 48 | assert.Contains(t, err.Error(), "unsafe.Pointer", "Error should mention unsafe.Pointer") 49 | }) 50 | }) 51 | 52 | t.Run("StructWithFields", func(t *testing.T) { 53 | t.Run("ExportedFields", func(t *testing.T) { 54 | type TestStruct struct { 55 | PublicField string 56 | AnotherField int 57 | } 58 | 59 | err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test") 60 | assert.NoError(t, err, "Struct with exported fields should be convertible") 61 | }) 62 | 63 | t.Run("UnexportedFieldsIgnored", func(t *testing.T) { 64 | type TestStruct struct { 65 | PublicField string 66 | privateField string 67 | } 68 | 69 | err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test") 70 | assert.NoError(t, err, "Struct with unexported fields should be convertible (unexported fields ignored)") 71 | }) 72 | 73 | t.Run("MixedFieldsWithUnsupportedPrivate", func(t *testing.T) { 74 | type TestStruct struct { 75 | PublicField string 76 | privateField unsafe.Pointer 77 | } 78 | 79 | err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test") 80 | assert.NoError(t, err, "Struct should be convertible when unsupported types are in unexported fields") 81 | }) 82 | }) 83 | 84 | t.Run("StructWithJSONTags", func(t *testing.T) { 85 | t.Run("JSONOmitTag", func(t *testing.T) { 86 | type TestStruct struct { 87 | PublicField string 88 | OmittedField unsafe.Pointer `json:"-"` 89 | AnotherField int 90 | } 91 | 92 | err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test") 93 | assert.NoError(t, err, "Struct should be convertible when unsupported types have json:\"-\" tag") 94 | }) 95 | 96 | t.Run("JSONRenameTag", func(t *testing.T) { 97 | type TestStruct struct { 98 | Field string `json:"renamed_field"` 99 | UnsupportedField unsafe.Pointer `json:"-"` 100 | } 101 | 102 | err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test") 103 | assert.NoError(t, err, "Struct should be convertible with JSON rename tags") 104 | }) 105 | 106 | t.Run("UnsupportedFieldWithoutOmitTag", func(t *testing.T) { 107 | type TestStruct struct { 108 | PublicField string 109 | UnsupportedField unsafe.Pointer 110 | } 111 | 112 | err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test") 113 | assert.Error(t, err, "Struct should not be convertible with exported unsupported fields") 114 | assert.Contains(t, err.Error(), "TestStruct.UnsupportedField", "Error should mention the problematic field") 115 | }) 116 | 117 | t.Run("ComplexJSONTags", func(t *testing.T) { 118 | type TestStruct struct { 119 | Field1 string `json:"field1,omitempty"` 120 | Field2 int `json:"field2"` 121 | OmittedField unsafe.Pointer `json:"-"` 122 | AnotherOmitted unsafe.Pointer `json:"-,"` 123 | } 124 | 125 | err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test") 126 | assert.NoError(t, err, "Struct should handle complex JSON tags correctly") 127 | }) 128 | }) 129 | 130 | t.Run("RecursiveTypes", func(t *testing.T) { 131 | type RecursiveStruct struct { 132 | Name string 133 | Self *RecursiveStruct 134 | } 135 | 136 | err := qjs.IsConvertibleToJs(reflect.TypeOf(RecursiveStruct{}), make(map[reflect.Type]bool), "test") 137 | assert.NoError(t, err, "Recursive struct should be convertible") 138 | }) 139 | 140 | t.Run("NestedStructs", func(t *testing.T) { 141 | t.Run("ValidNested", func(t *testing.T) { 142 | type Inner struct { 143 | Value int 144 | } 145 | type Outer struct { 146 | Inner Inner 147 | Name string 148 | } 149 | 150 | err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test") 151 | assert.NoError(t, err, "Nested convertible structs should be convertible") 152 | }) 153 | 154 | t.Run("InvalidNested", func(t *testing.T) { 155 | type Inner struct { 156 | BadField unsafe.Pointer 157 | } 158 | type Outer struct { 159 | Inner Inner 160 | Name string 161 | } 162 | 163 | err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test") 164 | assert.Error(t, err, "Nested struct with unsupported fields should not be convertible") 165 | }) 166 | 167 | t.Run("NestedWithOmittedField", func(t *testing.T) { 168 | type Inner struct { 169 | GoodField string 170 | BadField chan int `json:"-"` 171 | } 172 | type Outer struct { 173 | Inner Inner 174 | Name string 175 | } 176 | 177 | err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test") 178 | assert.NoError(t, err, "Nested struct with omitted unsupported fields should be convertible") 179 | }) 180 | 181 | t.Run("EmbeddedPointerStruct", func(t *testing.T) { 182 | type Inner struct { 183 | GoodField string 184 | } 185 | type Outer struct { 186 | *Inner 187 | Name string 188 | } 189 | 190 | err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test") 191 | assert.NoError(t, err, "Struct with embedded pointer to struct should be convertible") 192 | }) 193 | 194 | t.Run("EmbeddedPointerWithUnsupportedField", func(t *testing.T) { 195 | type Inner struct { 196 | BadField unsafe.Pointer 197 | } 198 | type Outer struct { 199 | *Inner 200 | Name string 201 | } 202 | 203 | err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test") 204 | assert.Error(t, err, "Struct with embedded pointer containing unsupported fields should not be convertible") 205 | }) 206 | 207 | t.Run("EmbeddedPointerWithOmittedField", func(t *testing.T) { 208 | type Inner struct { 209 | GoodField string 210 | BadField chan int `json:"-"` 211 | } 212 | type Outer struct { 213 | *Inner 214 | Name string 215 | } 216 | 217 | err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test") 218 | assert.NoError(t, err, "Struct with embedded pointer containing omitted unsupported fields should be convertible") 219 | }) 220 | }) 221 | 222 | t.Run("Pointers", func(t *testing.T) { 223 | type TestStruct struct { 224 | Field string 225 | OmittedField unsafe.Pointer `json:"-"` 226 | } 227 | 228 | err := qjs.IsConvertibleToJs(reflect.TypeOf(&TestStruct{}), make(map[reflect.Type]bool), "test") 229 | assert.NoError(t, err, "Pointer to convertible struct should be convertible") 230 | }) 231 | } 232 | 233 | func TestGetGoTypeName(t *testing.T) { 234 | // Test input handling - reflect.Type vs value 235 | assert.Equal(t, "int", qjs.GetGoTypeName(reflect.TypeOf(42))) 236 | assert.Equal(t, "int", qjs.GetGoTypeName(42)) 237 | 238 | // Test nil input panics 239 | assert.Panics(t, func() { qjs.GetGoTypeName(nil) }) 240 | 241 | // Test pointer branch 242 | var intPtr *int 243 | assert.Equal(t, "*int", qjs.GetGoTypeName(intPtr)) 244 | 245 | // Test slice branch 246 | var intSlice []int 247 | assert.Equal(t, "[]int", qjs.GetGoTypeName(intSlice)) 248 | 249 | // Test array branch 250 | var intArray [5]int 251 | assert.Equal(t, "[5]int", qjs.GetGoTypeName(intArray)) 252 | 253 | // Test map branch 254 | var stringIntMap map[string]int 255 | assert.Equal(t, "map[string]int", qjs.GetGoTypeName(stringIntMap)) 256 | 257 | // Test channel branch 258 | var intChan chan int 259 | assert.Equal(t, "chan int", qjs.GetGoTypeName(intChan)) 260 | 261 | // Test function branch 262 | var simpleFunc func() 263 | assert.Equal(t, "func()", qjs.GetGoTypeName(simpleFunc)) 264 | 265 | // Test default case (basic types) 266 | assert.Equal(t, "string", qjs.GetGoTypeName("hello")) 267 | assert.Equal(t, "bool", qjs.GetGoTypeName(true)) 268 | assert.Equal(t, "float64", qjs.GetGoTypeName(3.14)) 269 | 270 | var goFunc func(int, ...int) (int, error) 271 | assert.Equal(t, "func(int, ...int) (int, error)", qjs.GetGoTypeName(goFunc)) 272 | } 273 | -------------------------------------------------------------------------------- /testutils_test.go: -------------------------------------------------------------------------------- 1 | package qjs_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "slices" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/fastschema/qjs" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func setupTestContext(_ *testing.T) (*qjs.Runtime, *qjs.Context) { 18 | runtime := must(qjs.New()) 19 | ctx := runtime.Context() 20 | // t.Cleanup(runtime.Close) 21 | return runtime, ctx 22 | } 23 | 24 | type CustomUnmarshaler struct { 25 | Value string 26 | } 27 | 28 | func (c *CustomUnmarshaler) UnmarshalJSON(data []byte) error { 29 | // Fail for specific input to test error handling 30 | if strings.Contains(string(data), "invalid") { 31 | return errors.New("custom unmarshaling error") 32 | } 33 | 34 | // Strip quotes for string 35 | s := string(data) 36 | if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { 37 | s = s[1 : len(s)-1] 38 | } 39 | 40 | c.Value = s 41 | return nil 42 | } 43 | 44 | type EmbeddedStruct struct { 45 | Name string 46 | Age int 47 | } 48 | 49 | type unExportedFieldStruct struct { 50 | Anything string 51 | } 52 | 53 | type NestedEmbeddedStruct struct { 54 | EmbeddedStruct 55 | unExportedFieldStruct 56 | Label string 57 | } 58 | 59 | func must[T any](val T, err error) T { 60 | if err != nil { 61 | panic(err) 62 | } 63 | return val 64 | } 65 | 66 | func extractErr[T any](_ T, err error) error { 67 | return err 68 | } 69 | 70 | func fileContent(file string) string { 71 | content := must(os.ReadFile(file)) 72 | return string(content) 73 | } 74 | 75 | func glob(path string, exts ...string) ([]string, error) { 76 | var files []string 77 | must[any](nil, filepath.Walk(path, 78 | func(path string, info os.FileInfo, err error) error { 79 | if err != nil { 80 | return err 81 | } 82 | if !info.IsDir() { 83 | if slices.Contains(exts, filepath.Ext(path)) { 84 | files = append(files, path) 85 | } 86 | } 87 | 88 | return nil 89 | }), 90 | ) 91 | 92 | return files, nil 93 | } 94 | 95 | type modeGlobalTest struct { 96 | file string 97 | wantError bool 98 | expect func(val *qjs.Value, err error) 99 | } 100 | 101 | type modeModuleTest struct { 102 | moduleDir string 103 | wantError bool 104 | expect func(val *qjs.Value, err error) 105 | } 106 | 107 | func genModeGlobalTests(t *testing.T) []modeGlobalTest { 108 | tests := []modeGlobalTest{ 109 | { 110 | file: "01_invalid_syntax.js", 111 | wantError: true, 112 | expect: func(val *qjs.Value, err error) { 113 | assert.Nil(t, val) 114 | assert.Error(t, err) 115 | }, 116 | }, 117 | { 118 | wantError: true, 119 | file: "02_throw_error.js", 120 | expect: func(val *qjs.Value, err error) { 121 | assert.Nil(t, val) 122 | assert.Error(t, err) 123 | assert.Contains(t, err.Error(), "script error") 124 | }, 125 | }, 126 | { 127 | file: "03_result_no_value.js", 128 | expect: func(val *qjs.Value, err error) { 129 | assert.True(t, val.IsUndefined()) 130 | }, 131 | }, 132 | { 133 | file: "04_result_number.js", 134 | expect: func(val *qjs.Value, err error) { 135 | assert.Equal(t, int32(3), val.Int32()) 136 | }, 137 | }, 138 | { 139 | file: "05_result_string.js", 140 | expect: func(val *qjs.Value, err error) { 141 | assert.Equal(t, "Hello, World!", val.String()) 142 | }, 143 | }, 144 | { 145 | file: "06_result_boolean.js", 146 | expect: func(val *qjs.Value, err error) { 147 | assert.True(t, val.Bool()) 148 | }, 149 | }, 150 | { 151 | file: "07_result_null.js", 152 | expect: func(val *qjs.Value, err error) { 153 | assert.True(t, val.IsNull()) 154 | }, 155 | }, 156 | { 157 | file: "08_result_undefined.js", 158 | expect: func(val *qjs.Value, err error) { 159 | assert.True(t, val.IsUndefined()) 160 | }, 161 | }, 162 | { 163 | file: "09_result_object.js", 164 | expect: func(val *qjs.Value, err error) { 165 | assert.True(t, val.IsObject()) 166 | obj := val.Object() 167 | defer obj.Free() 168 | assert.Equal(t, int32(1), obj.GetPropertyStr("a").Int32()) 169 | assert.Equal(t, "2", obj.GetPropertyStr("b").String()) 170 | assert.True(t, obj.GetPropertyStr("c").Bool()) 171 | assert.True(t, obj.GetPropertyStr("d").IsUndefined()) 172 | }, 173 | }, 174 | { 175 | wantError: true, 176 | file: "10_top_level_await_not_supported.js", 177 | expect: func(val *qjs.Value, err error) { 178 | assert.Nil(t, val) 179 | assert.Error(t, err) 180 | }, 181 | }, 182 | { 183 | file: "11_empty_script.js", 184 | expect: func(val *qjs.Value, err error) { 185 | // An empty script should return undefined without error. 186 | assert.True(t, val.IsUndefined()) 187 | assert.NoError(t, err) 188 | }, 189 | }, 190 | { 191 | file: "12_function.js", 192 | expect: func(val *qjs.Value, err error) { 193 | assert.True(t, val.IsFunction()) 194 | }, 195 | }, 196 | { 197 | file: "13_array.js", 198 | expect: func(val *qjs.Value, err error) { 199 | // Assuming arrays are returned as objects with numeric keys. 200 | assert.True(t, val.IsArray()) 201 | arrObj := val.Object() 202 | defer arrObj.Free() 203 | assert.Equal(t, int32(1), arrObj.GetPropertyStr("0").Int32()) 204 | assert.Equal(t, int32(2), arrObj.GetPropertyStr("1").Int32()) 205 | assert.Equal(t, int32(3), arrObj.GetPropertyStr("2").Int32()) 206 | }, 207 | }, 208 | } 209 | 210 | return tests 211 | } 212 | 213 | func runModeGlobalTests(t *testing.T, tests []modeGlobalTest, isScript bool) { 214 | for _, test := range tests { 215 | t.Run(test.file, func(t *testing.T) { 216 | runtime := must(qjs.New(qjs.Option{MaxStackSize: 512 * 1024})) 217 | defer runtime.Close() 218 | var val *qjs.Value 219 | var err error 220 | 221 | fileName := path.Join("./testdata/01_global", test.file) 222 | if isScript { 223 | script := fileContent(fileName) 224 | val, err = runtime.Eval( 225 | fileName, 226 | qjs.Code(script), 227 | ) 228 | } else { 229 | val, err = runtime.Eval(fileName) 230 | } 231 | 232 | defer val.Free() 233 | if !test.wantError { 234 | assert.NoError(t, err) 235 | } 236 | test.expect(val, err) 237 | }) 238 | } 239 | } 240 | 241 | func genModeModuleTests(t *testing.T) []modeModuleTest { 242 | tests := []modeModuleTest{ 243 | { 244 | moduleDir: "01_invalid_syntax", 245 | wantError: true, 246 | expect: func(val *qjs.Value, err error) { 247 | assert.Nil(t, val) 248 | assert.Error(t, err) 249 | }, 250 | }, 251 | { 252 | moduleDir: "02_throw_error", 253 | wantError: true, 254 | expect: func(val *qjs.Value, err error) { 255 | assert.Nil(t, val) 256 | assert.Error(t, err) 257 | assert.Contains(t, err.Error(), "module error") 258 | }, 259 | }, 260 | { 261 | moduleDir: "03_default_export_return_value", 262 | expect: func(val *qjs.Value, err error) { 263 | assert.Equal(t, int32(3), val.Int32()) 264 | }, 265 | }, 266 | { 267 | moduleDir: "04_without_default_export_return_undefined", 268 | expect: func(val *qjs.Value, err error) { 269 | assert.True(t, val.IsUndefined()) 270 | }, 271 | }, 272 | { 273 | moduleDir: "05_import_not_exist", 274 | wantError: true, 275 | expect: func(val *qjs.Value, err error) { 276 | assert.Nil(t, val) 277 | assert.Error(t, err) 278 | }, 279 | }, 280 | { 281 | moduleDir: "06_import_export", 282 | expect: func(val *qjs.Value, err error) { 283 | assert.Equal(t, int32(6), val.Int32()) 284 | }, 285 | }, 286 | { 287 | moduleDir: "07_export_fn_call", 288 | expect: func(val *qjs.Value, err error) { 289 | assert.Equal(t, int32(20), val.Int32()) 290 | }, 291 | }, 292 | { 293 | moduleDir: "08_export_empty", 294 | expect: func(val *qjs.Value, err error) { 295 | assert.True(t, val.IsUndefined()) 296 | }, 297 | }, 298 | { 299 | moduleDir: "09_side_effect", 300 | expect: func(val *qjs.Value, err error) { 301 | assert.True(t, val.IsUndefined()) 302 | }, 303 | }, 304 | } 305 | 306 | return tests 307 | } 308 | 309 | func runModeModuleTests(t *testing.T, tests []modeModuleTest, isScript bool) { 310 | for _, test := range tests { 311 | t.Run(test.moduleDir, func(t *testing.T) { 312 | runtime := must(qjs.New()) 313 | defer runtime.Close() 314 | 315 | if !isScript { 316 | moduleMainFile := filepath.Join("./testdata/02_module", test.moduleDir, "999_index.js") 317 | val, err := runtime.Eval( 318 | moduleMainFile, 319 | qjs.TypeModule(), 320 | ) 321 | defer val.Free() 322 | if !test.wantError { 323 | require.NoError(t, err) 324 | } 325 | test.expect(val, err) 326 | return 327 | } 328 | 329 | moduleDir := filepath.Join("./testdata/02_module", test.moduleDir) 330 | moduleFiles, err := glob(moduleDir, ".js") 331 | assert.NoError(t, err) 332 | 333 | // main file is the last file in the list. 334 | mainPath := moduleFiles[len(moduleFiles)-1] 335 | // Evaluate all dependencies first (all modules except the last one) 336 | depPaths := moduleFiles[:len(moduleFiles)-1] 337 | for _, depPath := range depPaths { 338 | depScript := fileContent(depPath) 339 | val, err := runtime.Load( 340 | depPath, 341 | qjs.Code(depScript), 342 | qjs.TypeModule(), 343 | ) 344 | defer val.Free() 345 | assert.NoError(t, err) 346 | } 347 | 348 | // Evaluate the main module. 349 | mainScript := fileContent(mainPath) 350 | val, err := runtime.Eval( 351 | mainPath, 352 | qjs.Code(mainScript), 353 | qjs.TypeModule(), 354 | ) 355 | defer val.Free() 356 | 357 | // Check error expectations. 358 | if !test.wantError { 359 | assert.NoError(t, err) 360 | } 361 | test.expect(val, err) 362 | }) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /handle_test.go: -------------------------------------------------------------------------------- 1 | package qjs_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/fastschema/qjs" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHandle_Basic(t *testing.T) { 12 | runtime := must(qjs.New()) 13 | defer runtime.Close() 14 | 15 | t.Run("Raw", func(t *testing.T) { 16 | handle := qjs.NewHandle(runtime, 42) 17 | assert.Equal(t, uint64(42), handle.Raw()) 18 | }) 19 | 20 | t.Run("Free", func(t *testing.T) { 21 | handle := qjs.NewHandle(runtime, 42) 22 | // Test that Free() is safe to call and doesn't panic 23 | handle.Free() 24 | // Test that double free is safe and doesn't cause issues 25 | handle.Free() 26 | }) 27 | } 28 | 29 | func TestHandle_BoolConversion(t *testing.T) { 30 | runtime := must(qjs.New()) 31 | defer runtime.Close() 32 | 33 | tests := []struct { 34 | name string 35 | value uint64 36 | expected bool 37 | }{ 38 | {"Zero", 0, false}, 39 | {"One", 1, true}, 40 | {"MaxUint64", math.MaxUint64, true}, 41 | } 42 | 43 | for _, test := range tests { 44 | t.Run(test.name, func(t *testing.T) { 45 | handle := qjs.NewHandle(runtime, test.value) 46 | assert.Equal(t, test.expected, handle.Bool()) 47 | }) 48 | } 49 | } 50 | 51 | func TestHandle_IntegerConversions(t *testing.T) { 52 | runtime := must(qjs.New()) 53 | defer runtime.Close() 54 | 55 | t.Run("Int", func(t *testing.T) { 56 | handle := qjs.NewHandle(runtime, 42) 57 | assert.Equal(t, int(42), handle.Int()) 58 | }) 59 | 60 | t.Run("Int8", func(t *testing.T) { 61 | handle := qjs.NewHandle(runtime, 127) 62 | assert.Equal(t, int8(127), handle.Int8()) 63 | }) 64 | 65 | t.Run("Int16", func(t *testing.T) { 66 | handle := qjs.NewHandle(runtime, 32767) 67 | assert.Equal(t, int16(32767), handle.Int16()) 68 | }) 69 | 70 | t.Run("Int32", func(t *testing.T) { 71 | handle := qjs.NewHandle(runtime, 2147483647) 72 | assert.Equal(t, int32(2147483647), handle.Int32()) 73 | }) 74 | 75 | t.Run("Int64", func(t *testing.T) { 76 | handle := qjs.NewHandle(runtime, 9223372036854775807) 77 | assert.Equal(t, int64(9223372036854775807), handle.Int64()) 78 | }) 79 | } 80 | 81 | func TestHandle_UintegerConversions(t *testing.T) { 82 | runtime := must(qjs.New()) 83 | defer runtime.Close() 84 | 85 | t.Run("Uint", func(t *testing.T) { 86 | handle := qjs.NewHandle(runtime, 42) 87 | assert.Equal(t, uint(42), handle.Uint()) 88 | }) 89 | 90 | t.Run("Uint8", func(t *testing.T) { 91 | handle := qjs.NewHandle(runtime, 255) 92 | assert.Equal(t, uint8(255), handle.Uint8()) 93 | }) 94 | 95 | t.Run("Uint16", func(t *testing.T) { 96 | handle := qjs.NewHandle(runtime, 65535) 97 | assert.Equal(t, uint16(65535), handle.Uint16()) 98 | }) 99 | 100 | t.Run("Uint32", func(t *testing.T) { 101 | handle := qjs.NewHandle(runtime, 4294967295) 102 | assert.Equal(t, uint32(4294967295), handle.Uint32()) 103 | }) 104 | 105 | t.Run("Uint64", func(t *testing.T) { 106 | handle := qjs.NewHandle(runtime, 18446744073709551615) 107 | assert.Equal(t, uint64(18446744073709551615), handle.Uint64()) 108 | }) 109 | 110 | t.Run("Uintptr", func(t *testing.T) { 111 | handle := qjs.NewHandle(runtime, 42) 112 | assert.Equal(t, uintptr(42), handle.Uintptr()) 113 | }) 114 | } 115 | 116 | func TestHandle_FloatConversions(t *testing.T) { 117 | runtime := must(qjs.New()) 118 | defer runtime.Close() 119 | 120 | t.Run("Float32", func(t *testing.T) { 121 | // Test IEEE 754 bit pattern interpretation for float32 122 | bits := math.Float32bits(3.14159) 123 | handle := qjs.NewHandle(runtime, uint64(bits)) 124 | assert.InDelta(t, float32(3.14159), handle.Float32(), 0.0001) 125 | }) 126 | 127 | t.Run("Float64", func(t *testing.T) { 128 | // Test IEEE 754 bit pattern interpretation for float64 129 | bits := math.Float64bits(3.14159265359) 130 | handle := qjs.NewHandle(runtime, bits) 131 | assert.InDelta(t, 3.14159265359, handle.Float64(), 0.0000000001) 132 | }) 133 | } 134 | 135 | func TestHandle_String(t *testing.T) { 136 | runtime := must(qjs.New()) 137 | defer runtime.Close() 138 | 139 | v := runtime.Context().NewString("hello world") 140 | v2 := runtime.Context().Call( 141 | "QJS_ToCString", 142 | runtime.Context().Raw(), 143 | v.Raw(), 144 | ) 145 | 146 | assert.Equal(t, "hello world", v2.Handle().String()) 147 | } 148 | 149 | func TestHandle_Bytes(t *testing.T) { 150 | runtime := must(qjs.New()) 151 | defer runtime.Close() 152 | 153 | t.Run("empty_handle", func(t *testing.T) { 154 | handle := qjs.NewHandle(runtime, 0) 155 | bytes := handle.Bytes() 156 | 157 | assert.Empty(t, bytes) 158 | assert.Nil(t, bytes) 159 | }) 160 | 161 | t.Run("non_empty_handle", func(t *testing.T) { 162 | value, err := runtime.Context().Eval("test.js", qjs.Code(`({ 163 | hello: "hello world", 164 | })`)) 165 | assert.NoError(t, err) 166 | json := runtime.Context().Call( 167 | "QJS_JSONStringify", 168 | runtime.Context().Raw(), 169 | value.Raw(), 170 | ) 171 | bytes := json.Handle().Bytes() 172 | 173 | assert.NotEmpty(t, bytes) 174 | assert.JSONEq(t, `{"hello":"hello world"}`, string(bytes)) 175 | }) 176 | } 177 | 178 | // Test coverage for uncovered areas in handle.go 179 | func TestHandle_NilChecks(t *testing.T) { 180 | runtime := must(qjs.New()) 181 | defer runtime.Close() 182 | 183 | t.Run("NewHandle_NilRuntime", func(t *testing.T) { 184 | assert.Panics(t, func() { 185 | qjs.NewHandle(nil, 42) 186 | }, "NewHandle should panic with nil runtime") 187 | }) 188 | 189 | t.Run("Free_NilHandle", func(t *testing.T) { 190 | var handle *qjs.Handle 191 | // Should not panic 192 | handle.Free() 193 | }) 194 | 195 | t.Run("IsFreed_NilHandle", func(t *testing.T) { 196 | var handle *qjs.Handle 197 | assert.True(t, handle.IsFreed(), "nil handle should be considered freed") 198 | }) 199 | 200 | t.Run("Raw_NilHandle", func(t *testing.T) { 201 | var handle *qjs.Handle 202 | assert.Equal(t, uint64(0), handle.Raw(), "nil handle should return 0") 203 | }) 204 | 205 | t.Run("Bool_NilHandle", func(t *testing.T) { 206 | var handle *qjs.Handle 207 | assert.False(t, handle.Bool(), "nil handle should return false") 208 | }) 209 | 210 | t.Run("Float32_NilHandle", func(t *testing.T) { 211 | var handle *qjs.Handle 212 | assert.Equal(t, float32(0.0), handle.Float32(), "nil handle should return 0.0") 213 | }) 214 | 215 | t.Run("Float64_NilHandle", func(t *testing.T) { 216 | var handle *qjs.Handle 217 | assert.Equal(t, 0.0, handle.Float64(), "nil handle should return 0.0") 218 | }) 219 | 220 | t.Run("String_NilHandle", func(t *testing.T) { 221 | var handle *qjs.Handle 222 | assert.Equal(t, "", handle.String(), "nil handle should return empty string") 223 | }) 224 | 225 | t.Run("Bytes_NilHandle", func(t *testing.T) { 226 | var handle *qjs.Handle 227 | assert.Nil(t, handle.Bytes(), "nil handle should return nil bytes") 228 | }) 229 | } 230 | 231 | func TestHandle_FreedChecks(t *testing.T) { 232 | runtime := must(qjs.New()) 233 | defer runtime.Close() 234 | 235 | t.Run("ConversionsAfterFree", func(t *testing.T) { 236 | handle := qjs.NewHandle(runtime, 42) 237 | handle.Free() 238 | 239 | // All conversions should return zero values after free 240 | assert.Equal(t, uint64(0), handle.Raw()) 241 | assert.False(t, handle.Bool()) 242 | assert.Equal(t, int(0), handle.Int()) 243 | assert.Equal(t, int8(0), handle.Int8()) 244 | assert.Equal(t, int16(0), handle.Int16()) 245 | assert.Equal(t, int32(0), handle.Int32()) 246 | assert.Equal(t, int64(0), handle.Int64()) 247 | assert.Equal(t, uint(0), handle.Uint()) 248 | assert.Equal(t, uint8(0), handle.Uint8()) 249 | assert.Equal(t, uint16(0), handle.Uint16()) 250 | assert.Equal(t, uint32(0), handle.Uint32()) 251 | assert.Equal(t, uint64(0), handle.Uint64()) 252 | assert.Equal(t, uintptr(0), handle.Uintptr()) 253 | assert.Equal(t, float32(0.0), handle.Float32()) 254 | assert.Equal(t, 0.0, handle.Float64()) 255 | assert.Equal(t, "", handle.String()) 256 | assert.Nil(t, handle.Bytes()) 257 | }) 258 | } 259 | 260 | func TestHandle_OverflowChecks(t *testing.T) { 261 | runtime := must(qjs.New()) 262 | defer runtime.Close() 263 | 264 | t.Run("Uint8_Overflow", func(t *testing.T) { 265 | handle := qjs.NewHandle(runtime, math.MaxUint64) // Value too large for uint8 266 | assert.Panics(t, func() { 267 | handle.Uint8() 268 | }, "Should panic on uint8 overflow") 269 | }) 270 | 271 | t.Run("Uint16_Overflow", func(t *testing.T) { 272 | handle := qjs.NewHandle(runtime, math.MaxUint64) // Value too large for uint16 273 | assert.Panics(t, func() { 274 | handle.Uint16() 275 | }, "Should panic on uint16 overflow") 276 | }) 277 | 278 | t.Run("Uint32_Overflow", func(t *testing.T) { 279 | handle := qjs.NewHandle(runtime, math.MaxUint64) // Value too large for uint32 280 | assert.Panics(t, func() { 281 | handle.Uint32() 282 | }, "Should panic on uint32 overflow") 283 | }) 284 | } 285 | 286 | func TestHandle_StringExceptionHandling(t *testing.T) { 287 | runtime := must(qjs.New()) 288 | defer runtime.Close() 289 | 290 | t.Run("String_ZeroRawValue", func(t *testing.T) { 291 | handle := qjs.NewHandle(runtime, 0) 292 | // Should return empty string without panic when no exception 293 | assert.Equal(t, "", handle.String()) 294 | }) 295 | } 296 | 297 | func TestHandle_SignExtension(t *testing.T) { 298 | runtime := must(qjs.New()) 299 | defer runtime.Close() 300 | 301 | t.Run("Int8_SignExtension", func(t *testing.T) { 302 | // Test negative value sign extension for int8 303 | handle := qjs.NewHandle(runtime, 0xFF) // 255 as uint, should be -1 as int8 304 | assert.Equal(t, int8(-1), handle.Int8()) 305 | }) 306 | 307 | t.Run("Int16_SignExtension", func(t *testing.T) { 308 | // Test negative value sign extension for int16 309 | handle := qjs.NewHandle(runtime, 0xFFFF) // 65535 as uint, should be -1 as int16 310 | assert.Equal(t, int16(-1), handle.Int16()) 311 | }) 312 | 313 | t.Run("Int32_SignExtension", func(t *testing.T) { 314 | // Test negative value sign extension for int32 315 | handle := qjs.NewHandle(runtime, 0xFFFFFFFF) // MaxUint32 as uint, should be -1 as int32 316 | assert.Equal(t, int32(-1), handle.Int32()) 317 | }) 318 | } 319 | 320 | func TestHandle_FreeWithNilRuntime(t *testing.T) { 321 | t.Run("Free_WithNilRuntime", func(t *testing.T) { 322 | handle := &qjs.Handle{} // Handle with nil runtime 323 | // Should not panic 324 | handle.Free() 325 | }) 326 | } 327 | 328 | func TestHandle_BytesFreeHandle(t *testing.T) { 329 | runtime := must(qjs.New()) 330 | defer runtime.Close() 331 | 332 | t.Run("Bytes_FreedHandle", func(t *testing.T) { 333 | handle := qjs.NewHandle(runtime, 42) 334 | handle.Free() 335 | bytes := handle.Bytes() 336 | assert.Nil(t, bytes, "freed handle should return nil bytes") 337 | }) 338 | } 339 | 340 | func TestHandle_CustomSignedTypes(t *testing.T) { 341 | runtime := must(qjs.New()) 342 | defer runtime.Close() 343 | 344 | // Custom signed integer types that will hit the default case in ConvertToSigned 345 | type MyInt int 346 | type MyInt8 int8 347 | type MyInt16 int16 348 | type MyInt32 int32 349 | type MyInt64 int64 350 | 351 | t.Run("CustomInt", func(t *testing.T) { 352 | handle := qjs.NewHandle(runtime, 42) 353 | result := qjs.ConvertToSigned[MyInt](handle) 354 | assert.Equal(t, MyInt(42), result) 355 | }) 356 | 357 | t.Run("CustomInt8", func(t *testing.T) { 358 | handle := qjs.NewHandle(runtime, 100) 359 | result := qjs.ConvertToSigned[MyInt8](handle) 360 | assert.Equal(t, MyInt8(100), result) 361 | }) 362 | 363 | t.Run("CustomInt16", func(t *testing.T) { 364 | handle := qjs.NewHandle(runtime, 1000) 365 | result := qjs.ConvertToSigned[MyInt16](handle) 366 | assert.Equal(t, MyInt16(1000), result) 367 | }) 368 | 369 | t.Run("CustomInt32", func(t *testing.T) { 370 | handle := qjs.NewHandle(runtime, 100000) 371 | result := qjs.ConvertToSigned[MyInt32](handle) 372 | assert.Equal(t, MyInt32(100000), result) 373 | }) 374 | 375 | t.Run("CustomInt64", func(t *testing.T) { 376 | handle := qjs.NewHandle(runtime, 1000000000) 377 | result := qjs.ConvertToSigned[MyInt64](handle) 378 | assert.Equal(t, MyInt64(1000000000), result) 379 | }) 380 | } 381 | -------------------------------------------------------------------------------- /runtime.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "runtime/debug" 9 | "sync" 10 | 11 | "github.com/tetratelabs/wazero" 12 | "github.com/tetratelabs/wazero/api" 13 | wsp1 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 14 | ) 15 | 16 | //go:embed qjs.wasm 17 | var wasmBytes []byte 18 | 19 | var ( 20 | compiledQJSModule wazero.CompiledModule 21 | cachedRuntimeConfig wazero.RuntimeConfig 22 | cachedBytesHash uint64 23 | compilationMutex sync.Mutex 24 | ) 25 | 26 | // Runtime wraps a QuickJS WebAssembly runtime with memory management. 27 | type Runtime struct { 28 | wrt wazero.Runtime 29 | module api.Module 30 | malloc api.Function 31 | free api.Function 32 | mem *Mem 33 | option Option 34 | handle *Handle 35 | context *Context 36 | registry *ProxyRegistry 37 | } 38 | 39 | func createGlobalCompiledModule( 40 | ctx context.Context, 41 | closeOnContextDone bool, 42 | disableBuildCache bool, 43 | cacheDir string, 44 | quickjsWasmBytes ...[]byte, 45 | ) (err error) { 46 | // Protect global compilation state with mutex 47 | compilationMutex.Lock() 48 | defer compilationMutex.Unlock() 49 | 50 | var qjsBytes []byte 51 | if len(quickjsWasmBytes) > 0 && len(quickjsWasmBytes[0]) > 0 { 52 | qjsBytes = quickjsWasmBytes[0] 53 | } else { 54 | qjsBytes = wasmBytes 55 | } 56 | 57 | // Calculate hash of the bytes to check if we need to recompile 58 | currentHash := hashBytes(qjsBytes) 59 | 60 | // Check if we need to compile or recompile 61 | if compiledQJSModule == nil || cachedBytesHash != currentHash || disableBuildCache { 62 | var cache wazero.CompilationCache 63 | if cacheDir == "" { 64 | cache = wazero.NewCompilationCache() 65 | } else if cache, err = wazero.NewCompilationCacheWithDir(cacheDir); err != nil { 66 | return fmt.Errorf("failed to create compilation cache with dir %s: %w", cacheDir, err) 67 | } 68 | 69 | cachedRuntimeConfig = wazero. 70 | NewRuntimeConfig(). 71 | WithCompilationCache(cache). 72 | WithCloseOnContextDone(closeOnContextDone) 73 | wrt := wazero.NewRuntimeWithConfig(ctx, cachedRuntimeConfig) 74 | 75 | if compiledQJSModule, err = wrt.CompileModule(ctx, qjsBytes); err != nil { 76 | return fmt.Errorf("failed to compile qjs module: %w", err) 77 | } 78 | 79 | cachedBytesHash = currentHash 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // New creates a QuickJS runtime with optional configuration. 86 | func New(options ...Option) (runtime *Runtime, err error) { 87 | defer func() { 88 | rerr := AnyToError(recover()) 89 | if rerr != nil { 90 | runtime = nil 91 | err = fmt.Errorf("failed to create QJS runtime: %w", rerr) 92 | } 93 | }() 94 | 95 | proxyRegistry := NewProxyRegistry() 96 | 97 | option, err := getRuntimeOption(proxyRegistry, options...) 98 | if err != nil { 99 | return nil, fmt.Errorf("failed to get runtime options: %w", err) 100 | } 101 | 102 | if err := createGlobalCompiledModule( 103 | option.Context, 104 | option.CloseOnContextDone, 105 | option.DisableBuildCache, 106 | option.CacheDir, 107 | option.QuickJSWasmBytes, 108 | ); err != nil { 109 | return nil, fmt.Errorf("failed to create global compiled module: %w", err) 110 | } 111 | 112 | runtime = &Runtime{ 113 | option: option, 114 | context: &Context{Context: option.Context}, 115 | registry: proxyRegistry, 116 | } 117 | 118 | runtime.wrt = wazero.NewRuntimeWithConfig( 119 | option.Context, 120 | cachedRuntimeConfig, 121 | ) 122 | 123 | if _, err := wsp1.Instantiate(option.Context, runtime.wrt); err != nil { 124 | return nil, fmt.Errorf("failed to instantiate WASI: %w", err) 125 | } 126 | 127 | if _, err := runtime.wrt.NewHostModuleBuilder("env"). 128 | NewFunctionBuilder(). 129 | WithFunc(option.ProxyFunction). 130 | Export("jsFunctionProxy"). 131 | Instantiate(option.Context); err != nil { 132 | return nil, fmt.Errorf("failed to setup host module: %w", err) 133 | } 134 | 135 | fsConfig := wazero. 136 | NewFSConfig(). 137 | WithDirMount(runtime.option.CWD, "/") 138 | if runtime.module, err = runtime.wrt.InstantiateModule( 139 | option.Context, 140 | compiledQJSModule, 141 | wazero.NewModuleConfig(). 142 | WithStartFunctions(option.StartFunctionName). 143 | WithSysWalltime(). 144 | WithSysNanotime(). 145 | WithSysNanosleep(). 146 | WithFSConfig(fsConfig). 147 | WithStdout(option.Stdout). 148 | WithStderr(option.Stderr), 149 | ); err != nil { 150 | return nil, fmt.Errorf("failed to instantiate module: %w", err) 151 | } 152 | 153 | runtime.initializeRuntime() 154 | 155 | return runtime, nil 156 | } 157 | 158 | func (r *Runtime) Raw() uint64 { 159 | return r.handle.raw 160 | } 161 | 162 | // FreeQJSRuntime frees the QJS runtime. 163 | func (r *Runtime) FreeQJSRuntime() { 164 | defer func() { 165 | err := AnyToError(recover()) 166 | if err != nil { 167 | panic(fmt.Errorf("failed to free QJS runtime: %w", err)) 168 | } 169 | }() 170 | 171 | r.Call("QJS_Free", r.handle.raw) 172 | } 173 | 174 | // Mem returns the WebAssembly memory interface for this runtime. 175 | func (r *Runtime) Mem() *Mem { 176 | return r.mem 177 | } 178 | 179 | // String returns a string representation of the runtime. 180 | func (r *Runtime) String() string { 181 | return fmt.Sprintf("QJSRuntime: %p", r) 182 | } 183 | 184 | // Close cleanly shuts down the runtime and frees all associated resources. 185 | func (r *Runtime) Close() { 186 | if r == nil { 187 | return 188 | } 189 | 190 | // Free QJS runtime handle 191 | if r.handle != nil { 192 | r.FreeQJSRuntime() 193 | r.handle = nil 194 | } 195 | 196 | // Close WASM module 197 | if r.module != nil { 198 | r.module.Close(r.context) 199 | r.module = nil 200 | } 201 | 202 | // Clear references 203 | if r.context != nil { 204 | r.context = nil 205 | } 206 | 207 | if r.registry != nil { 208 | r.registry.Clear() 209 | r.registry = nil 210 | } 211 | 212 | // Clear function references 213 | r.malloc = nil 214 | r.free = nil 215 | r.mem = nil 216 | } 217 | 218 | // Load executes a JavaScript file in the runtime's context. 219 | func (r *Runtime) Load(file string, flags ...EvalOptionFunc) (*Value, error) { 220 | return r.context.Load(file, flags...) 221 | } 222 | 223 | // Eval executes JavaScript code in the runtime's context. 224 | func (r *Runtime) Eval(file string, flags ...EvalOptionFunc) (*Value, error) { 225 | return r.context.Eval(file, flags...) 226 | } 227 | 228 | // Compile compiles JavaScript code to bytecode without executing it. 229 | func (r *Runtime) Compile(file string, flags ...EvalOptionFunc) ([]byte, error) { 230 | return r.context.Compile(file, flags...) 231 | } 232 | 233 | // Context returns the JavaScript execution context for this runtime. 234 | func (r *Runtime) Context() *Context { 235 | return r.context 236 | } 237 | 238 | // Call invokes a WebAssembly function by name with the given arguments. 239 | func (r *Runtime) Call(name string, args ...uint64) *Handle { 240 | return NewHandle(r, r.call(name, args...)) 241 | } 242 | 243 | // CallUnPack calls a WebAssembly function and unpacks the returned pointer. 244 | func (r *Runtime) CallUnPack(name string, args ...uint64) (uint32, uint32) { 245 | return r.mem.UnpackPtr(r.Call(name, args...).raw) 246 | } 247 | 248 | // Malloc allocates memory in the WebAssembly linear memory and return a pointer to it. 249 | func (r *Runtime) Malloc(size uint64) uint64 { 250 | ptrs, err := r.malloc.Call(r.context, size) 251 | if err != nil { 252 | panic(fmt.Errorf("failed to allocate memory: %w", err)) 253 | } 254 | 255 | return ptrs[0] 256 | } 257 | 258 | // FreeHandle releases memory allocated in WebAssembly linear memory. 259 | func (r *Runtime) FreeHandle(ptr uint64) { 260 | if _, err := r.free.Call(r.context, ptr); err != nil { 261 | panic(fmt.Errorf("failed to free memory: %w", err)) 262 | } 263 | } 264 | 265 | // FreeJsValue frees a JavaScript value in the QuickJS runtime. 266 | func (r *Runtime) FreeJsValue(val uint64) { 267 | r.Call("QJS_FreeValue", r.context.Raw(), val) 268 | } 269 | 270 | // NewBytesHandle creates a handle for byte data in WebAssembly memory. 271 | func (r *Runtime) NewBytesHandle(b []byte) *Handle { 272 | if len(b) == 0 { 273 | return nil 274 | } 275 | 276 | ptr := r.Malloc(uint64(len(b))) 277 | r.mem.MustWrite(uint32(ptr), b) 278 | 279 | return NewHandle(r, ptr) 280 | } 281 | 282 | // NewStringHandle creates a handle for string data with null termination. 283 | func (r *Runtime) NewStringHandle(v string) *Handle { 284 | // Allocate len+1 for null terminator 285 | ptr := r.Malloc(uint64(len(v) + 1)) 286 | 287 | // Write string data 288 | r.mem.MustWrite(uint32(ptr), []byte(v)) 289 | // Add null terminator 290 | r.mem.MustWrite(uint32(ptr)+uint32(len(v)), []byte{0}) 291 | 292 | return NewHandle(r, ptr) 293 | } 294 | 295 | // initializeRuntime sets up the runtime components after module instantiation. 296 | func (r *Runtime) initializeRuntime() { 297 | r.malloc = r.module.ExportedFunction("malloc") 298 | r.free = r.module.ExportedFunction("free") 299 | r.mem = &Mem{mem: r.module.Memory()} 300 | r.handle = r.Call( 301 | "New_QJS", 302 | uint64(r.option.MemoryLimit), 303 | uint64(r.option.MaxStackSize), 304 | uint64(r.option.MaxExecutionTime), 305 | uint64(r.option.GCThreshold), 306 | ) 307 | 308 | r.context.handle = r.Call("QJS_GetContext", r.handle.raw) 309 | r.context.runtime = r 310 | } 311 | 312 | func (r *Runtime) call(name string, args ...uint64) uint64 { 313 | fn := r.module.ExportedFunction(name) 314 | if fn == nil { 315 | panic(fmt.Errorf("WASM function %s not found", name)) 316 | } 317 | 318 | results, err := fn.Call(r.context, args...) 319 | if err != nil { 320 | stack := debug.Stack() 321 | panic(fmt.Errorf("failed to call %s: %w\nstack: %s", name, err, stack)) 322 | } 323 | 324 | if len(results) == 0 { 325 | return 0 326 | } 327 | 328 | return results[0] 329 | } 330 | 331 | // Pool manages a collection of reusable QuickJS runtimes. 332 | type Pool struct { 333 | pools chan *Runtime 334 | size int 335 | option Option 336 | setupFuncs []func(*Runtime) error 337 | mu sync.Mutex 338 | } 339 | 340 | // NewPool creates a new runtime pool with the specified size and configuration. 341 | func NewPool(size int, option Option, setupFuncs ...func(*Runtime) error) *Pool { 342 | if size <= 0 { 343 | panic("pool size must be greater than 0") 344 | } 345 | 346 | p := &Pool{ 347 | pools: make(chan *Runtime, size), 348 | size: size, 349 | option: option, 350 | setupFuncs: setupFuncs, 351 | } 352 | 353 | return p 354 | } 355 | 356 | // Get returns a runtime from the pool or creates a new one if the pool is empty. 357 | // The caller must call Put() to return the runtime when finished. 358 | func (p *Pool) Get() (*Runtime, error) { 359 | if p == nil { 360 | return nil, errors.New("pool is nil") 361 | } 362 | 363 | // Try to get from pool first 364 | select { 365 | case rt := <-p.pools: 366 | return p.prepareRuntimeForUse(rt), nil 367 | // Pool is empty, need to create new runtime 368 | default: 369 | } 370 | 371 | // Double-check with lock to avoid race conditions 372 | p.mu.Lock() 373 | defer p.mu.Unlock() 374 | 375 | select { 376 | case rt := <-p.pools: 377 | return p.prepareRuntimeForUse(rt), nil 378 | default: 379 | return p.createNewRuntime() 380 | } 381 | } 382 | 383 | // Put returns the runtime back to the pool for reuse. 384 | // If the pool is full, the runtime is closed to prevent resource leaks. 385 | func (p *Pool) Put(rt *Runtime) { 386 | if rt == nil { 387 | return 388 | } 389 | 390 | // Update stack top before returning to pool 391 | if rt.handle != nil { 392 | rt.Call("QJS_UpdateStackTop", rt.handle.raw) 393 | } 394 | 395 | select { 396 | case p.pools <- rt: 397 | // Successfully returned to pool 398 | default: 399 | // Pool is full, close the runtime 400 | rt.Close() 401 | } 402 | } 403 | 404 | // prepareRuntimeForUse prepares a pooled runtime for use. 405 | func (p *Pool) prepareRuntimeForUse(rt *Runtime) *Runtime { 406 | if rt != nil && rt.handle != nil { 407 | rt.Call("QJS_UpdateStackTop", rt.handle.raw) 408 | } 409 | 410 | return rt 411 | } 412 | 413 | // createNewRuntime creates a new runtime with setup functions applied. 414 | func (p *Pool) createNewRuntime() (*Runtime, error) { 415 | rt, err := New(p.option) 416 | if err != nil { 417 | return nil, fmt.Errorf("failed to create new runtime: %w", err) 418 | } 419 | 420 | // Apply setup functions 421 | for i, setupFunc := range p.setupFuncs { 422 | err := setupFunc(rt) 423 | if err != nil { 424 | rt.Close() 425 | 426 | return nil, fmt.Errorf("setup function %d failed: %w", i, err) 427 | } 428 | } 429 | 430 | return rt, nil 431 | } 432 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "math" 8 | "time" 9 | ) 10 | 11 | // Context represents a QuickJS execution context with associated runtime. 12 | type Context struct { 13 | context.Context 14 | 15 | handle *Handle 16 | runtime *Runtime 17 | global *Value 18 | } 19 | 20 | func (c *Context) Call(name string, args ...uint64) *Value { 21 | return c.NewValue(c.runtime.Call(name, args...)) 22 | } 23 | 24 | // CallUnPack delegates function calls and unpacks the result. 25 | func (c *Context) CallUnPack(name string, args ...uint64) (uint32, uint32) { 26 | return c.runtime.CallUnPack(name, args...) 27 | } 28 | 29 | // FreeHandle releases memory associated with the given handle pointer. 30 | func (c *Context) FreeHandle(ptr uint64) { 31 | c.runtime.FreeHandle(ptr) 32 | } 33 | 34 | // FreeJsValue releases memory associated with the JavaScript value. 35 | func (c *Context) FreeJsValue(val uint64) { 36 | c.runtime.FreeJsValue(val) 37 | } 38 | 39 | // Malloc allocates memory in the WASM runtime. 40 | func (c *Context) Malloc(size uint64) uint64 { 41 | return c.runtime.Malloc(size) 42 | } 43 | 44 | // MemRead reads bytes from WASM memory at the given address. 45 | func (c *Context) MemRead(addr uint32, size uint64) []byte { 46 | return c.runtime.mem.MustRead(addr, size) 47 | } 48 | 49 | // MemWrite writes bytes to WASM memory at the given address. 50 | func (c *Context) MemWrite(addr uint32, b []byte) { 51 | c.runtime.mem.MustWrite(addr, b) 52 | } 53 | 54 | // NewProxyValue creates a new Value that represents a proxy to a Go value. 55 | func (c *Context) NewProxyValue(v any) *Value { 56 | proxyID := c.runtime.registry.Register(v) 57 | 58 | return c.Call("QJS_NewProxyValue", c.Raw(), proxyID) 59 | } 60 | 61 | // NewStringHandle creates a Value from a string using runtime handle. 62 | func (c *Context) NewStringHandle(v string) *Value { 63 | handle := c.runtime.NewStringHandle(v) 64 | 65 | return c.NewValue(handle) 66 | } 67 | 68 | // NewBytes creates a Value from a byte slice. 69 | func (c *Context) NewBytes(v []byte) *Value { 70 | return c.NewValue(c.runtime.NewBytesHandle(v)) 71 | } 72 | 73 | // String returns the string representation of the Context. 74 | func (c *Context) String() string { 75 | return fmt.Sprintf("Context(%p)", c.handle) 76 | } 77 | 78 | // Raw returns the raw handle value for low-level operations. 79 | func (c *Context) Raw() uint64 { 80 | return c.handle.raw 81 | } 82 | 83 | // Load loads a JavaScript module without evaluating it. 84 | func (c *Context) Load(file string, flags ...EvalOptionFunc) (*Value, error) { 85 | return load(c, file, flags...) 86 | } 87 | 88 | // Eval evaluates a script within the current context. 89 | func (c *Context) Eval(file string, flags ...EvalOptionFunc) (*Value, error) { 90 | return eval(c, file, flags...) 91 | } 92 | 93 | // Compile compiles a script into bytecode. 94 | func (c *Context) Compile(file string, flags ...EvalOptionFunc) ([]byte, error) { 95 | return compile(c, file, flags...) 96 | } 97 | 98 | // Global returns the global object, caching it for subsequent calls. 99 | func (c *Context) Global() *Value { 100 | if c.global == nil { 101 | c.global = c.Call("JS_GetGlobalObject", c.Raw()) 102 | } 103 | 104 | return c.global 105 | } 106 | 107 | // SetFunc sets a function with given name in the global object. 108 | func (c *Context) SetFunc(name string, fn Function) { 109 | jsFn := c.Function(fn) 110 | global := c.Global() 111 | global.SetPropertyStr(name, jsFn) 112 | } 113 | 114 | // SetAsyncFunc sets an async function with given name in the global object. 115 | func (c *Context) SetAsyncFunc(name string, fn AsyncFunction) { 116 | jsFn := c.Function(func(this *This) (*Value, error) { 117 | fn(this) 118 | 119 | return this.context.NewUndefined(), nil 120 | }, true) 121 | global := c.Global() 122 | global.SetPropertyStr(name, jsFn) 123 | } 124 | 125 | // ParseJSON parses given JSON string and returns an object value. 126 | func (c *Context) ParseJSON(v string) *Value { 127 | cStr := c.NewStringHandle(v) 128 | defer cStr.Free() 129 | 130 | return c.Call("QJS_ParseJSON", c.Raw(), cStr.Raw()) 131 | } 132 | 133 | // NewValue creates a new Value wrapper around the given handle. 134 | func (c *Context) NewValue(handle *Handle) *Value { 135 | return &Value{context: c, handle: handle} 136 | } 137 | 138 | // NewUninitialized creates a new uninitialized JavaScript value. 139 | func (c *Context) NewUninitialized() *Value { 140 | return c.Call("JS_NewUninitialized") 141 | } 142 | 143 | // NewNull creates a new null JavaScript value. 144 | func (c *Context) NewNull() *Value { 145 | return c.Call("JS_NewNull") 146 | } 147 | 148 | // NewUndefined creates a new undefined JavaScript value. 149 | func (c *Context) NewUndefined() *Value { 150 | return c.Call("JS_NewUndefined") 151 | } 152 | 153 | // NewDate creates a new JavaScript Date object from the given time. 154 | func (c *Context) NewDate(t *time.Time) *Value { 155 | epochMs := float64(t.UnixNano()) / NanosToMillis // Nanoseconds to milliseconds 156 | 157 | return c.Call("JS_NewDate", c.Raw(), math.Float64bits(epochMs)) 158 | } 159 | 160 | // NewBool creates a new JavaScript boolean value. 161 | func (c *Context) NewBool(b bool) *Value { 162 | boolVal := uint64(0) 163 | if b { 164 | boolVal = 1 165 | } 166 | 167 | return c.Call("QJS_NewBool", c.Raw(), boolVal) 168 | } 169 | 170 | // NewUint32 creates a new JavaScript number from uint32. 171 | func (c *Context) NewUint32(v uint32) *Value { 172 | return c.Call("QJS_NewUint32", c.Raw(), uint64(v)) 173 | } 174 | 175 | // NewInt32 creates a new JavaScript number from int32. 176 | func (c *Context) NewInt32(v int32) *Value { 177 | return c.Call("QJS_NewInt32", c.Raw(), uint64(v)) 178 | } 179 | 180 | // NewInt64 creates a new JavaScript number from int64. 181 | func (c *Context) NewInt64(v int64) *Value { 182 | return c.Call("QJS_NewInt64", c.Raw(), uint64(v)) 183 | } 184 | 185 | // NewBigInt64 creates a new JavaScript BigInt from int64. 186 | func (c *Context) NewBigInt64(v int64) *Value { 187 | return c.Call("QJS_NewBigInt64", c.Raw(), uint64(v)) 188 | } 189 | 190 | // NewBigUint64 creates a new JavaScript BigInt from uint64. 191 | func (c *Context) NewBigUint64(v uint64) *Value { 192 | return c.Call("QJS_NewBigUint64", c.Raw(), v) 193 | } 194 | 195 | // NewFloat64 creates a new JavaScript number from float64. 196 | func (c *Context) NewFloat64(v float64) *Value { 197 | return c.Call("QJS_NewFloat64", c.Raw(), math.Float64bits(v)) 198 | } 199 | 200 | // NewString creates a new JavaScript string value. 201 | func (c *Context) NewString(v string) *Value { 202 | str := c.NewStringHandle(v) 203 | 204 | return c.Call("QJS_NewString", c.Raw(), str.Raw()) 205 | } 206 | 207 | // NewObject creates a new empty JavaScript object. 208 | func (c *Context) NewObject() *Value { 209 | return c.Call("JS_NewObject", c.Raw()) 210 | } 211 | 212 | // NewArray creates a new empty JavaScript array. 213 | func (c *Context) NewArray() *Array { 214 | val := c.Call("JS_NewArray", c.Raw()) 215 | 216 | return NewArray(val) 217 | } 218 | 219 | // NewMap creates a new JavaScript Map object. 220 | func (c *Context) NewMap() *Map { 221 | m := c.Global().GetPropertyStr("Map") 222 | defer m.Free() 223 | 224 | val := c.Call("JS_CallConstructor", c.Raw(), m.Raw(), 0, 0) 225 | 226 | return NewMap(val) 227 | } 228 | 229 | // NewSet creates a new JavaScript Set object. 230 | func (c *Context) NewSet() *Set { 231 | s := c.Global().GetPropertyStr("Set") 232 | defer s.Free() 233 | 234 | val := c.Call("JS_CallConstructor", c.Raw(), s.Raw(), 0, 0) 235 | 236 | return NewSet(val) 237 | } 238 | 239 | // NewAtom creates a new Atom from the given string. 240 | func (c *Context) NewAtom(v string) Atom { 241 | cstr := c.NewStringHandle(v) 242 | 243 | atomValue := c.Call("JS_NewAtom", c.Raw(), cstr.Raw()) 244 | 245 | defer cstr.Free() 246 | 247 | return Atom{context: c, Value: atomValue} 248 | } 249 | 250 | // NewAtomIndex creates a new Atom with the given index. 251 | func (c *Context) NewAtomIndex(index int64) Atom { 252 | return Atom{ 253 | context: c, 254 | Value: c.Call("QJS_NewAtomUInt32", c.Raw(), uint64(index)), 255 | } 256 | } 257 | 258 | // NewArrayBuffer creates a new JavaScript ArrayBuffer with the given binary data. 259 | func (c *Context) NewArrayBuffer(binaryData []byte) *Value { 260 | ptr := c.Malloc(uint64(len(binaryData))) 261 | defer c.FreeHandle(ptr) 262 | 263 | c.MemWrite(uint32(ptr), binaryData) 264 | 265 | return c.Call("QJS_NewArrayBufferCopy", c.Raw(), ptr, uint64(len(binaryData))) 266 | } 267 | 268 | // NewError creates a new JavaScript Error object from Go error. 269 | func (c *Context) NewError(e error) *Value { 270 | errString := c.NewString(e.Error()) 271 | errVal := c.Call("JS_NewError", c.Raw()) 272 | errVal.SetPropertyStr("message", errString) 273 | 274 | return errVal 275 | } 276 | 277 | // HasException returns true if there is a pending exception. 278 | func (c *Context) HasException() bool { 279 | return c.Call("JS_HasException", c.Raw()).Bool() 280 | } 281 | 282 | // Exception returns and clears the current pending exception. 283 | func (c *Context) Exception() error { 284 | val := c.Call("JS_GetException", c.Raw()) 285 | defer val.Free() 286 | 287 | return val.Exception() 288 | } 289 | 290 | // Throw throws a value as an exception. 291 | func (c *Context) Throw(v *Value) *Value { 292 | return c.Call("JS_Throw", c.Raw(), v.Raw()) 293 | } 294 | 295 | // ThrowError throws an exception with the given error. 296 | func (c *Context) ThrowError(err error) *Value { 297 | return c.Throw(c.NewError(err)) 298 | } 299 | 300 | // ThrowSyntaxError throws syntax error with given cause. 301 | func (c *Context) ThrowSyntaxError(format string, args ...any) *Value { 302 | cause := fmt.Sprintf(format, args...) 303 | 304 | causePtr := c.NewStringHandle(cause) 305 | defer causePtr.Free() 306 | 307 | return c.Call("QJS_ThrowSyntaxError", c.Raw(), causePtr.Raw()) 308 | } 309 | 310 | // ThrowTypeError throws type error with given cause. 311 | func (c *Context) ThrowTypeError(format string, args ...any) *Value { 312 | cause := fmt.Sprintf(format, args...) 313 | 314 | causePtr := c.NewStringHandle(cause) 315 | defer causePtr.Free() 316 | 317 | return c.Call("QJS_ThrowTypeError", c.Raw(), causePtr.Raw()) 318 | } 319 | 320 | // ThrowReferenceError throws reference error with given cause. 321 | func (c *Context) ThrowReferenceError(format string, args ...any) *Value { 322 | cause := fmt.Sprintf(format, args...) 323 | 324 | causePtr := c.NewStringHandle(cause) 325 | defer causePtr.Free() 326 | 327 | return c.Call("QJS_ThrowReferenceError", c.Raw(), causePtr.Raw()) 328 | } 329 | 330 | // ThrowRangeError throws range error with given cause. 331 | func (c *Context) ThrowRangeError(format string, args ...any) *Value { 332 | cause := fmt.Sprintf(format, args...) 333 | 334 | causePtr := c.NewStringHandle(cause) 335 | defer causePtr.Free() 336 | 337 | return c.Call("QJS_ThrowRangeError", c.Raw(), causePtr.Raw()) 338 | } 339 | 340 | // ThrowInternalError throws internal error with given cause. 341 | func (c *Context) ThrowInternalError(format string, args ...any) *Value { 342 | cause := fmt.Sprintf(format, args...) 343 | 344 | causePtr := c.NewStringHandle(cause) 345 | defer causePtr.Free() 346 | 347 | return c.Call("QJS_ThrowInternalError", c.Raw(), causePtr.Raw()) 348 | } 349 | 350 | // Function creates a JavaScript function that wraps the given Go function. 351 | func (c *Context) Function(fn Function, isAsyncs ...bool) *Value { 352 | isAsync := uint64(0) 353 | if len(isAsyncs) > 0 && isAsyncs[0] { 354 | isAsync = 1 355 | } 356 | 357 | fnID := c.runtime.registry.Register(fn) 358 | ctxID := c.runtime.registry.Register(c) 359 | proxyFuncVal := c.Call("QJS_CreateFunctionProxy", c.Raw(), fnID, ctxID, isAsync) 360 | 361 | // Registry: Store IDs for cleanup and callback identification 362 | proxyFuncVal.SetPropertyStr("__fnID", c.NewInt64(int64(fnID))) 363 | proxyFuncVal.SetPropertyStr("__ctxID", c.NewInt64(int64(ctxID))) 364 | 365 | return proxyFuncVal 366 | } 367 | 368 | // Invoke invokes a function with given this value and arguments. 369 | func (c *Context) Invoke(fn *Value, this *Value, args ...*Value) (*Value, error) { 370 | argc, argvPtr := createJsCallArgs(c, args...) 371 | defer c.FreeHandle(argvPtr) 372 | 373 | jsCallArgs := []uint64{ 374 | c.Raw(), 375 | fn.Raw(), 376 | this.Raw(), 377 | argc, 378 | argvPtr, 379 | } 380 | result := c.Call("QJS_Call", jsCallArgs...) 381 | 382 | return normalizeJsValue(c, result) 383 | } 384 | 385 | // createJsCallArgs marshals Go Value arguments to WASM memory for JavaScript calls. 386 | func createJsCallArgs(c *Context, args ...*Value) (uint64, uint64) { 387 | var argvPtr uint64 388 | 389 | argc := uint64(len(args)) 390 | if argc > 0 { 391 | argvBytes := make([]byte, Uint64ByteSize*argc) 392 | for i, v := range args { 393 | // WASM: Pack 64-bit JSValue handles in little-endian format 394 | binary.LittleEndian.PutUint64(argvBytes[i*Uint64ByteSize:], v.Raw()) 395 | } 396 | 397 | // WASM: Allocate and write argument array to memory 398 | argvPtr = c.Malloc(uint64(len(argvBytes))) 399 | c.MemWrite(uint32(argvPtr), argvBytes) 400 | } 401 | 402 | return argc, argvPtr 403 | } 404 | -------------------------------------------------------------------------------- /qjswasm/eval.c: -------------------------------------------------------------------------------- 1 | #include "qjs.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /* Detect and load source code from a file or memory. 9 | * For file input, returns an allocated buffer (caller must free). 10 | * For in-memory input, returns the provided buffer. 11 | */ 12 | static uint8_t *detect_buf(JSContext *ctx, QJSEvalOptions *opts, size_t *buf_len, bool is_file) 13 | { 14 | // If the input is a file, load the file into a buffer. 15 | // The buffer will be freed by the caller. 16 | // buf_len will be set to the length of the buffer. 17 | if (is_file) 18 | { 19 | if (!opts->filename || opts->filename[0] == '\0') 20 | return NULL; 21 | 22 | opts->filename = detect_entry_point((char *)opts->filename); 23 | return js_load_file(ctx, buf_len, opts->filename); 24 | } 25 | // If the input is in-memory, return the provided buffer 26 | // and set the buffer length 27 | if (opts->buf) 28 | { 29 | *buf_len = strlen(opts->buf); 30 | if (!opts->filename || opts->filename[0] == '\0') 31 | opts->filename = ""; 32 | 33 | return (uint8_t *)opts->buf; 34 | } 35 | // If the input is bytecode, return the provided buffer 36 | // and set the buffer length 37 | if (opts->bytecode_buf) 38 | { 39 | *buf_len = opts->bytecode_len; 40 | return (uint8_t *)opts->bytecode_buf; 41 | } 42 | 43 | return NULL; 44 | } 45 | 46 | /* Returns a thrown exception for an empty buffer. 47 | * If buf is non-NULL, this function returns JS_NULL. 48 | */ 49 | static JSValue buf_empty_error(JSContext *ctx, bool is_file, QJSEvalOptions *opts) 50 | { 51 | if (JS_HasException(ctx)) 52 | return JS_Throw(ctx, JS_GetException(ctx)); 53 | 54 | if (errno != 0) 55 | return JS_ThrowReferenceError(ctx, "%s: %s", strerror(errno), opts->filename); 56 | 57 | if (is_file) 58 | { 59 | if (!opts->filename || opts->filename[0] == '\0') 60 | return JS_ThrowReferenceError(ctx, "filename is required for file evaluation"); 61 | else 62 | return JS_ThrowReferenceError(ctx, "could not load file: %s", opts->filename); 63 | } 64 | 65 | return JS_ThrowReferenceError(ctx, "in-memory buffer/bytecode is required for evaluation"); 66 | } 67 | 68 | static JSValue load_buf(JSContext *ctx, QJSEvalOptions opts, int flags, bool eval) 69 | { 70 | bool is_file = input_is_file(opts); 71 | size_t buf_len = 0; 72 | uint8_t *buf = detect_buf(ctx, &opts, &buf_len, is_file); 73 | if (buf == NULL) 74 | return buf_empty_error(ctx, is_file, &opts); 75 | 76 | JSValue module_val; 77 | if (opts.bytecode_buf != NULL) 78 | if (!eval) 79 | { 80 | module_val = JS_ReadObject(ctx, opts.bytecode_buf, opts.bytecode_len, JS_READ_OBJ_BYTECODE); 81 | if (JS_IsException(module_val)) 82 | { 83 | if (is_file) 84 | js_free(ctx, buf); 85 | return JS_Throw(ctx, JS_GetException(ctx)); 86 | } 87 | } 88 | else 89 | { 90 | JSValue obj = JS_ReadObject(ctx, opts.bytecode_buf, opts.bytecode_len, JS_READ_OBJ_BYTECODE); 91 | if (JS_IsException(obj)) 92 | { 93 | if (is_file) 94 | js_free(ctx, buf); 95 | return JS_Throw(ctx, JS_GetException(ctx)); 96 | } 97 | module_val = JS_EvalFunction(ctx, obj); 98 | if (JS_IsException(module_val)) 99 | { 100 | JS_FreeValue(ctx, obj); 101 | if (is_file) 102 | js_free(ctx, buf); 103 | return JS_Throw(ctx, JS_GetException(ctx)); 104 | } 105 | } 106 | 107 | else 108 | { 109 | module_val = JS_Eval(ctx, (const char *)buf, buf_len, opts.filename, flags); 110 | if (JS_IsException(module_val)) 111 | { 112 | if (is_file) 113 | js_free(ctx, buf); 114 | return JS_Throw(ctx, JS_GetException(ctx)); 115 | } 116 | } 117 | 118 | if (is_file) 119 | js_free(ctx, buf); 120 | 121 | return module_val; 122 | } 123 | 124 | /* Create a JSON module with the parsed JSON as default export */ 125 | static JSModuleDef *js_module_loader_json(JSContext *ctx, const char *module_name) 126 | { 127 | JSModuleDef *m; 128 | size_t buf_len; 129 | uint8_t *buf; 130 | JSValue json_val, func_val; 131 | 132 | buf = js_load_file(ctx, &buf_len, module_name); 133 | if (!buf) 134 | { 135 | JS_ThrowReferenceError(ctx, "could not load JSON module filename '%s'", module_name); 136 | return NULL; 137 | } 138 | 139 | /* Parse the JSON content */ 140 | json_val = JS_ParseJSON(ctx, (char *)buf, buf_len, module_name); 141 | js_free(ctx, buf); 142 | if (JS_IsException(json_val)) 143 | return NULL; 144 | 145 | /* Create a synthetic module source that exports the JSON */ 146 | const char *module_source_template = 147 | "const __json_data__ = %s;\n" 148 | "export default __json_data__;\n"; 149 | 150 | /* Convert JSON to string */ 151 | JSValue json_str = JS_JSONStringify(ctx, json_val, JS_NULL, JS_NULL); 152 | if (JS_IsException(json_str)) 153 | { 154 | JS_FreeValue(ctx, json_val); 155 | return NULL; 156 | } 157 | 158 | const char *json_string = JS_ToCString(ctx, json_str); 159 | if (!json_string) 160 | { 161 | JS_FreeValue(ctx, json_val); 162 | JS_FreeValue(ctx, json_str); 163 | return NULL; 164 | } 165 | 166 | /* Create the module source */ 167 | size_t source_len = strlen(module_source_template) + strlen(json_string) + 1; 168 | char *module_source = malloc(source_len); 169 | if (!module_source) 170 | { 171 | JS_FreeCString(ctx, json_string); 172 | JS_FreeValue(ctx, json_str); 173 | JS_FreeValue(ctx, json_val); 174 | return NULL; 175 | } 176 | snprintf(module_source, source_len, module_source_template, json_string); 177 | 178 | /* Compile the module */ 179 | func_val = JS_Eval(ctx, module_source, strlen(module_source), module_name, 180 | JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); 181 | 182 | /* Cleanup */ 183 | free(module_source); 184 | JS_FreeCString(ctx, json_string); 185 | JS_FreeValue(ctx, json_str); 186 | JS_FreeValue(ctx, json_val); 187 | 188 | if (JS_IsException(func_val)) 189 | return NULL; 190 | 191 | /* Set import.meta */ 192 | if (js_module_set_import_meta(ctx, func_val, true, false) < 0) 193 | { 194 | JS_FreeValue(ctx, func_val); 195 | return NULL; 196 | } 197 | 198 | /* Return the compiled module */ 199 | m = JS_VALUE_GET_PTR(func_val); 200 | JS_FreeValue(ctx, func_val); 201 | return m; 202 | } 203 | 204 | /* Module loader with support for appending common suffixes and JSON modules */ 205 | JSModuleDef *QJS_ModuleLoader(JSContext *ctx, const char *module_name, void *opaque) 206 | { 207 | module_name = detect_entry_point((char *)module_name); 208 | if (!module_name) 209 | return NULL; 210 | 211 | /* Check if it's a JSON module */ 212 | if (js__has_suffix(module_name, ".json")) 213 | { 214 | return js_module_loader_json(ctx, module_name); 215 | } 216 | 217 | JSModuleDef *mod = js_module_loader(ctx, module_name, opaque); 218 | return mod; 219 | } 220 | 221 | /* Loads a module, compiles it (with import.meta support), and resolves it */ 222 | JSValue QJS_Load(JSContext *ctx, QJSEvalOptions opts) 223 | { 224 | if ((opts.eval_flags & JS_EVAL_TYPE_MASK) != JS_EVAL_TYPE_MODULE) 225 | return JS_ThrowTypeError(ctx, "load only supports module evaluation"); 226 | 227 | int eval_flags = (opts.eval_flags ? opts.eval_flags : JS_EVAL_TYPE_MODULE); 228 | eval_flags |= JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY; 229 | 230 | JSValue module_val = load_buf(ctx, opts, eval_flags, false); 231 | if (JS_IsException(module_val)) 232 | return JS_Throw(ctx, JS_GetException(ctx)); 233 | 234 | if (JS_VALUE_GET_TAG(module_val) != JS_TAG_MODULE) 235 | return JS_ThrowTypeError(ctx, "is not a module"); 236 | 237 | if (JS_ResolveModule(ctx, module_val) != 0) 238 | { 239 | JSValue ex = JS_HasException(ctx) ? JS_Throw(ctx, JS_GetException(ctx)) 240 | : JS_ThrowTypeError(ctx, "resolve module failed"); 241 | JS_FreeValue(ctx, module_val); 242 | return ex; 243 | } 244 | 245 | if (js_module_set_import_meta(ctx, module_val, input_is_file(opts), true) < 0) 246 | { 247 | JSValue ex = JS_Throw(ctx, JS_GetException(ctx)); 248 | JS_FreeValue(ctx, module_val); 249 | return ex; 250 | } 251 | 252 | return module_val; 253 | } 254 | 255 | /* Evaluates a module with support for promises and default exports */ 256 | static JSValue qjs_eval_module(JSContext *ctx, QJSEvalOptions opts) 257 | { 258 | JSValue module_val = QJS_Load(ctx, opts); 259 | if (JS_IsException(module_val)) 260 | return module_val; 261 | 262 | JSValue result = JS_EvalFunction(ctx, module_val); 263 | if (JS_IsPromise(result)) 264 | result = js_std_await(ctx, result); 265 | 266 | js_std_loop(ctx); 267 | if (JS_IsException(result)) 268 | return JS_Throw(ctx, JS_GetException(ctx)); 269 | 270 | JSModuleDef *module_def = JS_VALUE_GET_PTR(module_val); 271 | JSValue ns = JS_GetModuleNamespace(ctx, module_def); 272 | JSAtom default_atom = JS_NewAtom(ctx, "default"); 273 | JSValue default_export = JS_GetProperty(ctx, ns, default_atom); 274 | JS_FreeAtom(ctx, default_atom); 275 | JS_FreeValue(ctx, ns); 276 | 277 | if (!JS_IsUndefined(default_export)) 278 | { 279 | JS_FreeValue(ctx, result); 280 | return default_export; 281 | } 282 | 283 | JS_FreeValue(ctx, default_export); 284 | return result; 285 | } 286 | 287 | /* Evaluates code (as a module or global script) with promise handling */ 288 | JSValue QJS_Eval(JSContext *ctx, QJSEvalOptions opts) 289 | { 290 | bool is_module = (opts.eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_MODULE; 291 | bool is_compile_only = (opts.eval_flags & JS_EVAL_FLAG_COMPILE_ONLY) > 0; 292 | JSValue result; 293 | 294 | // compile_only always uses qjs_eval_global 295 | if (!is_module || is_compile_only) 296 | result = load_buf(ctx, opts, opts.eval_flags, !is_compile_only); 297 | else 298 | result = qjs_eval_module(ctx, opts); 299 | 300 | if (JS_IsException(result)) 301 | return JS_Throw(ctx, JS_GetException(ctx)); 302 | 303 | if (JS_IsPromise(result)) 304 | result = js_std_await(ctx, result); 305 | 306 | js_std_loop(ctx); 307 | if (JS_HasException(ctx)) 308 | return JS_Throw(ctx, JS_GetException(ctx)); 309 | 310 | // If the eval is ran in GLOBAL mode with flag JS_EVAL_FLAG_ASYNC 311 | // The result will be an object of: { value: } 312 | // We need to extract the value from the object 313 | bool is_global = (opts.eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_GLOBAL; 314 | bool is_async = (opts.eval_flags & JS_EVAL_FLAG_ASYNC) > 0; 315 | bool is_async_global = is_global && is_async; 316 | if (is_async_global) 317 | { 318 | // JSValue result_json = JS_JSONStringify(ctx, result, JS_NULL, JS_NULL); 319 | // printf("result is: %s\n", JS_ToCString(ctx, result_json)); 320 | JSAtom value_atom = JS_NewAtom(ctx, "value"); 321 | JSValue value = JS_GetProperty(ctx, result, value_atom); 322 | JS_FreeAtom(ctx, value_atom); 323 | JS_FreeValue(ctx, result); 324 | result = value; 325 | } 326 | 327 | return result; 328 | } 329 | 330 | /* 331 | * Compiles code (as a module or global script) to bytecode 332 | * if return value is NULL, an error occurred, 333 | * use JS_GetException() to retrieve error details. 334 | */ 335 | unsigned char *QJS_Compile(JSContext *c, QJSEvalOptions opts, size_t *outSize) 336 | { 337 | opts.eval_flags = opts.eval_flags ? opts.eval_flags : JS_EVAL_TYPE_GLOBAL; 338 | opts.eval_flags |= JS_EVAL_FLAG_COMPILE_ONLY; 339 | 340 | JSValue val = QJS_Eval(c, opts); 341 | if (JS_IsException(val)) 342 | return NULL; 343 | 344 | size_t psize = 0; // Bytecode size 345 | unsigned char *tmp_buf = JS_WriteObject(c, &psize, val, JS_WRITE_OBJ_BYTECODE); 346 | JS_FreeValue(c, val); 347 | 348 | // Validate the output: 349 | // ensure a positive bytecode size and a valid buffer pointer. 350 | if (tmp_buf == NULL || (int)psize <= 0) 351 | { 352 | if (tmp_buf != NULL) 353 | js_free(c, tmp_buf); 354 | return NULL; 355 | } 356 | 357 | unsigned char *result = (unsigned char *)malloc(psize); 358 | if (result == NULL) 359 | { 360 | js_free(c, tmp_buf); 361 | return NULL; 362 | } 363 | 364 | memcpy(result, tmp_buf, psize); 365 | js_free(c, tmp_buf); 366 | *outSize = psize; 367 | return result; 368 | } 369 | 370 | /** 371 | * Compiles code (as a module or global script) to bytecode and returns a packed uint64_t value 372 | * containing both the bytecode's memory address (high 32 bits) and length (low 32 bits). 373 | * 374 | * Returns NULL on error. 375 | * NOTE: The caller must free both the returned uint64_t pointer and the bytecode memory it points to. 376 | */ 377 | uint64_t *QJS_Compile2(JSContext *ctx, QJSEvalOptions opts) 378 | { 379 | size_t bytecode_len = 0; 380 | unsigned char *bytecode = QJS_Compile(ctx, opts, &bytecode_len); 381 | if (!bytecode) 382 | { 383 | return NULL; 384 | } 385 | 386 | uint64_t *result = malloc(sizeof(uint64_t)); 387 | if (!result) 388 | { 389 | free(bytecode); 390 | return NULL; // Allocation failure 391 | } 392 | 393 | // Store the address of the bytecode in the high 32 bits and the length in the low 32 bits 394 | *result = ((uint64_t)(uintptr_t)bytecode << 32) | (uint32_t)bytecode_len; 395 | return result; 396 | } 397 | 398 | QJSEvalOptions *QJS_CreateEvalOption(void *buf, uint8_t *bytecode_buf, size_t bytecode_len, char *filename, int eval_flags) 399 | { 400 | QJSEvalOptions *opts = (QJSEvalOptions *)malloc(sizeof(QJSEvalOptions)); 401 | if (opts == NULL) 402 | { 403 | // Memory allocation failed - return NULL so caller can handle the error 404 | return NULL; 405 | } 406 | 407 | opts->buf = buf; 408 | opts->bytecode_buf = bytecode_buf; 409 | opts->bytecode_len = bytecode_len; 410 | opts->filename = filename; 411 | opts->eval_flags = eval_flags; 412 | 413 | return opts; 414 | } 415 | -------------------------------------------------------------------------------- /eval_test.go: -------------------------------------------------------------------------------- 1 | package qjs_test 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/fastschema/qjs" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type evalTest struct { 12 | name string 13 | file string 14 | code string 15 | options []qjs.EvalOptionFunc 16 | prepare func(*qjs.Runtime) 17 | useByteCode bool 18 | wantError bool 19 | expectErr func(*testing.T, error) 20 | expectValue func(*testing.T, *qjs.Value, error) 21 | } 22 | 23 | func testGlobalModeEvaluation(t *testing.T) { 24 | t.Run("Basic_Scripts", func(t *testing.T) { 25 | tests := []evalTest{ 26 | {name: "no_result_statement", file: "global_no_result.js", code: "const a = 55555", expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.True(t, val.IsUndefined()) }}, 27 | {name: "expression_result", file: "global_has_result.js", code: "55555", expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, int32(55555), val.Int32()) }}, 28 | {name: "syntax_error", file: "syntax_error.js", code: "const a = (", expectErr: func(t *testing.T, err error) { assert.Error(t, err); assert.Contains(t, err.Error(), "SyntaxError") }}, 29 | {name: "runtime_error", file: "throw_error.js", code: "throw new Error('test error')", expectErr: func(t *testing.T, err error) { assert.Error(t, err); assert.Contains(t, err.Error(), "test error") }}, 30 | } 31 | runEvalTests(t, tests, "") 32 | }) 33 | 34 | t.Run("Top_Level_Await", func(t *testing.T) { 35 | tests := []evalTest{ 36 | {name: "top_level_await_error_without_flag", file: "global_top_level_await_error.js", code: "await Promise.resolve(42)", expectErr: func(t *testing.T, err error) { assert.Error(t, err); assert.Contains(t, err.Error(), "SyntaxError") }}, 37 | {name: "top_level_await_works_with_flag", file: "global_top_level_await_works.js", code: "await Promise.resolve(42)", options: []qjs.EvalOptionFunc{qjs.FlagAsync()}, expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, int32(42), val.Int32()) }}, 38 | } 39 | runEvalTests(t, tests, "") 40 | }) 41 | 42 | t.Run("From_Test_Files", func(t *testing.T) { 43 | globalTests := genModeGlobalTests(t) 44 | runModeGlobalTests(t, globalTests, false) // File mode 45 | runModeGlobalTests(t, globalTests, true) // Script mode 46 | }) 47 | } 48 | 49 | func testModuleModeEvaluation(t *testing.T) { 50 | t.Run("Basic_Modules", func(t *testing.T) { 51 | tests := []evalTest{ 52 | {name: "no_result_statement", file: "module_no_result.js", code: "const a = 55555", options: []qjs.EvalOptionFunc{qjs.TypeModule()}, expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.True(t, val.IsUndefined()) }}, 53 | {name: "no_default_export", file: "module_no_default_export.js", code: "export const a = 55555", options: []qjs.EvalOptionFunc{qjs.TypeModule()}, expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.True(t, val.IsUndefined()) }}, 54 | {name: "has_default_export", file: "module_has_result.js", code: "export default 55555", options: []qjs.EvalOptionFunc{qjs.TypeModule()}, expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, int32(55555), val.Int32()) }}, 55 | } 56 | runEvalTests(t, tests, "") 57 | }) 58 | 59 | t.Run("Top_Level_Await", func(t *testing.T) { 60 | tests := []evalTest{ 61 | {name: "module_top_level_await", file: "module_top_level_await.js", code: `async function main() { const res = await Promise.resolve(42); return res; } export default await main();`, options: []qjs.EvalOptionFunc{qjs.TypeModule()}, expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, int32(42), val.Int32()) }}, 62 | } 63 | runEvalTests(t, tests, "") 64 | }) 65 | 66 | t.Run("From_Test_Files", func(t *testing.T) { 67 | moduleTests := genModeModuleTests(t) 68 | runModeModuleTests(t, moduleTests, false) // File mode 69 | runModeModuleTests(t, moduleTests, true) // Script mode 70 | }) 71 | } 72 | 73 | func testLoadOperations(t *testing.T) { 74 | tests := []evalTest{ 75 | { 76 | name: "empty_filename_error", 77 | file: "", 78 | wantError: true, 79 | expectErr: func(t *testing.T, err error) { 80 | assert.Equal(t, qjs.ErrInvalidFileName, err) 81 | }, 82 | }, 83 | { 84 | name: "load_module_code", 85 | file: "01_load_module_code.js", 86 | code: "export const moduleExported = 55555", 87 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 88 | assert.Equal(t, int32(55555), val.Int32()) 89 | }, 90 | }, 91 | { 92 | name: "load_module_file", 93 | file: "02_load_module_file.js", 94 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 95 | assert.Equal(t, "exported from module file", val.String()) 96 | }, 97 | }, 98 | { 99 | name: "load_module_bytecode", 100 | file: "03_load_module_bytecode.js", 101 | code: "export const moduleExported = 55555", 102 | useByteCode: true, 103 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 104 | assert.Equal(t, int32(55555), val.Int32()) 105 | }, 106 | }, 107 | { 108 | name: "mixed_loading_scenario", 109 | file: "03_mixed_load.js", 110 | prepare: func(rt *qjs.Runtime) { 111 | _ = must(rt.Load("mod_a.js", qjs.Code("export const getA = () => 'A';"))) 112 | _ = must(rt.Load("./testdata/04_load/03_mixed/mod_b")) 113 | _ = must(rt.Load("./testdata/04_load/03_mixed/mod_c")) 114 | bytesD := must(rt.Compile("mod_d.js", qjs.Code("export const getD = () => 'D';"), qjs.TypeModule())) 115 | _ = must(rt.Load("mod_d.js", qjs.Bytecode(bytesD))) 116 | bytesE := must(rt.Compile("./testdata/04_load/03_mixed/mod_e", qjs.TypeModule())) 117 | _ = must(rt.Load("./testdata/04_load/03_mixed/mod_e", qjs.Bytecode(bytesE))) 118 | }, 119 | code: `import { getA } from 'mod_a.js'; import { getB } from './03_mixed/mod_b'; import { getC } from './03_mixed/mod_c'; import { getD } from 'mod_d.js'; import { getE } from './03_mixed/mod_e'; export const moduleExported = getA() + getB() + getC() + getD() + getE();`, 120 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 121 | assert.Equal(t, "ABCDE", val.String()) 122 | }, 123 | }, 124 | } 125 | 126 | runLoadTests(t, tests, "./testdata/04_load") 127 | } 128 | 129 | func runEvalTests(t *testing.T, tests []evalTest, basePath string) { 130 | for _, test := range tests { 131 | t.Run(test.name, func(t *testing.T) { 132 | rt := must(qjs.New()) 133 | defer rt.Close() 134 | 135 | if test.prepare != nil { 136 | test.prepare(rt) 137 | } 138 | 139 | testFile := test.file 140 | if basePath != "" && testFile != "" { 141 | testFile = path.Join(basePath, test.file) 142 | } 143 | 144 | var options []qjs.EvalOptionFunc 145 | if test.code != "" { 146 | if test.useByteCode { 147 | bytecode := must(rt.Compile(testFile, qjs.Code(test.code), qjs.TypeModule())) 148 | options = append(options, qjs.Bytecode(bytecode)) 149 | } else { 150 | options = append(options, qjs.Code(test.code)) 151 | } 152 | } 153 | options = append(options, test.options...) 154 | 155 | val, err := rt.Eval(testFile, options...) 156 | if val != nil { 157 | defer val.Free() 158 | } 159 | 160 | if test.wantError { 161 | assert.Error(t, err) 162 | if test.expectErr != nil { 163 | test.expectErr(t, err) 164 | } 165 | return 166 | } 167 | 168 | if test.expectErr != nil { 169 | test.expectErr(t, err) 170 | return 171 | } 172 | 173 | assert.NoError(t, err) 174 | if test.expectValue != nil { 175 | test.expectValue(t, val, err) 176 | } 177 | }) 178 | } 179 | } 180 | 181 | func runCompileTests(t *testing.T, tests []evalTest, basePath string) { 182 | t.Run("compile_error", func(t *testing.T) { 183 | rt := must(qjs.New()) 184 | defer rt.Close() 185 | 186 | _, err := rt.Compile("compile_error.js", qjs.Code("const a = (")) 187 | assert.Error(t, err) 188 | }) 189 | 190 | for _, test := range tests { 191 | t.Run(test.name, func(t *testing.T) { 192 | rt := must(qjs.New()) 193 | defer rt.Close() 194 | 195 | testFile := path.Join(basePath, test.file) 196 | 197 | // Compile phase 198 | var compileOptions []qjs.EvalOptionFunc 199 | if test.code != "" { 200 | compileOptions = append(compileOptions, qjs.Code(test.code)) 201 | } 202 | compileOptions = append(compileOptions, test.options...) 203 | 204 | bytecode, err := rt.Compile(testFile, compileOptions...) 205 | assert.NoError(t, err) 206 | assert.NotEmpty(t, bytecode) 207 | 208 | // Execute phase 209 | var evalOptions []qjs.EvalOptionFunc 210 | evalOptions = append(evalOptions, qjs.Bytecode(bytecode)) 211 | evalOptions = append(evalOptions, test.options...) 212 | 213 | val, err := rt.Eval(testFile, evalOptions...) 214 | defer val.Free() 215 | assert.NoError(t, err) 216 | if test.expectValue != nil { 217 | test.expectValue(t, val, err) 218 | } 219 | }) 220 | } 221 | } 222 | 223 | func runLoadTests(t *testing.T, tests []evalTest, basePath string) { 224 | for _, test := range tests { 225 | t.Run(test.name, func(t *testing.T) { 226 | rt := must(qjs.New()) 227 | defer rt.Close() 228 | 229 | if test.prepare != nil { 230 | test.prepare(rt) 231 | } 232 | 233 | testFile := test.file 234 | if testFile != "" { 235 | testFile = path.Join(basePath, test.file) 236 | } 237 | 238 | var options []qjs.EvalOptionFunc 239 | if test.code != "" { 240 | if test.useByteCode { 241 | bytecode := must(rt.Compile(testFile, qjs.TypeModule(), qjs.Code(test.code))) 242 | options = append(options, qjs.Bytecode(bytecode)) 243 | } else { 244 | options = append(options, qjs.Code(test.code)) 245 | } 246 | } 247 | 248 | loadVal, err := rt.Load(testFile, options...) 249 | if test.expectErr != nil { 250 | test.expectErr(t, err) 251 | return 252 | } 253 | 254 | if test.wantError { 255 | assert.Error(t, err) 256 | return 257 | } 258 | 259 | loadVal.Free() 260 | 261 | // Test by importing the loaded module 262 | val, err := rt.Eval("import_"+test.name, qjs.Code(`import { moduleExported } from '`+testFile+`'; export default moduleExported`), qjs.TypeModule()) 263 | defer val.Free() 264 | assert.NoError(t, err) 265 | if test.expectValue != nil { 266 | test.expectValue(t, val, err) 267 | } 268 | }) 269 | } 270 | } 271 | 272 | func TestEval(t *testing.T) { 273 | t.Run("Invalid_Inputs", func(t *testing.T) { 274 | tests := []evalTest{ 275 | { 276 | name: "empty_filename", 277 | file: "", 278 | wantError: true, 279 | expectErr: func(t *testing.T, err error) { 280 | assert.Equal(t, qjs.ErrInvalidFileName, err) 281 | }, 282 | }, 283 | { 284 | name: "empty_filename", 285 | file: "", 286 | wantError: true, 287 | expectErr: func(t *testing.T, err error) { 288 | assert.Equal(t, qjs.ErrInvalidFileName, err) 289 | }, 290 | }, 291 | } 292 | runEvalTests(t, tests, "") 293 | }) 294 | 295 | t.Run("Invalid_Inputs", func(t *testing.T) { 296 | tests := []evalTest{ 297 | {name: "empty_filename", file: "", wantError: true, expectErr: func(t *testing.T, err error) { assert.Equal(t, qjs.ErrInvalidFileName, err) }}, 298 | } 299 | runEvalTests(t, tests, "") 300 | }) 301 | 302 | t.Run("Load_Operations", func(t *testing.T) { 303 | testLoadOperations(t) 304 | }) 305 | 306 | t.Run("Error_Normalization", func(t *testing.T) { 307 | tests := []evalTest{ 308 | {name: "throw_error_normalization", file: "error_test.js", code: `throw new Error("ThrowError test")`, expectErr: func(t *testing.T, err error) { 309 | assert.Error(t, err) 310 | assert.Contains(t, err.Error(), "ThrowError test") 311 | }}, 312 | {name: "is_error_path_normalization", file: "error_test.js", code: `new Error("IsError test")`, expectErr: func(t *testing.T, err error) { assert.Error(t, err); assert.Contains(t, err.Error(), "IsError test") }}, 313 | } 314 | runEvalTests(t, tests, "") 315 | }) 316 | } 317 | 318 | // Module Loading Tests 319 | func TestModuleLoading(t *testing.T) { 320 | t.Run("File_Resolution", func(t *testing.T) { 321 | tests := []evalTest{ 322 | { 323 | name: "file_not_found", 324 | file: "not_found.js", 325 | options: []qjs.EvalOptionFunc{qjs.TypeModule()}, 326 | expectErr: func(t *testing.T, err error) { 327 | assert.Error(t, err) 328 | assert.Contains(t, err.Error(), "No such file or directory: testdata/00_loader/not_found.js") 329 | }, 330 | }, 331 | { 332 | name: "real_filename", 333 | file: "01_realname/file.js", 334 | options: []qjs.EvalOptionFunc{qjs.TypeModule()}, 335 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 336 | assert.Equal(t, "Hello, World!", val.String()) 337 | }, 338 | }, 339 | } 340 | runEvalTests(t, tests, "./testdata/00_loader") 341 | }) 342 | 343 | t.Run("Auto_Extensions", func(t *testing.T) { 344 | tests := []evalTest{ 345 | { 346 | name: "auto_index_js", 347 | file: "02_autoindexjs", 348 | options: []qjs.EvalOptionFunc{qjs.TypeModule()}, 349 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 350 | assert.Equal(t, "autoindexjs", val.String()) 351 | }, 352 | }, 353 | { 354 | name: "auto_index_mjs", 355 | file: "03_autoindexmjs", 356 | options: []qjs.EvalOptionFunc{qjs.TypeModule()}, 357 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 358 | assert.Equal(t, "autoindexmjs", val.String()) 359 | }, 360 | }, 361 | { 362 | name: "auto_ext_js", 363 | file: "04_autoextjs/file", 364 | options: []qjs.EvalOptionFunc{qjs.TypeModule()}, 365 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 366 | assert.Equal(t, "file.js", val.String()) 367 | }, 368 | }, 369 | { 370 | name: "auto_ext_mjs", 371 | file: "05_autoextmjs/file", 372 | options: []qjs.EvalOptionFunc{qjs.TypeModule()}, 373 | expectValue: func(t *testing.T, val *qjs.Value, err error) { 374 | assert.Equal(t, "file.mjs", val.String()) 375 | }, 376 | }, 377 | } 378 | runEvalTests(t, tests, "./testdata/00_loader") 379 | }) 380 | } 381 | 382 | // Compilation Tests 383 | func TestCompilation(t *testing.T) { 384 | t.Run("Bytecode_Generation", func(t *testing.T) { 385 | tests := []evalTest{ 386 | {name: "compile_global_code", file: "compile_global_code.js", code: "55555", expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, int32(55555), val.Int32()) }}, 387 | {name: "compile_global_file", file: "01_global/index.js", expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, "Hello, World!", val.String()) }}, 388 | {name: "compile_module_code", file: "compile_module_code.js", code: "export default 55555", options: []qjs.EvalOptionFunc{qjs.TypeModule()}, expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, int32(55555), val.Int32()) }}, 389 | {name: "compile_module_file", file: "02_module/index.js", options: []qjs.EvalOptionFunc{qjs.TypeModule()}, expectValue: func(t *testing.T, val *qjs.Value, err error) { assert.Equal(t, "Hello from lib_a", val.String()) }}, 390 | } 391 | 392 | runCompileTests(t, tests, "./testdata/03_compile") 393 | }) 394 | } 395 | 396 | // Script Evaluation Tests 397 | func TestScriptEvaluation(t *testing.T) { 398 | t.Run("Global_Mode", func(t *testing.T) { 399 | testGlobalModeEvaluation(t) 400 | }) 401 | 402 | t.Run("Module_Mode", func(t *testing.T) { 403 | testModuleModeEvaluation(t) 404 | }) 405 | } 406 | -------------------------------------------------------------------------------- /gotojs.go: -------------------------------------------------------------------------------- 1 | package qjs 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // ToJsValue converts any Go value to a QuickJS value. 12 | func ToJsValue(c *Context, v any) (*Value, error) { 13 | jsVal, err := NewTracker[uintptr]().toJsValue(c, v) 14 | if err != nil { 15 | return nil, fmt.Errorf("[ToJSValue] %w", err) 16 | } 17 | 18 | if !jsVal.IsNull() && jsVal.IsObject() { 19 | goValueType := reflect.TypeOf(v).String() 20 | jsVal.SetPropertyStr("__go_type", c.NewString(goValueType)) 21 | jsVal.SetPropertyStr("__registry_id", c.NewInt64(int64(c.runtime.registry.Register(v)))) 22 | } 23 | 24 | return jsVal, nil 25 | } 26 | 27 | // toJsValue converts any Go value to a QuickJS Value using the conversion context. 28 | func (tracker *Tracker[T]) toJsValue(c *Context, v any) (*Value, error) { 29 | if v == nil { 30 | return c.NewNull(), nil 31 | } 32 | 33 | if err, ok := v.(error); ok { 34 | return c.NewError(err), nil 35 | } 36 | 37 | if jsVal := tryConvertBuiltinTypes(c, v); jsVal != nil { 38 | return jsVal, nil 39 | } 40 | 41 | if jsVal := tryConvertNumeric(c, v); jsVal != nil { 42 | return jsVal, nil 43 | } 44 | 45 | return tracker.convertReflectValue(c, v) 46 | } 47 | 48 | // GoStructToJs converts a Go struct to a JavaScript object. 49 | // Includes both fields and methods as object properties. 50 | func (tracker *Tracker[T]) GoStructToJs( 51 | c *Context, 52 | rtype reflect.Type, 53 | rval reflect.Value, 54 | ) (*Value, error) { 55 | return withJSObject(c, func(obj *Value) error { 56 | // obj.SetPropertyStr("__go_type", c.NewString(GetGoTypeName(rtype))) 57 | 58 | // Determine the struct type and value for field processing: 59 | // - pointer to struct: dereference for field processing. 60 | // - direct struct: use as-is. 61 | var ( 62 | structType reflect.Type 63 | structVal reflect.Value 64 | ) 65 | 66 | if rtype.Kind() == reflect.Pointer { 67 | structType = rtype.Elem() 68 | structVal = rval.Elem() 69 | } else { 70 | structType = rtype 71 | structVal = rval 72 | } 73 | 74 | err := tracker.addStructFieldsToObject(c, obj, structType, structVal) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | // For methods, use the original type and value to preserve pointer receiver methods 80 | return tracker.addStructMethodsToObject(c, obj, rtype, rval) 81 | }) 82 | } 83 | 84 | // GoSliceToJs converts a Go slice to a JavaScript array. 85 | func (tracker *Tracker[T]) GoSliceToJs(c *Context, rval reflect.Value) (*Value, error) { 86 | return tracker.arrayLikeToJS(c, rval, "slice") 87 | } 88 | 89 | // GoArrayToJs converts a Go array to a JavaScript array without unnecessary copying. 90 | func (tracker *Tracker[T]) GoArrayToJs(c *Context, rval reflect.Value) (*Value, error) { 91 | return tracker.arrayLikeToJS(c, rval, "array") 92 | } 93 | 94 | // GoMapToJs converts a Go map to a JavaScript object. 95 | // Non-string keys are converted to string representation. 96 | func (tracker *Tracker[T]) GoMapToJs( 97 | c *Context, 98 | rval reflect.Value, 99 | ) (*Value, error) { 100 | return withJSObject(c, func(obj *Value) error { 101 | for _, key := range rval.MapKeys() { 102 | value := rval.MapIndex(key) 103 | 104 | var keyStr string 105 | if key.Kind() == reflect.String { 106 | keyStr = key.String() 107 | } else { 108 | keyStr = fmt.Sprintf("%v", key.Interface()) 109 | } 110 | 111 | jsValue, err := tracker.toJsValue(c, value.Interface()) 112 | if err != nil { 113 | return newGoToJsErr("map key: "+keyStr, err) 114 | } 115 | 116 | obj.SetPropertyStr(keyStr, jsValue) 117 | } 118 | 119 | return nil 120 | }) 121 | } 122 | 123 | // GoNumberToJs converts Go numeric types to appropriate JS number types. 124 | func GoNumberToJs[T NumberType](c *Context, i T) *Value { 125 | switch v := any(i).(type) { 126 | case int32: 127 | return c.NewInt32(v) 128 | case uint32: 129 | return c.NewUint32(v) 130 | case int64: 131 | return c.NewInt64(v) 132 | case float64: 133 | return c.NewFloat64(v) 134 | case float32: 135 | return c.NewFloat64(float64(v)) 136 | default: 137 | // Convert other integer types to int64, floats to float64 138 | switch any(i).(type) { 139 | case int, int8, int16: 140 | return c.NewInt64(int64(i)) 141 | case uint, uint8, uint16, uint64: 142 | if uint64(i) > math.MaxInt64 { 143 | return c.NewBigUint64(uint64(i)) 144 | } 145 | 146 | return c.NewInt64(int64(i)) 147 | default: 148 | return c.NewFloat64(float64(i)) 149 | } 150 | } 151 | } 152 | 153 | // GoComplexToJs converts Go complex numbers to JS objects with real/imag properties. 154 | func GoComplexToJs[T complex64 | complex128](c *Context, z T) *Value { 155 | obj := c.NewObject() 156 | 157 | var realPart, imagPart float64 158 | 159 | switch v := any(z).(type) { 160 | case complex64: 161 | realPart = float64(real(v)) 162 | imagPart = float64(imag(v)) 163 | case complex128: 164 | realPart = real(v) 165 | imagPart = imag(v) 166 | } 167 | 168 | obj.SetPropertyStr("real", c.NewFloat64(realPart)) 169 | obj.SetPropertyStr("imag", c.NewFloat64(imagPart)) 170 | 171 | return obj 172 | } 173 | 174 | // GoStructToJs converts Go structs to JavaScript objects. 175 | func GoStructToJs( 176 | c *Context, 177 | rtype reflect.Type, 178 | rval reflect.Value, 179 | ) (*Value, error) { 180 | return NewTracker[uint64]().GoStructToJs(c, rtype, rval) 181 | } 182 | 183 | // GoSliceToJs converts Go slices to JavaScript arrays. 184 | func GoSliceToJs(c *Context, rval reflect.Value) (*Value, error) { 185 | return NewTracker[uint64]().GoSliceToJs(c, rval) 186 | } 187 | 188 | // GoMapToJs converts Go maps to JavaScript objects. 189 | func GoMapToJs(c *Context, rval reflect.Value) (*Value, error) { 190 | return NewTracker[uint64]().GoMapToJs(c, rval) 191 | } 192 | 193 | // tryConvertBuiltinTypes handles built-in Go types that don't require reflection. 194 | func tryConvertBuiltinTypes(c *Context, v any) *Value { 195 | switch val := v.(type) { 196 | case bool: 197 | return c.NewBool(val) 198 | case string: 199 | return c.NewString(val) 200 | case []byte: 201 | if val == nil { 202 | return c.NewNull() 203 | } 204 | 205 | return c.NewArrayBuffer(val) 206 | case time.Time: 207 | return c.NewDate(&val) 208 | case *time.Time: 209 | if val == nil { 210 | return c.NewNull() 211 | } 212 | 213 | return c.NewDate(val) 214 | case uintptr: 215 | return c.NewInt64(int64(val)) 216 | } 217 | 218 | return nil 219 | } 220 | 221 | // tryConvertNumeric handles all numeric types in a consolidated way. 222 | func tryConvertNumeric(c *Context, v any) *Value { 223 | switch val := v.(type) { 224 | case int8: 225 | return GoNumberToJs(c, val) 226 | case int16: 227 | return GoNumberToJs(c, val) 228 | case int32: 229 | return GoNumberToJs(c, val) 230 | case int: 231 | return GoNumberToJs(c, val) 232 | case int64: 233 | return GoNumberToJs(c, val) 234 | case uint8: 235 | return GoNumberToJs(c, val) 236 | case uint16: 237 | return GoNumberToJs(c, val) 238 | case uint32: 239 | return GoNumberToJs(c, val) 240 | case uint: 241 | return GoNumberToJs(c, val) 242 | case uint64: 243 | // Large uint64 values use float64 to avoid overflow 244 | if val&(uint64(1)< math.MaxInt64 { 323 | return c.NewFloat64(float64(uintVal)), nil 324 | } 325 | 326 | return GoNumberToJs(c, int64(uintVal)), nil 327 | case reflect.Float32, reflect.Float64: 328 | return GoNumberToJs(c, rval.Float()), nil 329 | case reflect.String: 330 | return c.NewString(rval.String()), nil 331 | case reflect.Bool: 332 | return c.NewBool(rval.Bool()), nil 333 | } 334 | 335 | return nil, newGoToJsErr(rtype.Name(), nil) 336 | } 337 | 338 | // withJSObject creates a JS object and ensures it's freed on error. 339 | func withJSObject(c *Context, fn func(*Value) error) (*Value, error) { 340 | obj := c.NewObject() 341 | if err := fn(obj); err != nil { 342 | obj.Free() 343 | 344 | return nil, err 345 | } 346 | 347 | return obj, nil 348 | } 349 | 350 | // addStructFieldsToObject converts struct fields to JS object properties. 351 | // Processes embedded fields first, then regular fields to allow overriding. 352 | func (tracker *Tracker[T]) addStructFieldsToObject( 353 | c *Context, 354 | obj *Value, 355 | rtype reflect.Type, 356 | rval reflect.Value, 357 | ) error { 358 | // Process embedded fields first to enable field overriding 359 | if err := tracker.processEmbeddedFields(c, obj, rtype, rval); err != nil { 360 | return err 361 | } 362 | 363 | // Process regular fields (can override embedded fields) 364 | return tracker.processRegularFields(c, obj, rtype, rval) 365 | } 366 | 367 | // processEmbeddedFields handles anonymous/embedded fields in structs. 368 | func (tracker *Tracker[T]) processEmbeddedFields( 369 | c *Context, 370 | obj *Value, 371 | rtype reflect.Type, 372 | rval reflect.Value, 373 | ) error { 374 | for i := range rtype.NumField() { 375 | field := rtype.Field(i) 376 | jsonIgnore := field.Tag.Get("json") == "-" 377 | 378 | if !field.IsExported() || !field.Anonymous || jsonIgnore { 379 | continue 380 | } 381 | 382 | if err := tracker.processEmbeddedField(c, obj, field, rval.Field(i)); err != nil { 383 | return err 384 | } 385 | } 386 | 387 | return nil 388 | } 389 | 390 | // processEmbeddedField handles a single embedded field. 391 | func (tracker *Tracker[T]) processEmbeddedField( 392 | c *Context, 393 | obj *Value, 394 | field reflect.StructField, 395 | fieldValue reflect.Value, 396 | ) error { 397 | switch fieldValue.Kind() { 398 | case reflect.Struct: 399 | return tracker.addStructFieldsToObject(c, obj, field.Type, fieldValue) 400 | case reflect.Ptr: 401 | return tracker.processEmbeddedPointer(c, obj, field.Name, field.Type, fieldValue) 402 | default: 403 | return tracker.addEmbeddedPrimitive(c, obj, field.Name, fieldValue) 404 | } 405 | } 406 | 407 | // processEmbeddedPointer handles embedded pointer fields. 408 | func (tracker *Tracker[T]) processEmbeddedPointer( 409 | c *Context, 410 | obj *Value, 411 | fieldName string, 412 | fieldType reflect.Type, 413 | fieldValue reflect.Value, 414 | ) error { 415 | elem := fieldValue.Elem() 416 | if elem.Kind() == reflect.Struct { 417 | // Embedded pointer to struct: recursively process struct fields 418 | return tracker.addStructFieldsToObject(c, obj, fieldType.Elem(), elem) 419 | } 420 | 421 | // Embedded pointer to primitive: add the dereferenced value as field 422 | return tracker.addEmbeddedPrimitive(c, obj, fieldName, elem) 423 | } 424 | 425 | // addEmbeddedPrimitive adds an embedded primitive type as a field. 426 | func (tracker *Tracker[T]) addEmbeddedPrimitive( 427 | c *Context, 428 | obj *Value, 429 | fieldName string, 430 | fieldValue reflect.Value, 431 | ) error { 432 | if !fieldValue.IsValid() { 433 | return nil 434 | } 435 | 436 | prop, err := tracker.toJsValue(c, fieldValue.Interface()) 437 | obj.SetPropertyStr(fieldName, prop) 438 | 439 | return err 440 | } 441 | 442 | // processRegularFields handles non-anonymous fields in structs. 443 | func (tracker *Tracker[T]) processRegularFields( 444 | c *Context, 445 | obj *Value, 446 | rtype reflect.Type, 447 | rval reflect.Value, 448 | ) error { 449 | for i := range rtype.NumField() { 450 | field := rtype.Field(i) 451 | 452 | if !field.IsExported() || field.Anonymous { 453 | continue 454 | } 455 | 456 | fieldName, skip := tracker.getFieldName(field) 457 | if skip { 458 | continue 459 | } 460 | 461 | prop, err := tracker.toJsValue(c, rval.Field(i).Interface()) 462 | if err != nil { 463 | return err 464 | } 465 | 466 | obj.SetPropertyStr(fieldName, prop) 467 | } 468 | 469 | return nil 470 | } 471 | 472 | // getFieldName determines the field name considering JSON tags. 473 | func (tracker *Tracker[T]) getFieldName(field reflect.StructField) (string, bool) { 474 | fieldName := field.Name 475 | if jsonTag := field.Tag.Get("json"); jsonTag != "" { 476 | name, _, _ := strings.Cut(jsonTag, ",") 477 | if name == "-" { 478 | return "", true 479 | } 480 | 481 | if name != "" { 482 | fieldName = name 483 | } 484 | } 485 | 486 | return fieldName, false 487 | } 488 | 489 | // addStructMethodsToObject adds exported struct methods as JS functions. 490 | func (tracker *Tracker[T]) addStructMethodsToObject( 491 | c *Context, 492 | obj *Value, 493 | rtype reflect.Type, 494 | rval reflect.Value, 495 | ) error { 496 | for i := range rtype.NumMethod() { 497 | method := rtype.Method(i) 498 | methodValue := rval.Method(i) 499 | 500 | jsMethod, err := FuncToJS(c, methodValue.Interface()) 501 | if err != nil { 502 | return newGoToJsErr( 503 | GetGoTypeName(rtype)+"."+method.Name, 504 | err, 505 | "struct method", 506 | ) 507 | } 508 | 509 | obj.SetPropertyStr(method.Name, jsMethod) 510 | } 511 | 512 | return nil 513 | } 514 | 515 | // arrayLikeToJS is a helper that converts arrays and slices to JS arrays. 516 | func (tracker *Tracker[T]) arrayLikeToJS( 517 | c *Context, 518 | rval reflect.Value, 519 | typeName string, 520 | ) (*Value, error) { 521 | arr := c.NewArray() 522 | 523 | for i := range rval.Len() { 524 | elem := rval.Index(i) 525 | 526 | jsElem, err := tracker.toJsValue(c, elem.Interface()) 527 | if err != nil { 528 | arr.Free() 529 | 530 | return nil, newGoToJsErr(typeName+": "+GetGoTypeName(rval.Type()), err) 531 | } 532 | 533 | arr.SetPropertyIndex(int64(i), jsElem) 534 | } 535 | 536 | return arr.Value, nil 537 | } 538 | --------------------------------------------------------------------------------