Skip to content

Commit

Permalink
Support FunctionCallback returning an error.
Browse files Browse the repository at this point in the history
If the error is a Valuer (like an *Exception), it will be thrown as a
proper JS error, otherwise it becomes a thrown string.
  • Loading branch information
tommie committed Oct 13, 2021
1 parent 3a20035 commit c25d563
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 39 deletions.
4 changes: 2 additions & 2 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
package v8go

// RegisterCallback is exported for testing only.
func (i *Isolate) RegisterCallback(cb FunctionCallback) int {
func (i *Isolate) RegisterCallback(cb FunctionCallbackWithError) int {
return i.registerCallback(cb)
}

// GetCallback is exported for testing only.
func (i *Isolate) GetCallback(ref int) FunctionCallback {
func (i *Isolate) GetCallback(ref int) FunctionCallbackWithError {
return i.getCallback(ref)
}

Expand Down
47 changes: 42 additions & 5 deletions function_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,26 @@ import (
// FunctionCallback is a callback that is executed in Go when a function is executed in JS.
type FunctionCallback func(info *FunctionCallbackInfo) *Value

// FunctionCallbackWithError is a callback that is executed in Go when
// a function is executed in JS. If a ValueError is returned, its
// value will be thrown as an exception in V8, otherwise Error() is
// invoked, and the string is thrown.
type FunctionCallbackWithError func(info *FunctionCallbackInfo) (*Value, error)

// FunctionCallbackInfo is the argument that is passed to a FunctionCallback.
type FunctionCallbackInfo struct {
ctx *Context
args []*Value
this *Object
}

// A ValuerError can be returned from a FunctionCallbackWithError, and
// its value will be thrown as an exception in V8.
type ValuerError interface {
error
Valuer
}

// Context is the current context that the callback is being executed in.
func (i *FunctionCallbackInfo) Context() *Context {
return i.ctx
Expand All @@ -44,8 +57,21 @@ type FunctionTemplate struct {
*template
}

// NewFunctionTemplate creates a FunctionTemplate for a given callback.
// NewFunctionTemplate creates a FunctionTemplate for a given
// callback. Prefer using NewFunctionTemplateWithError.
func NewFunctionTemplate(iso *Isolate, callback FunctionCallback) *FunctionTemplate {
if callback == nil {
panic("nil FunctionCallback argument not supported")
}
return NewFunctionTemplateWithError(iso, func(info *FunctionCallbackInfo) (*Value, error) {
return callback(info), nil
})
}

// NewFunctionTemplateWithError creates a FunctionTemplate for a given
// callback. If the callback returns an error, it will be thrown as a
// JS error.
func NewFunctionTemplateWithError(iso *Isolate, callback FunctionCallbackWithError) *FunctionTemplate {
if iso == nil {
panic("nil Isolate argument not supported")
}
Expand Down Expand Up @@ -77,7 +103,7 @@ func (tmpl *FunctionTemplate) GetFunction(ctx *Context) *Function {
// Note that ideally `thisAndArgs` would be split into two separate arguments, but they were combined
// to workaround an ERROR_COMMITMENT_LIMIT error on windows that was detected in CI.
//export goFunctionCallback
func goFunctionCallback(ctxref int, cbref int, thisAndArgs *C.ValuePtr, argsCount int) C.ValuePtr {
func goFunctionCallback(ctxref int, cbref int, thisAndArgs *C.ValuePtr, argsCount int) (rval C.ValuePtr, rerr C.ValuePtr) {
ctx := getContext(ctxref)

this := *thisAndArgs
Expand All @@ -94,8 +120,19 @@ func goFunctionCallback(ctxref int, cbref int, thisAndArgs *C.ValuePtr, argsCoun
}

callbackFunc := ctx.iso.getCallback(cbref)
if val := callbackFunc(info); val != nil {
return val.ptr
val, err := callbackFunc(info)
if err != nil {
if verr, ok := err.(ValuerError); ok {
return nil, verr.value().ptr
}
errv, err := NewValue(ctx.iso, err.Error())
if err != nil {
panic(err)
}
return nil, errv.ptr
}
if val == nil {
return nil, nil
}
return nil
return val.ptr, nil
}
100 changes: 76 additions & 24 deletions function_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,32 +55,84 @@ func TestFunctionTemplate_panic_on_nil_callback(t *testing.T) {
func TestFunctionTemplateGetFunction(t *testing.T) {
t.Parallel()

iso := v8.NewIsolate()
defer iso.Dispose()
ctx := v8.NewContext(iso)
defer ctx.Close()
t.Run("can_call", func(t *testing.T) {
t.Parallel()

iso := v8.NewIsolate()
defer iso.Dispose()
ctx := v8.NewContext(iso)
defer ctx.Close()

var args *v8.FunctionCallbackInfo
tmpl := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
args = info
reply, _ := v8.NewValue(iso, "hello")
return reply
})
fn := tmpl.GetFunction(ctx)
ten, err := v8.NewValue(iso, int32(10))
if err != nil {
t.Fatal(err)
}
ret, err := fn.Call(v8.Undefined(iso), ten)
if err != nil {
t.Fatal(err)
}
if len(args.Args()) != 1 || args.Args()[0].String() != "10" {
t.Fatalf("expected args [10], got: %+v", args.Args())
}
if !ret.IsString() || ret.String() != "hello" {
t.Fatalf("expected return value of 'hello', was: %v", ret)
}
})

t.Run("can_throw_string", func(t *testing.T) {
t.Parallel()

iso := v8.NewIsolate()
defer iso.Dispose()

tmpl := v8.NewFunctionTemplateWithError(iso, func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
return nil, fmt.Errorf("fake error")
})
global := v8.NewObjectTemplate(iso)
global.Set("foo", tmpl)

ctx := v8.NewContext(iso, global)
defer ctx.Close()

ret, err := ctx.RunScript("(() => { try { foo(); return null; } catch (e) { return e; } })()", "")
if err != nil {
t.Fatal(err)
}
if !ret.IsString() || ret.String() != "fake error" {
t.Fatalf("expected return value of 'hello', was: %v", ret)
}
})

t.Run("can_throw_exception", func(t *testing.T) {
t.Parallel()

iso := v8.NewIsolate()
defer iso.Dispose()

tmpl := v8.NewFunctionTemplateWithError(iso, func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
return nil, v8.NewError(iso, "fake error")
})
global := v8.NewObjectTemplate(iso)
global.Set("foo", tmpl)

ctx := v8.NewContext(iso, global)
defer ctx.Close()

var args *v8.FunctionCallbackInfo
tmpl := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
args = info
reply, _ := v8.NewValue(iso, "hello")
return reply
ret, err := ctx.RunScript("(() => { try { foo(); return null; } catch (e) { return e; } })()", "")
if err != nil {
t.Fatal(err)
}
if !ret.IsNativeError() || !strings.Contains(ret.String(), "fake error") {
t.Fatalf("expected return value of Error('hello'), was: %v", ret)
}
})
fn := tmpl.GetFunction(ctx)
ten, err := v8.NewValue(iso, int32(10))
if err != nil {
t.Fatal(err)
}
ret, err := fn.Call(v8.Undefined(iso), ten)
if err != nil {
t.Fatal(err)
}
if len(args.Args()) != 1 || args.Args()[0].String() != "10" {
t.Fatalf("expected args [10], got: %+v", args.Args())
}
if !ret.IsString() || ret.String() != "hello" {
t.Fatalf("expected return value of 'hello', was: %v", ret)
}
}

func TestFunctionCallbackInfoThis(t *testing.T) {
Expand Down
8 changes: 4 additions & 4 deletions isolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type Isolate struct {

cbMutex sync.RWMutex
cbSeq int
cbs map[int]FunctionCallback
cbs map[int]FunctionCallbackWithError

null *Value
undefined *Value
Expand Down Expand Up @@ -55,7 +55,7 @@ func NewIsolate() *Isolate {
})
iso := &Isolate{
ptr: C.NewIsolate(),
cbs: make(map[int]FunctionCallback),
cbs: make(map[int]FunctionCallbackWithError),
}
iso.null = newValueNull(iso)
iso.undefined = newValueUndefined(iso)
Expand Down Expand Up @@ -112,7 +112,7 @@ func (i *Isolate) apply(opts *contextOptions) {
opts.iso = i
}

func (i *Isolate) registerCallback(cb FunctionCallback) int {
func (i *Isolate) registerCallback(cb FunctionCallbackWithError) int {
i.cbMutex.Lock()
i.cbSeq++
ref := i.cbSeq
Expand All @@ -121,7 +121,7 @@ func (i *Isolate) registerCallback(cb FunctionCallback) int {
return ref
}

func (i *Isolate) getCallback(ref int) FunctionCallback {
func (i *Isolate) getCallback(ref int) FunctionCallbackWithError {
i.cbMutex.RLock()
defer i.cbMutex.RUnlock()
return i.cbs[ref]
Expand Down
2 changes: 1 addition & 1 deletion isolate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func TestCallbackRegistry(t *testing.T) {

iso := v8.NewIsolate()
defer iso.Dispose()
cb := func(*v8.FunctionCallbackInfo) *v8.Value { return nil }
cb := func(*v8.FunctionCallbackInfo) (*v8.Value, error) { return nil, nil }

cb0 := iso.GetCallback(0)
if cb0 != nil {
Expand Down
18 changes: 18 additions & 0 deletions promise.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ func (p *Promise) Result() *Value {
// The default MicrotaskPolicy processes them when the call depth decreases to 0.
// Call (*Context).PerformMicrotaskCheckpoint to trigger it manually.
func (p *Promise) Then(cbs ...FunctionCallback) *Promise {
cbwes := make([]FunctionCallbackWithError, len(cbs))
for i, cb := range cbs {
cb := cb
cbwes[i] = func(info *FunctionCallbackInfo) (*Value, error) {
return cb(info), nil
}
}

return p.ThenWithError(cbwes...)
}

func (p *Promise) ThenWithError(cbs ...FunctionCallbackWithError) *Promise {
var rtn C.RtnValue
switch len(cbs) {
case 1:
Expand All @@ -117,6 +129,12 @@ func (p *Promise) Then(cbs ...FunctionCallback) *Promise {
// Catch invokes the given function if the promise is rejected.
// See Then for other details.
func (p *Promise) Catch(cb FunctionCallback) *Promise {
return p.CatchWithError(func(info *FunctionCallbackInfo) (*Value, error) {
return cb(info), nil
})
}

func (p *Promise) CatchWithError(cb FunctionCallbackWithError) *Promise {
cbID := p.ctx.iso.registerCallback(cb)
rtn := C.PromiseCatch(p.ptr, C.int(cbID))
obj, err := objectResult(p.ctx, rtn)
Expand Down
31 changes: 31 additions & 0 deletions promise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package v8go_test

import (
"errors"
"testing"

v8 "rogchap.com/v8go"
Expand Down Expand Up @@ -110,6 +111,36 @@ func TestPromiseRejected(t *testing.T) {
}
}

func TestPromiseThenCanThrow(t *testing.T) {
t.Parallel()

iso := v8.NewIsolate()
defer iso.Dispose()
ctx := v8.NewContext(iso)
defer ctx.Close()

res, _ := v8.NewPromiseResolver(ctx)

promThenVal := res.GetPromise().ThenWithError(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
return nil, errors.New("faked error")
})
promThen, err := promThenVal.AsPromise()
if err != nil {
t.Fatalf("AsPromise failed: %v", err)
}

val, _ := v8.NewValue(iso, "foo")
res.Resolve(val)

if s := promThen.State(); s != v8.Rejected {
t.Fatalf("unexpected state for Promise, want Rejected (%v) got: %v", v8.Rejected, s)
}

if result := promThen.Result(); result.String() != "faked error" {
t.Errorf("expected the Promise result to match the resolve value, but got: %s", result)
}
}

func TestPromiseThenPanic(t *testing.T) {
t.Parallel()

Expand Down
8 changes: 5 additions & 3 deletions v8go.cc
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,12 @@ static void FunctionTemplateCallback(const FunctionCallbackInfo<Value>& info) {
args[i] = tracked_value(ctx, val);
}

ValuePtr val =
goFunctionCallback_return retval =
goFunctionCallback(ctx_ref, callback_ref, thisAndArgs, args_count);
if (val != nullptr) {
info.GetReturnValue().Set(val->ptr.Get(iso));
if (retval.r1 != nullptr) {
iso->ThrowException(retval.r1->ptr.Get(iso));
} else if (retval.r0 != nullptr) {
info.GetReturnValue().Set(retval.r0->ptr.Get(iso));
} else {
info.GetReturnValue().SetUndefined();
}
Expand Down

0 comments on commit c25d563

Please sign in to comment.