From eddbe9a2b5d18974af0653df923b0058793d75b0 Mon Sep 17 00:00:00 2001 From: Roger Chapman Date: Tue, 2 Feb 2021 15:09:58 +1100 Subject: [PATCH] Function Templates with callback functions in Go (#68) * refactor to extend from private template struct * refactor the C++ side to also match base class template * Basic callbacks with arguments * fix stat now there is an internal context * deal with Go -> C pointer madness * apply formatting and add examples * add tests to the function template and the registries * simplify, bug fixes and add comments --- CHANGELOG.md | 4 +- context.go | 65 +++++++++- context_test.go | 27 ++++ export_test.go | 29 +++++ function_template.go | 78 ++++++++++++ function_template_test.go | 65 ++++++++++ isolate.go | 46 +++++-- isolate_test.go | 24 +++- object_template.go | 53 +------- template.go | 59 +++++++++ v8go.cc | 257 +++++++++++++++++++++++++------------- v8go.h | 33 +++-- value.go | 4 +- value_test.go | 2 - 14 files changed, 585 insertions(+), 161 deletions(-) create mode 100644 export_test.go create mode 100644 function_template.go create mode 100644 function_template_test.go create mode 100644 template.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee01b2c..b8730320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for the BigInt value to the big.Int Go type - Create Object Templates with primitive values, including other Object Templates - Configure Object Template as the global object of any new Context +- Function Templates with callbacks to Go ### Changed - NewContext() API has been improved to handle optional global object, as well as optional Isolate - Package error messages are now prefixed with `v8go` rather than the struct name - -### Changed +- Deprecated `iso.Close()` in favor of `iso.Dispose()` to keep consistancy with the C++ API - Upgraded V8 to 8.8.278.14 ## [v0.4.0] - 2021-01-14 diff --git a/context.go b/context.go index 94b4afaf..fb8f4336 100644 --- a/context.go +++ b/context.go @@ -6,12 +6,28 @@ import "C" import ( "fmt" "runtime" + "sync" "unsafe" ) +// Due to the limitations of passing pointers to C from Go we need to create +// a registry so that we can lookup the Context from any given callback from V8. +// This is similar to what is described here: https://github.com/golang/go/wiki/cgo#function-variables +// To make sure we can still GC *Context we register the context only when we are +// running a script inside the context and then deregister. +type ctxRef struct { + ctx *Context + refCount int +} + +var ctxMutex sync.RWMutex +var ctxRegistry = make(map[int]*ctxRef) +var ctxSeq = 0 + // Context is a global root execution environment that allows separate, // unrelated, JavaScript applications to run in a single instance of V8. type Context struct { + ref int ptr C.ContextPtr iso *Isolate } @@ -45,12 +61,18 @@ func NewContext(opt ...ContextOption) (*Context, error) { } if opts.gTmpl == nil { - opts.gTmpl = &ObjectTemplate{} + opts.gTmpl = &ObjectTemplate{&template{}} } + ctxMutex.Lock() + ctxSeq++ + ref := ctxSeq + ctxMutex.Unlock() + ctx := &Context{ + ref: ref, + ptr: C.NewContext(opts.iso.ptr, opts.gTmpl.ptr, C.int(ref)), iso: opts.iso, - ptr: C.NewContext(opts.iso.ptr, opts.gTmpl.ptr), } runtime.SetFinalizer(ctx, (*Context).finalizer) // TODO: [RC] catch any C++ exceptions and return as error @@ -73,7 +95,10 @@ func (c *Context) RunScript(source string, origin string) (*Value, error) { defer C.free(unsafe.Pointer(cSource)) defer C.free(unsafe.Pointer(cOrigin)) + c.register() rtn := C.RunScript(c.ptr, cSource, cOrigin) + c.deregister() + return getValue(c, rtn), getError(rtn) } @@ -83,11 +108,45 @@ func (c *Context) Close() { } func (c *Context) finalizer() { - C.ContextDispose(c.ptr) + C.ContextFree(c.ptr) c.ptr = nil runtime.SetFinalizer(c, nil) } +func (c *Context) register() { + ctxMutex.Lock() + r := ctxRegistry[c.ref] + if r == nil { + r = &ctxRef{ctx: c} + ctxRegistry[c.ref] = r + } + r.refCount++ + ctxMutex.Unlock() +} + +func (c *Context) deregister() { + ctxMutex.Lock() + defer ctxMutex.Unlock() + r := ctxRegistry[c.ref] + if r == nil { + return + } + r.refCount-- + if r.refCount <= 0 { + delete(ctxRegistry, c.ref) + } +} + +func getContext(ref int) *Context { + ctxMutex.RLock() + defer ctxMutex.RUnlock() + r := ctxRegistry[ref] + if r == nil { + return nil + } + return r.ctx +} + func getValue(ctx *Context, rtn C.RtnValue) *Value { if rtn.value == nil { return nil diff --git a/context_test.go b/context_test.go index 07efe26c..d3d8649e 100644 --- a/context_test.go +++ b/context_test.go @@ -61,6 +61,33 @@ func TestJSExceptions(t *testing.T) { } } +func TestContextRegistry(t *testing.T) { + t.Parallel() + + ctx, _ := v8go.NewContext() + ctxref := ctx.Ref() + + c1 := v8go.GetContext(ctxref) + if c1 != nil { + t.Error("expected context to be ") + } + + ctx.Register() + c2 := v8go.GetContext(ctxref) + if c2 == nil { + t.Error("expected context, but got ") + } + if c2 != ctx { + t.Errorf("contexts should match %p != %p", c2, ctx) + } + ctx.Deregister() + + c3 := v8go.GetContext(ctxref) + if c3 != nil { + t.Error("expected context to be ") + } +} + func BenchmarkContext(b *testing.B) { b.ReportAllocs() vm, _ := v8go.NewIsolate() diff --git a/export_test.go b/export_test.go new file mode 100644 index 00000000..2f0cb324 --- /dev/null +++ b/export_test.go @@ -0,0 +1,29 @@ +package v8go + +// RegisterCallback is exported for testing only. +func (i *Isolate) RegisterCallback(cb FunctionCallback) int { + return i.registerCallback(cb) +} + +// GetCallback is exported for testing only. +func (i *Isolate) GetCallback(ref int) FunctionCallback { + return i.getCallback(ref) +} + +// Register is exported for testing only. +func (c *Context) Register() { + c.register() +} + +// Deregister is exported for testing only. +func (c *Context) Deregister() { + c.deregister() +} + +// GetContext is exported for testing only. +var GetContext = getContext + +// Ref is exported for testing only. +func (c *Context) Ref() int { + return c.ref +} diff --git a/function_template.go b/function_template.go new file mode 100644 index 00000000..443bd3f1 --- /dev/null +++ b/function_template.go @@ -0,0 +1,78 @@ +package v8go + +// #include +// #include "v8go.h" +import "C" +import ( + "errors" + "runtime" + "unsafe" +) + +// FunctionCallback is a callback that is executed in Go when a function is executed in JS. +type FunctionCallback func(info *FunctionCallbackInfo) *Value + +// FunctionCallbackInfo is the argument that is passed to a FunctionCallback. +type FunctionCallbackInfo struct { + ctx *Context + args []*Value +} + +// Context is the current context that the callback is being executed in. +func (i *FunctionCallbackInfo) Context() *Context { + return i.ctx +} + +// Args returns a slice of the value arguments that are passed to the JS function. +func (i *FunctionCallbackInfo) Args() []*Value { + return i.args +} + +// FunctionTemplate is used to create functions at runtime. +// There can only be one function created from a FunctionTemplate in a context. +// The lifetime of the created function is equal to the lifetime of the context. +type FunctionTemplate struct { + *template +} + +// NewFunctionTemplate creates a FunctionTemplate for a given callback. +func NewFunctionTemplate(iso *Isolate, callback FunctionCallback) (*FunctionTemplate, error) { + if iso == nil { + return nil, errors.New("v8go: failed to create new FunctionTemplate: Isolate cannot be ") + } + if callback == nil { + return nil, errors.New("v8go: failed to create new FunctionTemplate: FunctionCallback cannot be ") + } + + cbref := iso.registerCallback(callback) + + tmpl := &template{ + ptr: C.NewFunctionTemplate(iso.ptr, C.int(cbref)), + iso: iso, + } + runtime.SetFinalizer(tmpl, (*template).finalizer) + return &FunctionTemplate{tmpl}, nil +} + +//export goFunctionCallback +func goFunctionCallback(ctxref int, cbref int, args *C.ValuePtr, argsCount int) C.ValuePtr { + ctx := getContext(ctxref) + + info := &FunctionCallbackInfo{ + ctx: ctx, + args: make([]*Value, argsCount), + } + + argv := (*[1 << 30]C.ValuePtr)(unsafe.Pointer(args))[:argsCount:argsCount] + for i, v := range argv { + val := &Value{ptr: v} + runtime.SetFinalizer(val, (*Value).finalizer) + info.args[i] = val + } + + callbackFunc := ctx.iso.getCallback(cbref) + if val := callbackFunc(info); val != nil { + return val.ptr + } + return nil +} diff --git a/function_template_test.go b/function_template_test.go new file mode 100644 index 00000000..a061a37c --- /dev/null +++ b/function_template_test.go @@ -0,0 +1,65 @@ +package v8go_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "rogchap.com/v8go" +) + +func TestFunctionTemplate(t *testing.T) { + t.Parallel() + + if _, err := v8go.NewFunctionTemplate(nil, func(*v8go.FunctionCallbackInfo) *v8go.Value { return nil }); err == nil { + t.Error("expected error but got ") + } + + iso, _ := v8go.NewIsolate() + if _, err := v8go.NewFunctionTemplate(iso, nil); err == nil { + t.Error("expected error but got ") + } + + fn, err := v8go.NewFunctionTemplate(iso, func(*v8go.FunctionCallbackInfo) *v8go.Value { return nil }) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if fn == nil { + t.Error("expected FunctionTemplate, but got ") + } +} + +func ExampleFunctionTemplate() { + iso, _ := v8go.NewIsolate() + global, _ := v8go.NewObjectTemplate(iso) + printfn, _ := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + fmt.Printf("%+v\n", info.Args()) + return nil + }) + global.Set("print", printfn, v8go.ReadOnly) + ctx, _ := v8go.NewContext(iso, global) + ctx.RunScript("print('foo', 'bar', 0, 1)", "") + // Output: + // [foo bar 0 1] +} + +func ExampleFunctionTemplate_fetch() { + iso, _ := v8go.NewIsolate() + global, _ := v8go.NewObjectTemplate(iso) + fetchfn, _ := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + args := info.Args() + url := args[0].String() + res, _ := http.Get(url) + body, _ := ioutil.ReadAll(res.Body) + val, _ := v8go.NewValue(iso, string(body)) + return val + }) + global.Set("fetch", fetchfn, v8go.ReadOnly) + ctx, _ := v8go.NewContext(iso, global) + val, _ := ctx.RunScript("fetch('https://rogchap.com/v8go')", "") + fmt.Printf("%s\n", strings.Split(val.String(), "\n")[0]) + // Output: + // +} diff --git a/isolate.go b/isolate.go index acc13ac0..c5cf1416 100644 --- a/isolate.go +++ b/isolate.go @@ -15,6 +15,10 @@ var v8once sync.Once // with many V8 contexts for execution. type Isolate struct { ptr C.IsolatePtr + + cbMutex sync.RWMutex + cbSeq int + cbs map[int]FunctionCallback } // HeapStatistics represents V8 isolate heap statistics @@ -35,13 +39,18 @@ type HeapStatistics struct { // NewIsolate creates a new V8 isolate. Only one thread may access // a given isolate at a time, but different threads may access // different isolates simultaneously. +// When an isolate is no longer used its resources should be freed +// by calling iso.Dispose(). // An *Isolate can be used as a v8go.ContextOption to create a new // Context, rather than creating a new default Isolate. func NewIsolate() (*Isolate, error) { v8once.Do(func() { C.Init() }) - iso := &Isolate{C.NewIsolate()} + iso := &Isolate{ + ptr: C.NewIsolate(), + cbs: make(map[int]FunctionCallback), + } runtime.SetFinalizer(iso, (*Isolate).finalizer) // TODO: [RC] catch any C++ exceptions and return as error return iso, nil @@ -72,17 +81,40 @@ func (i *Isolate) GetHeapStatistics() HeapStatistics { } } -func (i *Isolate) finalizer() { - C.IsolateDispose(i.ptr) - i.ptr = nil - runtime.SetFinalizer(i, nil) +// Dispose will dispose the Isolate VM; subsequent calls will panic. +func (i *Isolate) Dispose() { + i.finalizer() } -// Close will dispose the Isolate VM; subsequent calls will panic +// Deprecated: use `iso.Dispose()`. func (i *Isolate) Close() { - i.finalizer() + i.Dispose() +} + +func (i *Isolate) finalizer() { + defer runtime.SetFinalizer(i, nil) + if i.ptr == nil { + return + } + C.IsolateDispose(i.ptr) + i.ptr = nil } func (i *Isolate) apply(opts *contextOptions) { opts.iso = i } + +func (i *Isolate) registerCallback(cb FunctionCallback) int { + i.cbMutex.Lock() + i.cbSeq++ + ref := i.cbSeq + i.cbs[ref] = cb + i.cbMutex.Unlock() + return ref +} + +func (i *Isolate) getCallback(ref int) FunctionCallback { + i.cbMutex.RLock() + defer i.cbMutex.RUnlock() + return i.cbs[ref] +} diff --git a/isolate_test.go b/isolate_test.go index e85ce9ae..af16783a 100644 --- a/isolate_test.go +++ b/isolate_test.go @@ -43,8 +43,8 @@ func TestGetHeapStatistics(t *testing.T) { hs := iso.GetHeapStatistics() - if hs.NumberOfNativeContexts != 2 { - t.Error("expect NumberOfNativeContexts return 2, got", hs.NumberOfNativeContexts) + if hs.NumberOfNativeContexts != 3 { + t.Error("expect NumberOfNativeContexts return 3, got", hs.NumberOfNativeContexts) } if hs.NumberOfDetachedContexts != 0 { @@ -52,6 +52,26 @@ func TestGetHeapStatistics(t *testing.T) { } } +func TestCallbackRegistry(t *testing.T) { + t.Parallel() + + iso, _ := v8go.NewIsolate() + cb := func(*v8go.FunctionCallbackInfo) *v8go.Value { return nil } + + cb0 := iso.GetCallback(0) + if cb0 != nil { + t.Error("expected callback function to be ") + } + ref1 := iso.RegisterCallback(cb) + if ref1 != 1 { + t.Errorf("expected callback ref == 1, got %d", ref1) + } + cb1 := iso.GetCallback(1) + if fmt.Sprintf("%p", cb1) != fmt.Sprintf("%p", cb) { + t.Errorf("unexpected callback function; want %p, got %p", cb, cb1) + } +} + func BenchmarkIsolateInitialization(b *testing.B) { b.ReportAllocs() for n := 0; n < b.N; n++ { diff --git a/object_template.go b/object_template.go index 3006ae9e..314bf742 100644 --- a/object_template.go +++ b/object_template.go @@ -5,10 +5,7 @@ package v8go import "C" import ( "errors" - "fmt" - "math/big" "runtime" - "unsafe" ) // PropertyAttribute are the attribute flags for a property on an Object. @@ -30,8 +27,7 @@ const ( // ObjectTemplate is used to create objects at runtime. // Properties added to an ObjectTemplate are added to each object created from the ObjectTemplate. type ObjectTemplate struct { - ptr C.ObjectTemplatePtr - iso *Isolate + *template } // NewObjectTemplate creates a new ObjectTemplate. @@ -40,54 +36,15 @@ func NewObjectTemplate(iso *Isolate) (*ObjectTemplate, error) { if iso == nil { return nil, errors.New("v8go: failed to create new ObjectTemplate: Isolate cannot be ") } - ob := &ObjectTemplate{ + + tmpl := &template{ ptr: C.NewObjectTemplate(iso.ptr), iso: iso, } - runtime.SetFinalizer(ob, (*ObjectTemplate).finalizer) - return ob, nil -} - -// Set adds a property to each instance created by this template. -// The property must be defined either as a primitive value, or a template. -// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int) -// then a value will be created and set as the value property. -func (o *ObjectTemplate) Set(name string, val interface{}, attributes ...PropertyAttribute) error { - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - - var attrs PropertyAttribute - for _, a := range attributes { - attrs |= a - } - - switch v := val.(type) { - case string, int32, uint32, int64, uint64, float64, bool, *big.Int: - newVal, err := NewValue(o.iso, v) - if err != nil { - return fmt.Errorf("v8go: unable to create new value: %v", err) - } - C.ObjectTemplateSetValue(o.ptr, cname, newVal.ptr, C.int(attrs)) - case *ObjectTemplate: - C.ObjectTemplateSetObjectTemplate(o.ptr, cname, v.ptr, C.int(attrs)) - case *Value: - if v.IsObject() || v.IsExternal() { - return errors.New("v8go: unsupported property: value type must be a primitive or use a template") - } - C.ObjectTemplateSetValue(o.ptr, cname, v.ptr, C.int(attrs)) - default: - return fmt.Errorf("v8go: unsupported property type `%T`, must be one of string, int32, uint32, int64, uint64, float64, *big.Int, *v8go.Value or *v8go.ObjectTemplate", v) - } - - return nil + runtime.SetFinalizer(tmpl, (*template).finalizer) + return &ObjectTemplate{tmpl}, nil } func (o *ObjectTemplate) apply(opts *contextOptions) { opts.gTmpl = o } - -func (o *ObjectTemplate) finalizer() { - C.ObjectTemplateDispose(o.ptr) - o.ptr = nil - runtime.SetFinalizer(o, nil) -} diff --git a/template.go b/template.go new file mode 100644 index 00000000..c1cdd8f3 --- /dev/null +++ b/template.go @@ -0,0 +1,59 @@ +package v8go + +// #include +// #include "v8go.h" +import "C" +import ( + "errors" + "fmt" + "math/big" + "runtime" + "unsafe" +) + +type template struct { + ptr C.TemplatePtr + iso *Isolate +} + +// Set adds a property to each instance created by this template. +// The property must be defined either as a primitive value, or a template. +// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int) +// then a value will be created and set as the value property. +func (t *template) Set(name string, val interface{}, attributes ...PropertyAttribute) error { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + + var attrs PropertyAttribute + for _, a := range attributes { + attrs |= a + } + + switch v := val.(type) { + case string, int32, uint32, int64, uint64, float64, bool, *big.Int: + newVal, err := NewValue(t.iso, v) + if err != nil { + return fmt.Errorf("v8go: unable to create new value: %v", err) + } + C.TemplateSetValue(t.ptr, cname, newVal.ptr, C.int(attrs)) + case *ObjectTemplate: + C.TemplateSetTemplate(t.ptr, cname, v.ptr, C.int(attrs)) + case *FunctionTemplate: + C.TemplateSetTemplate(t.ptr, cname, v.ptr, C.int(attrs)) + case *Value: + if v.IsObject() || v.IsExternal() { + return errors.New("v8go: unsupported property: value type must be a primitive or use a template") + } + C.TemplateSetValue(t.ptr, cname, v.ptr, C.int(attrs)) + default: + return fmt.Errorf("v8go: unsupported property type `%T`, must be one of string, int32, uint32, int64, uint64, float64, *big.Int, *v8go.Value, *v8go.ObjectTemplate or *v8go.FunctionTemplate", v) + } + + return nil +} + +func (t *template) finalizer() { + C.TemplateFree(t.ptr) + t.ptr = nil + runtime.SetFinalizer(t, nil) +} diff --git a/v8go.cc b/v8go.cc index eef49001..5e3c16cd 100644 --- a/v8go.cc +++ b/v8go.cc @@ -23,14 +23,14 @@ typedef struct { typedef struct { Persistent ptr; + Persistent ctx; Isolate* iso; - m_ctx* ctx_ptr; } m_value; typedef struct { - Persistent ptr; + Persistent