package jscriptx import ( "context" "fmt" "reflect" "runtime/debug" "sync" "time" "github.com/dop251/goja" ) // frame 是一次调用的上下文:拿到了 VM、目标函数,可以真正发起调用。 // 它只在 withCall 的回调里有效,不能存下来跨调用使用。 type frame struct { script *Script rt *goja.Runtime fn goja.Callable fnVal goja.Value this goja.Value // 方法调用时绑定的 this(导出实例),默认导出函数为 undefined name string } // arity 返回 JS 函数声明的形参个数(函数对象的 length 属性)。 // WithCall 的回调靠它判断脚本写的是哪种形状,见 Caller.Arity。 func (f *frame) arity() int64 { return f.fnVal.ToObject(f.rt).Get("length").ToInteger() } // call 调用目标函数,this 按 lookup 的结果绑定。 func (f *frame) call(args ...goja.Value) (goja.Value, error) { return f.fn(f.this, args...) } // callWith 用 Go 值直接调用目标函数。 func (f *frame) callWith(args []any) (goja.Value, error) { jsArgs := make([]goja.Value, len(args)) for i, a := range args { jsArgs[i] = f.rt.ToValue(a) } return f.call(jsArgs...) } // export 把脚本返回值转成 Go 值,写进 target 指向的变量。 func (f *frame) export(v goja.Value, target any) error { return f.rt.ExportTo(v, target) } // empty 判断脚本返回值是不是"什么都没返回"。 func empty(v goja.Value) bool { return v == nil || goja.IsUndefined(v) || goja.IsNull(v) } // Call 调用脚本里的函数,返回值导出成 Go 值(JS 对象变 map[string]any,数组变 []any, // 传进去的 Go 对象原样回来)。fn 传 DefaultFunc 表示调用脚本自身求值出的那个函数。 // // 脚本返回函数/闭包会被拒绝:那种值只在 VM 内部有效,VM 归还池子后再调用会出问题。 // 需要把脚本函数当回调用,走 WithCall——在 VM 借出期间调,别把函数带出来。 func (s *Script) Call(ctx context.Context, fn string, args ...any) (any, error) { return callAny(ctx, s, fn, args) } // CallInto 调用脚本里的函数,并把返回值转换进 out 指向的变量(out 必须是非 nil 指针), // 相当于带类型的 Call:目标是 int 就按 int 转,是某个接口就要求返回值实现它。 func (s *Script) CallInto(ctx context.Context, fn string, out any, args ...any) error { return callInto(ctx, s, fn, out, args) } // callAny 是 Call 的共用实现,*Script 和 *Session 都走这里。 func callAny(ctx context.Context, r runner, fn string, args []any) (any, error) { var out any err := invoke(ctx, r, fn, args, func(f *frame) error { res, err := f.callWith(args) if err != nil { return err } res, err = unwrapPromise(f, res, r.owner().name, fn) if err != nil { return err } if empty(res) { return nil } if _, isFunc := goja.AssertFunction(res); isFunc { return newError(KindType, r.owner().name, fn, ErrValueEscape, "返回值是 JS 函数,只在脚本内部有效;需要回调语义请用 WithCall") } out = res.Export() return nil }) return out, err } // callInto 是 CallInto 的共用实现。 func callInto(ctx context.Context, r runner, fn string, out any, args []any) error { name := r.owner().name rv := reflect.ValueOf(out) if !rv.IsValid() || rv.Kind() != reflect.Pointer || rv.IsNil() { return newError(KindType, name, fn, nil, "CallInto 的 out 必须是非 nil 指针,当前是 %T", out) } // 导出成 Go 函数意味着把一个绑定在 VM 上的闭包带出脚本边界, // 而这个 VM 马上就要还回池子给别的请求用了。 if rv.Type().Elem().Kind() == reflect.Func { return newError(KindType, name, fn, ErrValueEscape, "不能把脚本函数导出成 Go 函数(VM 归还池子后它就失效了);需要回调语义请用 WithCall") } return invoke(ctx, r, fn, args, func(f *frame) error { res, err := f.callWith(args) if err != nil { return err } res, err = unwrapPromise(f, res, name, fn) if err != nil { return err } if empty(res) { return nil } if err := f.export(res, out); err != nil { return newError(KindType, name, fn, err, "返回值无法转换成 %T", out) } return nil }) } // runner 抽象"这次调用用哪个 VM、用完怎么处理":*Script 从池里借还, // *Session 独占一个 VM(acquire 会一直持锁到 finish,保证同一会话串行执行)。 type runner interface { owner() *Script acquire(ctx context.Context) (*vmHandle, error) finish(inst *vmHandle, healthy bool) } func (s *Script) owner() *Script { return s } func (s *Script) acquire(ctx context.Context) (*vmHandle, error) { return s.borrow(ctx) } func (s *Script) finish(inst *vmHandle, healthy bool) { s.release(inst, healthy) } // invoke 是所有脚本调用的骨架:取 VM → 找函数 → 装超时哨兵 → 兜 panic → 分类错误 → 交还 VM。 // do 里只管发起调用和处理返回值,异常处理交给这里。 func invoke(ctx context.Context, r runner, fn string, args []any, do func(*frame) error) (err error) { s := r.owner() if s.closed.Load() { return newError(KindClosed, s.name, fn, ErrClosed, "脚本已关闭") } if ctx == nil { ctx = context.Background() } inst, err := r.acquire(ctx) if err != nil { return err } healthy := true // defer 是后进先出:recover 最先跑(把 healthy 置回 false), // 然后 stop 清掉可能迟到的中断标志,最后才交还 VM。 defer func() { r.finish(inst, healthy) }() callable, fnVal, this, ok := inst.lookup(fn) if !ok { return newError(KindNotFound, s.name, fn, ErrFuncNotFound, "%s", s.notFoundHint(inst)) } stop := guard(ctx, inst.rt, s.engine.timeout) defer stop() defer func() { if r := recover(); r != nil { healthy = false err = s.panicError(fn, args, r) } }() f := &frame{script: s, rt: inst.rt, fn: callable, fnVal: fnVal, this: this, name: fn} if e := do(f); e != nil { err = classify(e, s.name, fn, args) healthy = !fatal(err) return err } return nil } // panicError 把 recover 到的 panic 包成带现场的错误。脚本里的类型错误、 // 注入进去的 Go 方法内部 panic,都会走到这里,不会掀翻调用方的 goroutine。 func (s *Script) panicError(fn string, args []any, r any) *Error { return &Error{ Kind: KindPanic, Script: s.name, Func: fn, Msg: fmt.Sprintf("脚本执行过程中发生 panic: %v", r), Args: summarize(args), GoStack: string(debug.Stack()), Cause: toError(r), } } // interruptGuard 保证"中断"和"收尾"两件事不会打架:收尾之后再迟到的中断信号必须被丢掉, // 否则它会毒死下一次用到这个 VM 的调用。 type interruptGuard struct { mu sync.Mutex stopped bool rt *goja.Runtime } func (g *interruptGuard) fire(reason error) { g.mu.Lock() defer g.mu.Unlock() if g.stopped { return } g.rt.Interrupt(reason) } // stop 关掉哨兵并清掉可能已经发出的中断标志。返回后保证不会再有新的中断落到这个 VM 上。 func (g *interruptGuard) stop() { g.mu.Lock() g.stopped = true g.mu.Unlock() // 到这里要么 fire 已经跑完(中断标志由下面清掉),要么它以后永远不会再发。 g.rt.ClearInterrupt() } // guard 装一个中断哨兵:超时或者调用方 context 取消时,从另一个 goroutine 调 // Interrupt 打断正在执行的脚本(死循环也能断掉)。返回的函数负责收尾。 func guard(ctx context.Context, rt *goja.Runtime, timeout time.Duration) func() { watchCtx := ctx.Done() != nil if timeout <= 0 && !watchCtx { return func() {} } g := &interruptGuard{rt: rt} if !watchCtx { // 快路径:没有 context 取消要盯,一个定时器就够了,不用为每次调用起 goroutine。 // MQTT 消息级这种高频调用走的就是这条路。 timer := time.AfterFunc(timeout, func() { g.fire(context.DeadlineExceeded) }) return func() { g.stop() timer.Stop() } } var cancel context.CancelFunc if timeout > 0 { ctx, cancel = context.WithTimeout(ctx, timeout) } done := make(chan struct{}) exited := make(chan struct{}) go func() { defer close(exited) select { case <-ctx.Done(): g.fire(ctx.Err()) case <-done: } }() return func() { close(done) <-exited if cancel != nil { cancel() } g.stop() } } // unwrapPromise 把 async 方法返回的 Promise 解开。 // // goja 在调用返回前会把 microtask 队列跑干净,所以只要脚本里没有真正需要等待外部 // 事件的操作(goja 没有事件循环,也没有 setTimeout),Promise 到这里已经是完成态了。 // 解开它有两个理由:一是 *goja.Promise 属于引擎类型,不该出现在本库的返回值里; // 二是调用方拿到一个未解包的 Promise 什么也做不了。 func unwrapPromise(f *frame, v goja.Value, script, fn string) (goja.Value, error) { p, ok := promiseOf(v) if !ok { return v, nil } switch p.State() { case goja.PromiseStateFulfilled: return p.Result(), nil case goja.PromiseStateRejected: reason := p.Result() e := &Error{ Kind: KindRuntime, Script: script, Func: fn, Msg: "async 方法的 Promise 被 reject", Cause: ErrPromiseRejected, } if reason != nil { e.Msg = reason.String() e.Value = reason.Export() } return nil, e default: return nil, newError(KindRuntime, script, fn, ErrPromisePending, "async 方法返回的 Promise 一直没完成。goja 没有事件循环,"+ "脚本里没法等待真正的异步操作(定时器、网络、IO);"+ "异步的活交给 Go 侧做,脚本只写同步逻辑") } } // promiseOf 判断一个返回值是不是 Promise。 // // *goja.Promise 只能从 Export() 拿到,但对普通对象调 Export 会把整个对象转成 map, // 那个开销不能加在每次调用上。先看有没有 then 属性——一次廉价的属性查找就能排除掉 // 绝大多数返回值,只有 thenable 才走到 Export。 func promiseOf(v goja.Value) (*goja.Promise, bool) { obj, ok := v.(*goja.Object) if !ok { return nil, false } if then := obj.Get("then"); then == nil || goja.IsUndefined(then) { return nil, false } p, ok := obj.Export().(*goja.Promise) return p, ok }