English | 简体中文
Go 语言的 QuickJS 绑定库:快速、小型、可嵌入的 ES2020 JavaScript 解释器。
⚠️ 此项目尚未准备好用于生产环境。请自行承担使用风险。API 可能会随时更改。
以下基准测试结果由 benchcmp 工具生成。如需重新生成,请运行 cd benchcmp && go run . 并更新此文件。
默认 cd benchcmp && go run . 输出如下:
Computing factorial(10) 1,000,000 times
| Iteration | quickjs-go(cgo / QuickJS-ng) | GOJA(pure Go) | ModerncQuickJS(ccgo / QuickJS) | QJS(Wasm / wazero) | V8go(cgo / V8 JIT) |
|---|---|---|---|---|---|
| 1 | 213.363ms | 712.622ms | 879.695ms | 626.162ms | 37.025ms |
| 2 | 309.421ms | 728.404ms | 878.465ms | 626.668ms | 22.883ms |
| 3 | 208.848ms | 697.569ms | 913.305ms | 629.525ms | 22.682ms |
| 4 | 217.621ms | 672.554ms | 951.755ms | 627.514ms | 22.803ms |
| 5 | 317.197ms | 695.230ms | 964.052ms | 627.884ms | 23.183ms |
| Average | 253.290ms | 701.276ms | 917.455ms | 627.551ms | 25.715ms |
| Total | 1.266s | 3.506s | 4.587s | 3.138s | 128.576ms |
| Speed vs quickjs-go(cgo / QuickJS-ng) | 1.00x | 0.36x | 0.28x | 0.40x | 9.85x |
| Metric | quickjs-go(cgo / QuickJS-ng) | GOJA(pure Go) | ModerncQuickJS(ccgo / QuickJS) | QJS(Wasm / wazero) | V8go(cgo / V8 JIT) |
|---|---|---|---|---|---|
| Richards | 1425 | 453 | 259 | 470 | 51086 |
| DeltaBlue | 1398 | 461 | 285 | 561 | 191967 |
| Crypto | 1021 | 287 | 304 | 382 | 97807 |
| RayTrace | 1628 | 588 | 519 | 760 | 208751 |
| EarleyBoyer | 3216 | 1220 | 774 | 1163 | 152728 |
| RegExp | 459 | 415 | 199 | 211 | 21456 |
| Splay | 3989 | 2049 | 1324 | 2331 | 68909 |
| NavierStokes | 1800 | 450 | 529 | 625 | 67832 |
| Score (version 7) | 1560 | 599 | 434 | 638 | 86266 |
| Duration (seconds) | 35.108s | 61.477s | 70.983s | 57.522s | 20.052s |
| Score vs quickjs-go(cgo / QuickJS-ng) | 1.00x | 0.38x | 0.28x | 0.41x | 55.30x |
| Speed vs quickjs-go(cgo / QuickJS-ng) | 1.00x | 0.57x | 0.49x | 0.61x | 1.75x |
如果想看 benchmark 命令、套件选择和完整输出格式说明,可继续参考 benchcmp/README.md。
本次 Benchmarks 运行环境:Apple M4,32GB RAM,macOS。
本项目已将上游运行时源码从 bellard/quickjs 迁移到 quickjs-ng/quickjs。
这次迁移的主要原因是长期可维护性,而不是对 quickjs-go 的 Go API 做出调整。quickjs-ng 在社区协作、发布节奏、跨平台支持,以及测试和 CI 覆盖方面更适合作为绑定库项目的长期上游。对于一个需要将引擎源码 vendored 到仓库中、并通过 cgo 在 Linux、macOS 和 Windows 上持续构建与测试的 Go 绑定库来说,这些工程能力会直接影响日常维护成本和版本升级效率。
此次迁移仅涉及底层 vendored JavaScript 引擎实现的上游切换;quickjs-go 对外提供的 Go API 设计与使用方式保持不变,现有 Go 侧集成代码无需因本次迁移而调整。
deps/quickjs 中的运行时源码不再通过 git submodule 更新,而是按 quickjs-ng GitHub release 进行同步。
go test ./... 验证当前仓库。WithOwnerGoroutineCheck(false) 显式关闭 owner goroutine 校验。注意 WithOwnerGoroutineCheck(false) 是显式不安全开关,仅适用于由你自己的调度器在外部严格串行化所有 QuickJS 访问的场景,一旦该前提被破坏,跨 goroutine 调用可能与 QuickJS 内部状态竞争并导致内存破坏。WithStrictOSThread(true);该选项只负责校验,不会自动绑定线程。如果启用了该模式,请由调用方在 owner goroutine 中自行调用 runtime.LockOSThread(),并在创建 Runtime 之前完成绑定。Runtime.NewContext() 保持默认宿主 bootstrap 行为(注册 std/os 并注入全局 setTimeout/clearTimeout)。如需更细粒度控制,可使用 Runtime.NewBareContext() 后手动调用 BootstrapStdOS / BootstrapTimers,或使用 Runtime.NewContextWithOptions(...) 配合 DefaultBootstrap、MinimalBootstrap、NoBootstrap。Runtime.NewContextWithOptions(...) 中,启用 WithBootstrapTimers(true) 会隐式启用 std/os 注册,因为计时器注入依赖从 os 模块导入 setTimeout/clearTimeout。*Value 对象调用 value.Free();Runtime 和 Context 使用完毕后通过 Close() 清理。Value 的上下文引用已失效时,Value.Free() 会 fail-closed,避免关闭后的不安全 cgo 调用。Set/SetIdx 变为 no-op,Call/Execute/CallConstructor/已废弃的 Context.Invoke 返回 nil。Marshal/Unmarshal 在无效 context/value 输入下采用 fail-closed:对 nil/closed context、nil 源值或跨 Context 值返回受控错误,不再向 C 层传递无效 ref。Context.Close() 之后,边界查询采用 fail-closed:Runtime() 返回 nil,HasException() 返回 false,Exception() 返回 nil,Loop() 变为 no-op,Schedule() 返回 false。contextID + handleID)解析,finalizer 与 Value.GetGoObject() 使用确定性归属定位。nil 时,会统一映射为 JavaScript undefined。nil、上下文可用且属于当前初始化上下文。runtime.SetFinalizer(),因为它可能干扰 QuickJS GC。Value.IsException() 或 Context.HasException() 检查异常Context.Exception() 获取异常作为 Go errordefer value.Free() 以防止内存泄漏Context.HasException()import "github.com/buke/quickjs-go"
rt := quickjs.NewRuntime()
defer rt.Close()
// 默认模式(与历史行为兼容)
ctxDefault := rt.NewContext()
defer ctxDefault.Close()
// 裸 Context(不注册 std/os,不注入计时器)
ctxBare := rt.NewBareContext()
defer ctxBare.Close()
// 手动应用 bootstrap
quickjs.BootstrapStdOS(ctxBare)
quickjs.BootstrapTimers(ctxBare)
// 通过选项选择 bootstrap
ctxMinimal := rt.NewContextWithOptions(quickjs.MinimalBootstrap())
defer ctxMinimal.Close()
package main
import (
"fmt"
"runtime"
"github.com/buke/quickjs-go"
)
func main() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// 创建新的运行时
rt := quickjs.NewRuntime()
defer rt.Close()
// 创建新的上下文
ctx := rt.NewContext()
defer ctx.Close()
ret := ctx.Eval("'Hello ' + 'QuickJS!'")
defer ret.Free()
if ret.IsException() {
err := ctx.Exception()
println(err.Error())
return
}
fmt.Println(ret.ToString())
}
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
// 创建新的运行时
rt := quickjs.NewRuntime()
defer rt.Close()
// 创建新的上下文
ctx := rt.NewContext()
defer ctx.Close()
test := ctx.NewObject()
test.Set("A", ctx.NewString("String A"))
test.Set("B", ctx.NewString("String B"))
test.Set("C", ctx.NewString("String C"))
ctx.Globals().Set("test", test)
ret := ctx.Eval(`Object.keys(test).map(key => test[key]).join(" ")`)
defer ret.Free()
if ret.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(ret.ToString())
}
package main
import (
"fmt"
"time"
"github.com/buke/quickjs-go"
)
func main() {
// 创建新的运行时
rt := quickjs.NewRuntime()
defer rt.Close()
// 创建新的上下文
ctx := rt.NewContext()
defer ctx.Close()
// 创建新对象
test := ctx.NewObject()
// 将属性绑定到对象
test.Set("A", ctx.NewString("String A"))
test.Set("B", ctx.NewInt32(0))
test.Set("C", ctx.NewBool(false))
// 将 go 函数绑定到 js 对象
test.Set("hello", ctx.NewFunction(func(ctx *quickjs.Context, this *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
return ctx.NewString("Hello " + args[0].ToString())
}))
// 将 "test" 对象绑定到全局对象
ctx.Globals().Set("test", test)
// 通过 js 调用 js 函数(同步)
js_ret := ctx.Eval(`test.hello("Javascript!")`)
defer js_ret.Free()
if js_ret.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(js_ret.ToString())
// 通过 go 调用 js 函数(同步)
go_ret := test.Call("hello", ctx.NewString("Golang!"))
defer go_ret.Free()
if go_ret.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(go_ret.ToString())
// --- 同步 Promise 示例(立即 resolve/reject,不开 goroutine)---
ctx.Globals().Set("syncPromise", ctx.NewFunction(func(ctx *quickjs.Context, this *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
return ctx.NewPromise(func(resolve, reject func(*quickjs.Value)) {
// 可以在 executor 中直接同步 resolve:
msg := ctx.NewString("Hello from sync Promise")
defer msg.Free()
resolve(msg)
// 如需同步失败,可以改为:
// errVal := ctx.NewString("sync error")
// defer errVal.Free()
// reject(errVal)
})
}))
syncPromise := ctx.Eval(`syncPromise()`)
defer syncPromise.Free()
syncRet := ctx.Await(syncPromise)
defer syncRet.Free()
if syncRet.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(syncRet.ToString())
// --- 使用 Function + Promise + 调度器 的异步示例 ---
ctx.Globals().Set("testAsync", ctx.NewFunction(func(ctx *quickjs.Context, this *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
return ctx.NewPromise(func(resolve, reject func(*quickjs.Value)) {
// 耗时或阻塞操作可以放在 goroutine 中
go func() {
time.Sleep(10 * time.Millisecond)
// 但 QuickJS/Context API 必须回到 Context 线程上
// 通过 ctx.Schedule 调度执行
ctx.Schedule(func(inner *quickjs.Context) {
value := inner.NewString("Hello Async Function!")
defer value.Free()
resolve(value)
})
}()
})
}))
// 从 JS 角度看,这只是一个返回 Promise 的普通函数
promiseResult := ctx.Eval(`testAsync()`)
defer promiseResult.Free()
// ctx.Await 内部会驱动 QuickJS 的 pending-job 队列
// 以及 Context 级调度器,因此如果你只关心 Promise
// 的最终结果,通常不需要单独调用 ctx.Loop()
asyncRet := ctx.Await(promiseResult)
defer asyncRet.Free()
if asyncRet.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(asyncRet.ToString())
// 输出:
// Hello Javascript!
// Hello Golang!
// Hello from sync Promise
// Hello Async Function!
}
// 注意:
// - 线程归属由调用方负责;如果一个 Runtime 必须固定在同一个 OS 线程,请由调用方自行在 owner goroutine 中调用 runtime.LockOSThread()。
// - WithOwnerGoroutineCheck(false) 是不安全开关,只应在你能保证外部串行化 QuickJS 访问时使用。
// - 不要在 goroutine 中直接调用 Context 或任何 QuickJS API。
// - 所有 QuickJS 相关操作都应该通过 ctx.Schedule 调度回 Context 线程后再执行。
// - ctx.Await 会在内部驱动 pending jobs 和调度器,直至 Promise 解决。
package main
import (
"fmt"
"errors"
"github.com/buke/quickjs-go"
)
func main() {
// 创建新的运行时
rt := quickjs.NewRuntime()
defer rt.Close()
// 创建新的上下文
ctx := rt.NewContext()
defer ctx.Close()
ctx.Globals().SetFunction("A", func(ctx *quickjs.Context, this *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
// 抛出错误
return ctx.ThrowError(errors.New("expected error"))
})
result := ctx.Eval("A()")
defer result.Free()
if result.IsException() {
actual := ctx.Exception()
fmt.Println(actual.Error())
}
}
QuickJS-Go 提供对 JavaScript TypedArray 的支持,实现 Go 和 JavaScript 之间的二进制数据处理。
```go package main
import ( "fmt" "github.com/buke/quickjs-go" )
func main() { rt := quickjs.NewRuntime() defer rt.Close() ctx := rt.NewContext() defer ctx.Close()
// 从 Go 切片创建各种 TypedArray
int8Data := []int8{-128, -1, 0, 1, 127}
int8Array := ctx.NewInt8Array(int8Data)
uint8Data := []uint8{0, 128, 255}
uint8Array := ctx.NewUint8Array(uint8Data)
float32Data := []float32{-3.14, 0.0, 2.718, 100.5}
float32Array := ctx.NewFloat32Array(float32Data)
int64Data := []int64{-9223372036854775808, 0, 9223372036854775807}
bigInt64Array := ctx.NewBigInt64Array(int64Data)
// 将 TypedArray 设置为全局变量
ctx.Globals().Set("int8Array", int8Array)
ctx.Globals().Set("uint8Array", uint8Array)
ctx.Globals().Set("float32Array", float32Array)
ctx.Globals().Set("bigInt64Array", bigInt64Array)
// 在 JavaScript 中使用
result := ctx.Eval(`
// 检查类型
const results = {
int8Type: int8Array instanceof Int8Array,
uint8Type: uint8Array instanceof Uint8Arr
$ claude mcp add quickjs-go \
-- python -m otcore.mcp_server <graph>