Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function Templates with callback functions in Go #68

Merged
merged 8 commits into from
Feb 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does vm.TerminateExecution interact with callback of v8go.NewFunctionTemplate?

would it be possible to include context.Context as first argument of callback?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the VM is terminated, you will no longer be able to fire the callback. These callbacks are connected to the isolate, so once the Isolate is GC'd so will be the callback functions (unless attached to some other struct etc)

You can get the current context via info.Context()

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context.Context will get confusing with v8go.Context (as I just did)
The V8 API does not have such a concept as the Go Context; if we introduce the Go Context, this would have to exist throughout the API, which would likely be a breaking change.

Copy link
Owner Author

@rogchap rogchap Feb 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look at your code (https://github.com/choonkeat/try-v8go/blob/main/main.go)
I see what you are trying to do... you want to be able to check if the Go Context is Done within the callback?
Maybe we could attach a context in a way that is non-breaking ie. something like info.GoContext() ??

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@choonkeat Would you be able to submit this Issue as a feature request? It's maybe something we could look to supporting.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context.Context will get confusing with v8go.Context (as I just did)

ya :-P

Would you be able to submit this Issue as a feature request?

it was more of a comment, since idiomatic Go callback function usually comes with context.Context. But if it doesn't fit v8, I don't mind passing my own context.Context via closure


i'm still trying to wrap my head around how things "should" work. e.g. implementing fetch i'd need to return a Promise i think and thus will be tracking #61 closely

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, to implement true fetch you need to return the Promise; A poor man's version is here: https://github.com/rogchap/v8go/blob/master/function_template_test.go#L52

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI. this is better supported now that we have #76

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