Skip to content

Commit

Permalink
Function Templates with callback functions in Go (#68)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rogchap authored Feb 2, 2021
1 parent b35f871 commit eddbe9a
Show file tree
Hide file tree
Showing 14 changed files with 585 additions and 161 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 62 additions & 3 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
}

Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <nil>")
}

ctx.Register()
c2 := v8go.GetContext(ctxref)
if c2 == nil {
t.Error("expected context, but got <nil>")
}
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 <nil>")
}
}

func BenchmarkContext(b *testing.B) {
b.ReportAllocs()
vm, _ := v8go.NewIsolate()
Expand Down
29 changes: 29 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -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
}
78 changes: 78 additions & 0 deletions function_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package v8go

// #include <stdlib.h>
// #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 <nil>")
}
if callback == nil {
return nil, errors.New("v8go: failed to create new FunctionTemplate: FunctionCallback cannot be <nil>")
}

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
}
65 changes: 65 additions & 0 deletions function_template_test.go
Original file line number Diff line number Diff line change
@@ -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 <nil>")
}

iso, _ := v8go.NewIsolate()
if _, err := v8go.NewFunctionTemplate(iso, nil); err == nil {
t.Error("expected error but got <nil>")
}

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 <nil>")
}
}

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:
// <!DOCTYPE html>
}
Loading

0 comments on commit eddbe9a

Please sign in to comment.