diff --git a/export_test.go b/export_test.go index 8d5e61f2..710da31b 100644 --- a/export_test.go +++ b/export_test.go @@ -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) } diff --git a/function_template.go b/function_template.go index 5fad3808..d4e3796c 100644 --- a/function_template.go +++ b/function_template.go @@ -15,6 +15,12 @@ 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 @@ -22,6 +28,13 @@ type FunctionCallbackInfo struct { this *Object } +// A ValueError can be returned from a FunctionCallbackWithError, and +// its value will be thrown as an exception in V8. +type ValueError interface { + error + Valuer +} + // Context is the current context that the callback is being executed in. func (i *FunctionCallbackInfo) Context() *Context { return i.ctx @@ -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") } @@ -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 @@ -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.(ValueError); 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 } diff --git a/function_template_test.go b/function_template_test.go index f2a0913d..48304121 100644 --- a/function_template_test.go +++ b/function_template_test.go @@ -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) { diff --git a/isolate.go b/isolate.go index 52e9f98a..92b3bdc5 100644 --- a/isolate.go +++ b/isolate.go @@ -21,7 +21,7 @@ type Isolate struct { cbMutex sync.RWMutex cbSeq int - cbs map[int]FunctionCallback + cbs map[int]FunctionCallbackWithError null *Value undefined *Value @@ -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) @@ -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 @@ -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] diff --git a/isolate_test.go b/isolate_test.go index fe400457..c653be72 100644 --- a/isolate_test.go +++ b/isolate_test.go @@ -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 { diff --git a/promise.go b/promise.go index f056bfd9..2807368b 100644 --- a/promise.go +++ b/promise.go @@ -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: @@ -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) diff --git a/promise_test.go b/promise_test.go index 0ecf467d..7e014ad1 100644 --- a/promise_test.go +++ b/promise_test.go @@ -5,6 +5,7 @@ package v8go_test import ( + "errors" "testing" v8 "rogchap.com/v8go" @@ -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() diff --git a/v8go.cc b/v8go.cc index 92632731..3a960e3e 100644 --- a/v8go.cc +++ b/v8go.cc @@ -311,10 +311,12 @@ static void FunctionTemplateCallback(const FunctionCallbackInfo& 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(); }