package jscriptx import ( "context" "github.com/dop251/goja" ) // Caller 是在 VM 借出期间对脚本函数的操作入口。 // // 它存在的理由是:有些调用模式没法用 Call/CallInto 表达——典型的是「回调 + next」, // 需要先看脚本函数声明了几个形参,再决定怎么调它、把哪个 Go 闭包传进去。这些都得在 // 同一次 VM 借出期间完成,因为传给脚本的 Go 闭包只在那段时间里有效。 // // 拿它写一个自定义的调用约定: // // err := target.WithCall(ctx, "handle", func(c jscriptx.Caller) error { // if c.Arity() == 2 { // res, err := c.Call(model, next) // next 是 Go 闭包,脚本能直接调 // … // } // return nil // }) // // 框架自己的回调约定(比如 orm 那套三种签名)就是这么实现的,见 jscriptx/dispatch。 // // Caller 只在 do 回调执行期间有效,别存下来跨调用用。 type Caller interface { // Arity 返回脚本函数声明的形参个数(JS 函数的 length 属性)。 // 靠它判断脚本写的是哪种形状,脚本就不用额外声明签名。 Arity() int // Call 调用脚本函数。参数按 goja 的规则转换:Go 对象反射包装成脚本对象, // Go 函数变成脚本能直接调的函数——「把 next 传给脚本」就是这么实现的。 Call(args ...any) (Result, error) // Script 返回脚本名,拼错误信息时用得上。 Script() string } // Result 是脚本函数一次调用的返回值。 // // 它只在下一次 Call 之前有效——同一个 Caller 的多次调用复用同一个对象, // 要留着以后用就先 Value() 或 Into() 取出来。 type Result interface { // IsEmpty 判断脚本有没有返回东西(undefined 或 null)。 // 「1 个形参、没有返回值」这种纯副作用的写法靠它识别。 IsEmpty() bool // Value 返回导出成 Go 值的结果。空返回值时是 nil。 Value() any // Into 把返回值转换进 out 指向的变量(out 必须是非 nil 指针), // 目标是接口时要求返回值实现它。 Into(out any) error } // WithCall 借一个 VM,在借出期间把控制权交给 do。 // // 超时中断、panic 恢复、错误分类、VM 归还这些都跟普通调用一样,do 里只管发起调用。 // do 返回的错误会被包成 *Error;想让调用方 errors.Is 得到,wrap 一个哨兵错误进去, // 比如 ErrUnsupportedSignature。 func (s *Script) WithCall(ctx context.Context, fn string, do func(Caller) error) error { return withCaller(ctx, s, fn, do) } // WithCall 同 Script.WithCall,只是在这个实例独占的 VM 上执行。 func (i *Instance) WithCall(ctx context.Context, fn string, do func(Caller) error) error { return withCaller(ctx, i, fn, do) } func withCaller(ctx context.Context, r runner, fn string, do func(Caller) error) error { return invoke(ctx, r, fn, nil, func(f *frame) error { return do(&caller{f: f}) }) } type caller struct { f *frame // 复用同一个 result:一次 WithCall 里可能调好几次脚本函数, // 每次都分配一个返回值对象不划算。所以 Result 只在下一次 Call 之前有效。 res result } func (c *caller) Arity() int { return int(c.f.arity()) } func (c *caller) Script() string { return c.f.script.name } func (c *caller) Call(args ...any) (Result, error) { v, err := c.f.callWith(args) if err != nil { return nil, err } c.res = result{f: c.f, v: v} return &c.res, nil } type result struct { f *frame v goja.Value } func (r *result) IsEmpty() bool { return empty(r.v) } func (r *result) Value() any { if empty(r.v) { return nil } return r.v.Export() } func (r *result) Into(out any) error { if empty(r.v) { return nil } if err := r.f.export(r.v, out); err != nil { return newError(KindType, r.f.script.name, r.f.name, err, "返回值无法转换成 %T(拿到的是 %T)", out, r.v.Export()) } return nil } // Target 是能发起脚本调用的对象:*Script(从 VM 池借用,脚本无跨调用状态) // 或 *Instance(独占一个 VM,脚本里的状态跨调用保持)。 // // 这个接口不对外开放实现,只是让 dispatch 这类函数能同时接受两者。 type Target interface { // Name 返回脚本名。 Name() string // Has 判断脚本里有没有这个函数。 Has(fn string) bool // Call 调用脚本函数,返回值导出成 Go 值。 Call(ctx context.Context, fn string, args ...any) (any, error) // CallInto 调用脚本函数,并把返回值转换进 out 指向的变量。 CallInto(ctx context.Context, fn string, out any, args ...any) error // WithCall 借一个 VM,在借出期间把控制权交给 do,用来实现自定义的调用约定。 WithCall(ctx context.Context, fn string, do func(Caller) error) error // 未导出方法,接口不对外开放实现。 owner() *Script }