Skip to content

Commit eddbe9a

Browse files
authored
Function Templates with callback functions in Go (rogchap#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
1 parent b35f871 commit eddbe9a

14 files changed

+585
-161
lines changed

CHANGELOG.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Support for the BigInt value to the big.Int Go type
1111
- Create Object Templates with primitive values, including other Object Templates
1212
- Configure Object Template as the global object of any new Context
13+
- Function Templates with callbacks to Go
1314

1415
### Changed
1516
- NewContext() API has been improved to handle optional global object, as well as optional Isolate
1617
- Package error messages are now prefixed with `v8go` rather than the struct name
17-
18-
### Changed
18+
- Deprecated `iso.Close()` in favor of `iso.Dispose()` to keep consistancy with the C++ API
1919
- Upgraded V8 to 8.8.278.14
2020

2121
## [v0.4.0] - 2021-01-14

context.go

+62-3
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,28 @@ import "C"
66
import (
77
"fmt"
88
"runtime"
9+
"sync"
910
"unsafe"
1011
)
1112

13+
// Due to the limitations of passing pointers to C from Go we need to create
14+
// a registry so that we can lookup the Context from any given callback from V8.
15+
// This is similar to what is described here: https://github.com/golang/go/wiki/cgo#function-variables
16+
// To make sure we can still GC *Context we register the context only when we are
17+
// running a script inside the context and then deregister.
18+
type ctxRef struct {
19+
ctx *Context
20+
refCount int
21+
}
22+
23+
var ctxMutex sync.RWMutex
24+
var ctxRegistry = make(map[int]*ctxRef)
25+
var ctxSeq = 0
26+
1227
// Context is a global root execution environment that allows separate,
1328
// unrelated, JavaScript applications to run in a single instance of V8.
1429
type Context struct {
30+
ref int
1531
ptr C.ContextPtr
1632
iso *Isolate
1733
}
@@ -45,12 +61,18 @@ func NewContext(opt ...ContextOption) (*Context, error) {
4561
}
4662

4763
if opts.gTmpl == nil {
48-
opts.gTmpl = &ObjectTemplate{}
64+
opts.gTmpl = &ObjectTemplate{&template{}}
4965
}
5066

67+
ctxMutex.Lock()
68+
ctxSeq++
69+
ref := ctxSeq
70+
ctxMutex.Unlock()
71+
5172
ctx := &Context{
73+
ref: ref,
74+
ptr: C.NewContext(opts.iso.ptr, opts.gTmpl.ptr, C.int(ref)),
5275
iso: opts.iso,
53-
ptr: C.NewContext(opts.iso.ptr, opts.gTmpl.ptr),
5476
}
5577
runtime.SetFinalizer(ctx, (*Context).finalizer)
5678
// TODO: [RC] catch any C++ exceptions and return as error
@@ -73,7 +95,10 @@ func (c *Context) RunScript(source string, origin string) (*Value, error) {
7395
defer C.free(unsafe.Pointer(cSource))
7496
defer C.free(unsafe.Pointer(cOrigin))
7597

98+
c.register()
7699
rtn := C.RunScript(c.ptr, cSource, cOrigin)
100+
c.deregister()
101+
77102
return getValue(c, rtn), getError(rtn)
78103
}
79104

@@ -83,11 +108,45 @@ func (c *Context) Close() {
83108
}
84109

85110
func (c *Context) finalizer() {
86-
C.ContextDispose(c.ptr)
111+
C.ContextFree(c.ptr)
87112
c.ptr = nil
88113
runtime.SetFinalizer(c, nil)
89114
}
90115

116+
func (c *Context) register() {
117+
ctxMutex.Lock()
118+
r := ctxRegistry[c.ref]
119+
if r == nil {
120+
r = &ctxRef{ctx: c}
121+
ctxRegistry[c.ref] = r
122+
}
123+
r.refCount++
124+
ctxMutex.Unlock()
125+
}
126+
127+
func (c *Context) deregister() {
128+
ctxMutex.Lock()
129+
defer ctxMutex.Unlock()
130+
r := ctxRegistry[c.ref]
131+
if r == nil {
132+
return
133+
}
134+
r.refCount--
135+
if r.refCount <= 0 {
136+
delete(ctxRegistry, c.ref)
137+
}
138+
}
139+
140+
func getContext(ref int) *Context {
141+
ctxMutex.RLock()
142+
defer ctxMutex.RUnlock()
143+
r := ctxRegistry[ref]
144+
if r == nil {
145+
return nil
146+
}
147+
return r.ctx
148+
}
149+
91150
func getValue(ctx *Context, rtn C.RtnValue) *Value {
92151
if rtn.value == nil {
93152
return nil

context_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,33 @@ func TestJSExceptions(t *testing.T) {
6161
}
6262
}
6363

64+
func TestContextRegistry(t *testing.T) {
65+
t.Parallel()
66+
67+
ctx, _ := v8go.NewContext()
68+
ctxref := ctx.Ref()
69+
70+
c1 := v8go.GetContext(ctxref)
71+
if c1 != nil {
72+
t.Error("expected context to be <nil>")
73+
}
74+
75+
ctx.Register()
76+
c2 := v8go.GetContext(ctxref)
77+
if c2 == nil {
78+
t.Error("expected context, but got <nil>")
79+
}
80+
if c2 != ctx {
81+
t.Errorf("contexts should match %p != %p", c2, ctx)
82+
}
83+
ctx.Deregister()
84+
85+
c3 := v8go.GetContext(ctxref)
86+
if c3 != nil {
87+
t.Error("expected context to be <nil>")
88+
}
89+
}
90+
6491
func BenchmarkContext(b *testing.B) {
6592
b.ReportAllocs()
6693
vm, _ := v8go.NewIsolate()

export_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package v8go
2+
3+
// RegisterCallback is exported for testing only.
4+
func (i *Isolate) RegisterCallback(cb FunctionCallback) int {
5+
return i.registerCallback(cb)
6+
}
7+
8+
// GetCallback is exported for testing only.
9+
func (i *Isolate) GetCallback(ref int) FunctionCallback {
10+
return i.getCallback(ref)
11+
}
12+
13+
// Register is exported for testing only.
14+
func (c *Context) Register() {
15+
c.register()
16+
}
17+
18+
// Deregister is exported for testing only.
19+
func (c *Context) Deregister() {
20+
c.deregister()
21+
}
22+
23+
// GetContext is exported for testing only.
24+
var GetContext = getContext
25+
26+
// Ref is exported for testing only.
27+
func (c *Context) Ref() int {
28+
return c.ref
29+
}

function_template.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package v8go
2+
3+
// #include <stdlib.h>
4+
// #include "v8go.h"
5+
import "C"
6+
import (
7+
"errors"
8+
"runtime"
9+
"unsafe"
10+
)
11+
12+
// FunctionCallback is a callback that is executed in Go when a function is executed in JS.
13+
type FunctionCallback func(info *FunctionCallbackInfo) *Value
14+
15+
// FunctionCallbackInfo is the argument that is passed to a FunctionCallback.
16+
type FunctionCallbackInfo struct {
17+
ctx *Context
18+
args []*Value
19+
}
20+
21+
// Context is the current context that the callback is being executed in.
22+
func (i *FunctionCallbackInfo) Context() *Context {
23+
return i.ctx
24+
}
25+
26+
// Args returns a slice of the value arguments that are passed to the JS function.
27+
func (i *FunctionCallbackInfo) Args() []*Value {
28+
return i.args
29+
}
30+
31+
// FunctionTemplate is used to create functions at runtime.
32+
// There can only be one function created from a FunctionTemplate in a context.
33+
// The lifetime of the created function is equal to the lifetime of the context.
34+
type FunctionTemplate struct {
35+
*template
36+
}
37+
38+
// NewFunctionTemplate creates a FunctionTemplate for a given callback.
39+
func NewFunctionTemplate(iso *Isolate, callback FunctionCallback) (*FunctionTemplate, error) {
40+
if iso == nil {
41+
return nil, errors.New("v8go: failed to create new FunctionTemplate: Isolate cannot be <nil>")
42+
}
43+
if callback == nil {
44+
return nil, errors.New("v8go: failed to create new FunctionTemplate: FunctionCallback cannot be <nil>")
45+
}
46+
47+
cbref := iso.registerCallback(callback)
48+
49+
tmpl := &template{
50+
ptr: C.NewFunctionTemplate(iso.ptr, C.int(cbref)),
51+
iso: iso,
52+
}
53+
runtime.SetFinalizer(tmpl, (*template).finalizer)
54+
return &FunctionTemplate{tmpl}, nil
55+
}
56+
57+
//export goFunctionCallback
58+
func goFunctionCallback(ctxref int, cbref int, args *C.ValuePtr, argsCount int) C.ValuePtr {
59+
ctx := getContext(ctxref)
60+
61+
info := &FunctionCallbackInfo{
62+
ctx: ctx,
63+
args: make([]*Value, argsCount),
64+
}
65+
66+
argv := (*[1 << 30]C.ValuePtr)(unsafe.Pointer(args))[:argsCount:argsCount]
67+
for i, v := range argv {
68+
val := &Value{ptr: v}
69+
runtime.SetFinalizer(val, (*Value).finalizer)
70+
info.args[i] = val
71+
}
72+
73+
callbackFunc := ctx.iso.getCallback(cbref)
74+
if val := callbackFunc(info); val != nil {
75+
return val.ptr
76+
}
77+
return nil
78+
}

function_template_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package v8go_test
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"net/http"
7+
"strings"
8+
"testing"
9+
10+
"rogchap.com/v8go"
11+
)
12+
13+
func TestFunctionTemplate(t *testing.T) {
14+
t.Parallel()
15+
16+
if _, err := v8go.NewFunctionTemplate(nil, func(*v8go.FunctionCallbackInfo) *v8go.Value { return nil }); err == nil {
17+
t.Error("expected error but got <nil>")
18+
}
19+
20+
iso, _ := v8go.NewIsolate()
21+
if _, err := v8go.NewFunctionTemplate(iso, nil); err == nil {
22+
t.Error("expected error but got <nil>")
23+
}
24+
25+
fn, err := v8go.NewFunctionTemplate(iso, func(*v8go.FunctionCallbackInfo) *v8go.Value { return nil })
26+
if err != nil {
27+
t.Errorf("unexpected error: %v", err)
28+
}
29+
if fn == nil {
30+
t.Error("expected FunctionTemplate, but got <nil>")
31+
}
32+
}
33+
34+
func ExampleFunctionTemplate() {
35+
iso, _ := v8go.NewIsolate()
36+
global, _ := v8go.NewObjectTemplate(iso)
37+
printfn, _ := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
38+
fmt.Printf("%+v\n", info.Args())
39+
return nil
40+
})
41+
global.Set("print", printfn, v8go.ReadOnly)
42+
ctx, _ := v8go.NewContext(iso, global)
43+
ctx.RunScript("print('foo', 'bar', 0, 1)", "")
44+
// Output:
45+
// [foo bar 0 1]
46+
}
47+
48+
func ExampleFunctionTemplate_fetch() {
49+
iso, _ := v8go.NewIsolate()
50+
global, _ := v8go.NewObjectTemplate(iso)
51+
fetchfn, _ := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
52+
args := info.Args()
53+
url := args[0].String()
54+
res, _ := http.Get(url)
55+
body, _ := ioutil.ReadAll(res.Body)
56+
val, _ := v8go.NewValue(iso, string(body))
57+
return val
58+
})
59+
global.Set("fetch", fetchfn, v8go.ReadOnly)
60+
ctx, _ := v8go.NewContext(iso, global)
61+
val, _ := ctx.RunScript("fetch('https://rogchap.com/v8go')", "")
62+
fmt.Printf("%s\n", strings.Split(val.String(), "\n")[0])
63+
// Output:
64+
// <!DOCTYPE html>
65+
}

0 commit comments

Comments
 (0)