diff --git a/exception.go b/exception.go new file mode 100644 index 000000000..faed9e847 --- /dev/null +++ b/exception.go @@ -0,0 +1,109 @@ +// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package v8go + +import ( + // #include + // #include "v8go.h" + "C" + + "fmt" + "unsafe" +) + +// NewRangeError creates a RangeError. +func NewRangeError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_RANGE, msg) +} + +// NewReferenceError creates a ReferenceError. +func NewReferenceError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_REFERENCE, msg) +} + +// NewSyntaxError creates a SyntaxError. +func NewSyntaxError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_SYNTAX, msg) +} + +// NewTypeError creates a TypeError. +func NewTypeError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_TYPE, msg) +} + +// NewWasmCompileError creates a WasmCompileError. +func NewWasmCompileError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_WASM_COMPILE, msg) +} + +// NewWasmLinkError creates a WasmLinkError. +func NewWasmLinkError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_WASM_LINK, msg) +} + +// NewWasmRuntimeError creates a WasmRuntimeError. +func NewWasmRuntimeError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_WASM_RUNTIME, msg) +} + +// NewError creates an Error, which is the common thing to throw from +// user code. +func NewError(iso *Isolate, msg string) *Exception { + return newExceptionError(iso, C.ERROR_GENERIC, msg) +} + +func newExceptionError(iso *Isolate, typ C.ErrorTypeIndex, msg string) *Exception { + cmsg := C.CString(msg) + defer C.free(unsafe.Pointer(cmsg)) + eptr := C.NewValueError(iso.ptr, typ, cmsg) + if eptr == nil { + panic(fmt.Errorf("invalid error type index: %d", typ)) + } + return &Exception{&Value{ptr: eptr}} +} + +// An Exception is a JavaScript exception. +type Exception struct { + *Value +} + +// value implements Valuer. +func (e *Exception) value() *Value { + return e.Value +} + +// Error implements error. +func (e *Exception) Error() string { + return e.String() +} + +// As provides support for errors.As. +func (e *Exception) As(target interface{}) bool { + ep, ok := target.(**Exception) + if !ok { + return false + } + *ep = e + return true +} + +// Is provides support for errors.Is. +func (e *Exception) Is(err error) bool { + eerr, ok := err.(*Exception) + if !ok { + return false + } + return eerr.String() == e.String() +} + +// String implements fmt.Stringer. +func (e *Exception) String() string { + if e.Value == nil { + return "" + } + s := C.ExceptionGetMessageString(e.ptr) + defer C.free(unsafe.Pointer(s)) + return C.GoString(s) +} diff --git a/exception_test.go b/exception_test.go new file mode 100644 index 000000000..1b661ba9b --- /dev/null +++ b/exception_test.go @@ -0,0 +1,79 @@ +// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package v8go_test + +import ( + "errors" + "strings" + "testing" + + v8 "rogchap.com/v8go" +) + +func TestNewError(t *testing.T) { + t.Parallel() + + tsts := []struct { + New func(*v8.Isolate, string) *v8.Exception + WantType string + }{ + {v8.NewRangeError, "RangeError"}, + {v8.NewReferenceError, "ReferenceError"}, + {v8.NewSyntaxError, "SyntaxError"}, + {v8.NewTypeError, "TypeError"}, + {v8.NewWasmCompileError, "CompileError"}, + {v8.NewWasmLinkError, "LinkError"}, + {v8.NewWasmRuntimeError, "RuntimeError"}, + {v8.NewError, "Error"}, + } + for _, tst := range tsts { + t.Run(tst.WantType, func(t *testing.T) { + iso := v8.NewIsolate() + defer iso.Dispose() + + got := tst.New(iso, "amessage") + if !got.IsNativeError() { + t.Error("IsNativeError returned false, want true") + } + if got := got.Error(); !strings.Contains(got, " "+tst.WantType+":") { + t.Errorf("Error(): got %q, want containing %q", got, tst.WantType) + } + }) + } +} + +func TestExceptionAs(t *testing.T) { + iso := v8.NewIsolate() + defer iso.Dispose() + + want := v8.NewRangeError(iso, "faked error") + + var got *v8.Exception + if !want.As(&got) { + t.Fatalf("As failed") + } + + if got != want { + t.Errorf("As: got %#v, want %#v", got, want) + } +} + +func TestExceptionIs(t *testing.T) { + iso := v8.NewIsolate() + defer iso.Dispose() + + t.Run("ok", func(t *testing.T) { + ex := v8.NewRangeError(iso, "faked error") + if !ex.Is(v8.NewRangeError(iso, "faked error")) { + t.Fatalf("Is: got false, want true") + } + }) + + t.Run("notok", func(t *testing.T) { + if (&v8.Exception{}).Is(errors.New("other error")) { + t.Fatalf("Is: got true, want false") + } + }) +} diff --git a/export_test.go b/export_test.go index 8d5e61f2a..710da31b1 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 5fad38088..d4e3796c9 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 de2dd382d..c4b8b42c5 100644 --- a/function_template_test.go +++ b/function_template_test.go @@ -6,6 +6,7 @@ package v8go_test import ( "fmt" + "strings" "testing" v8 "rogchap.com/v8go" @@ -52,32 +53,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 661fbec05..a01c6247a 100644 --- a/isolate.go +++ b/isolate.go @@ -23,7 +23,7 @@ type Isolate struct { cbMutex sync.RWMutex cbSeq int - cbs map[int]FunctionCallback + cbs map[int]FunctionCallbackWithError null *Value undefined *Value @@ -57,7 +57,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) @@ -171,7 +171,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 @@ -180,7 +180,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 a6cca8e53..4ef204690 100644 --- a/isolate_test.go +++ b/isolate_test.go @@ -164,7 +164,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 f056bfd9e..2807368b5 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 0ecf467d2..7e014ad1d 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 97390883e..15d20f4bb 100644 --- a/v8go.cc +++ b/v8go.cc @@ -508,10 +508,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(); } @@ -908,6 +910,50 @@ RtnValue NewValueBigIntFromWords(IsolatePtr iso, return rtn; } +ValuePtr NewValueError(IsolatePtr iso, + ErrorTypeIndex idx, + const char* message) { + ISOLATE_SCOPE_INTERNAL_CONTEXT(iso); + Local local_ctx = ctx->ptr.Get(iso); + Context::Scope context_scope(local_ctx); + + Local local_msg = String::NewFromUtf8(iso, message).ToLocalChecked(); + Local v; + switch (idx) { + case ERROR_RANGE: + v = Exception::RangeError(local_msg); + break; + case ERROR_REFERENCE: + v = Exception::ReferenceError(local_msg); + break; + case ERROR_SYNTAX: + v = Exception::SyntaxError(local_msg); + break; + case ERROR_TYPE: + v = Exception::TypeError(local_msg); + break; + case ERROR_WASM_COMPILE: + v = Exception::WasmCompileError(local_msg); + break; + case ERROR_WASM_LINK: + v = Exception::WasmLinkError(local_msg); + break; + case ERROR_WASM_RUNTIME: + v = Exception::WasmRuntimeError(local_msg); + break; + case ERROR_GENERIC: + v = Exception::Error(local_msg); + break; + default: + return nullptr; + } + m_value* val = new m_value; + val->iso = iso; + val->ctx = ctx; + val->ptr = Persistent>(iso, v); + return tracked_value(ctx, val); +} + const uint32_t* ValueToArrayIndex(ValuePtr ptr) { LOCAL_VALUE(ptr); Local array_index; @@ -1284,6 +1330,17 @@ int ValueIsModuleNamespaceObject(ValuePtr ptr) { return value->IsModuleNamespaceObject(); } +/********** Exception **********/ + +const char* ExceptionGetMessageString(ValuePtr ptr) { + LOCAL_VALUE(ptr); + + Local local_msg = Exception::CreateMessage(iso, value); + Local local_str = local_msg->Get(); + String::Utf8Value utf8(iso, local_str); + return CopyString(utf8); +} + /********** Object **********/ #define LOCAL_OBJECT(ptr) \ diff --git a/v8go.h b/v8go.h index 7acaf0425..3c9db810c 100644 --- a/v8go.h +++ b/v8go.h @@ -53,6 +53,17 @@ typedef m_value* ValuePtr; typedef m_template* TemplatePtr; typedef m_unboundScript* UnboundScriptPtr; +typedef enum { + ERROR_RANGE = 1, + ERROR_REFERENCE, + ERROR_SYNTAX, + ERROR_TYPE, + ERROR_WASM_COMPILE, + ERROR_WASM_LINK, + ERROR_WASM_RUNTIME, + ERROR_GENERIC, +} ErrorTypeIndex; + typedef struct { const char* msg; const char* location; @@ -203,6 +214,9 @@ extern RtnValue NewValueBigIntFromWords(IsolatePtr iso_ptr, int sign_bit, int word_count, const uint64_t* words); +extern ValuePtr NewValueError(IsolatePtr iso_ptr, + ErrorTypeIndex idx, + const char* message); extern RtnString ValueToString(ValuePtr ptr); const uint32_t* ValueToArrayIndex(ValuePtr ptr); int ValueToBoolean(ValuePtr ptr); @@ -269,6 +283,8 @@ int ValueIsProxy(ValuePtr ptr); int ValueIsWasmModuleObject(ValuePtr ptr); int ValueIsModuleNamespaceObject(ValuePtr ptr); +const char* ExceptionGetMessageString(ValuePtr ptr); + extern void ObjectSet(ValuePtr ptr, const char* key, ValuePtr val_ptr); extern void ObjectSetIdx(ValuePtr ptr, uint32_t idx, ValuePtr val_ptr); extern int ObjectSetInternalField(ValuePtr ptr, int idx, ValuePtr val_ptr); diff --git a/value.go b/value.go index c4ba9acf0..5f07a026a 100644 --- a/value.go +++ b/value.go @@ -560,6 +560,13 @@ func (v *Value) AsPromise() (*Promise, error) { return &Promise{&Object{v}}, nil } +func (v *Value) AsException() (*Exception, error) { + if !v.IsNativeError() { + return nil, errors.New("v8go: value is not an Error") + } + return &Exception{v}, nil +} + func (v *Value) AsFunction() (*Function, error) { if !v.IsFunction() { return nil, errors.New("v8go: value is not a Function") diff --git a/value_test.go b/value_test.go index d386a5893..bfe9b6f21 100644 --- a/value_test.go +++ b/value_test.go @@ -498,6 +498,27 @@ func TestValuePromise(t *testing.T) { } +func TestValueAsException(t *testing.T) { + t.Parallel() + + ctx := v8.NewContext() + defer ctx.Isolate().Dispose() + defer ctx.Close() + + val, _ := ctx.RunScript("1", "") + if _, err := val.AsException(); err == nil { + t.Error("Expected error but got ") + } + val, err := ctx.RunScript("new Error('foo')", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if _, err := val.AsException(); err != nil { + t.Errorf("Expected success but got: %v", err) + } + +} + func TestValueFunction(t *testing.T) { t.Parallel()