diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7778130..8ee01b2cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 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 + +### 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 - Upgraded V8 to 8.8.278.14 diff --git a/context.go b/context.go index 234bec9a8..94b4afafc 100644 --- a/context.go +++ b/context.go @@ -12,27 +12,45 @@ import ( // Context is a global root execution environment that allows separate, // unrelated, JavaScript applications to run in a single instance of V8. type Context struct { - iso *Isolate ptr C.ContextPtr + iso *Isolate +} + +type contextOptions struct { + iso *Isolate + gTmpl *ObjectTemplate +} + +// ContextOption sets options such as Isolate and Global Template to the NewContext +type ContextOption interface { + apply(*contextOptions) } -// NewContext creates a new JavaScript context for a given isoltate; -// if isolate is `nil` than a new isolate will be created. -func NewContext(iso *Isolate) (*Context, error) { - if iso == nil { +// NewContext creates a new JavaScript context; if no Isolate is passed as a +// ContextOption than a new Isolate will be created. +func NewContext(opt ...ContextOption) (*Context, error) { + opts := contextOptions{} + for _, o := range opt { + if o != nil { + o.apply(&opts) + } + } + + if opts.iso == nil { var err error - iso, err = NewIsolate() + opts.iso, err = NewIsolate() if err != nil { - return nil, fmt.Errorf("context: failed to create new Isolate: %v", err) + return nil, fmt.Errorf("v8go: failed to create new Isolate: %v", err) } } - // TODO: [RC] does the isolate need to track all the contexts created? - // any script run against the context should make sure the VM has not been - // terninated + if opts.gTmpl == nil { + opts.gTmpl = &ObjectTemplate{} + } + ctx := &Context{ - iso: iso, - ptr: C.NewContext(iso.ptr), + 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 @@ -56,7 +74,7 @@ func (c *Context) RunScript(source string, origin string) (*Value, error) { defer C.free(unsafe.Pointer(cOrigin)) rtn := C.RunScript(c.ptr, cSource, cOrigin) - return getValue(rtn), getError(rtn) + return getValue(c, rtn), getError(rtn) } // Close will dispose the context and free the memory. @@ -70,11 +88,11 @@ func (c *Context) finalizer() { runtime.SetFinalizer(c, nil) } -func getValue(rtn C.RtnValue) *Value { +func getValue(ctx *Context, rtn C.RtnValue) *Value { if rtn.value == nil { return nil } - v := &Value{rtn.value} + v := &Value{rtn.value, ctx} runtime.SetFinalizer(v, (*Value).finalizer) return v } diff --git a/context_test.go b/context_test.go index 4d2d679d9..07efe26c7 100644 --- a/context_test.go +++ b/context_test.go @@ -74,3 +74,39 @@ func BenchmarkContext(b *testing.B) { ctx.Close() } } + +func ExampleContext() { + ctx, _ := v8go.NewContext() + ctx.RunScript("const add = (a, b) => a + b", "math.js") + ctx.RunScript("const result = add(3, 4)", "main.js") + val, _ := ctx.RunScript("result", "value.js") + fmt.Println(val) + // Output: + // 7 +} + +func ExampleContext_isolate() { + iso, _ := v8go.NewIsolate() + ctx1, _ := v8go.NewContext(iso) + ctx1.RunScript("const foo = 'bar'", "context_one.js") + val, _ := ctx1.RunScript("foo", "foo.js") + fmt.Println(val) + + ctx2, _ := v8go.NewContext(iso) + _, err := ctx2.RunScript("foo", "context_two.js") + fmt.Println(err) + // Output: + // bar + // ReferenceError: foo is not defined +} + +func ExampleContext_globalTemplate() { + iso, _ := v8go.NewIsolate() + obj, _ := v8go.NewObjectTemplate(iso) + obj.Set("version", "v1.0.0") + ctx, _ := v8go.NewContext(iso, obj) + val, _ := ctx.RunScript("version", "main.js") + fmt.Println(val) + // Output: + // v1.0.0 +} diff --git a/isolate.go b/isolate.go index 8b45bbbab..acc13ac01 100644 --- a/isolate.go +++ b/isolate.go @@ -35,6 +35,8 @@ type HeapStatistics struct { // NewIsolate creates a new V8 isolate. Only one thread may access // a given isolate at a time, but different threads may access // different isolates simultaneously. +// An *Isolate can be used as a v8go.ContextOption to create a new +// Context, rather than creating a new default Isolate. func NewIsolate() (*Isolate, error) { v8once.Do(func() { C.Init() @@ -80,3 +82,7 @@ func (i *Isolate) finalizer() { func (i *Isolate) Close() { i.finalizer() } + +func (i *Isolate) apply(opts *contextOptions) { + opts.iso = i +} diff --git a/object_template.go b/object_template.go new file mode 100644 index 000000000..3006ae9ef --- /dev/null +++ b/object_template.go @@ -0,0 +1,93 @@ +package v8go + +// #include +// #include "v8go.h" +import "C" +import ( + "errors" + "fmt" + "math/big" + "runtime" + "unsafe" +) + +// PropertyAttribute are the attribute flags for a property on an Object. +// Typical usage when setting an Object or TemplateObject property, and +// can also be validated when accessing a property. +type PropertyAttribute uint8 + +const ( + // None. + None PropertyAttribute = 0 + // ReadOnly, ie. not writable. + ReadOnly PropertyAttribute = 1 << iota + // DontEnum, ie. not enumerable. + DontEnum + // DontDelete, ie. not configurable. + DontDelete +) + +// ObjectTemplate is used to create objects at runtime. +// Properties added to an ObjectTemplate are added to each object created from the ObjectTemplate. +type ObjectTemplate struct { + ptr C.ObjectTemplatePtr + iso *Isolate +} + +// NewObjectTemplate creates a new ObjectTemplate. +// The *ObjectTemplate can be used as a v8go.ContextOption to create a global object in a Context. +func NewObjectTemplate(iso *Isolate) (*ObjectTemplate, error) { + if iso == nil { + return nil, errors.New("v8go: failed to create new ObjectTemplate: Isolate cannot be ") + } + ob := &ObjectTemplate{ + ptr: C.NewObjectTemplate(iso.ptr), + iso: iso, + } + runtime.SetFinalizer(ob, (*ObjectTemplate).finalizer) + return ob, nil +} + +// Set adds a property to each instance created by this template. +// The property must be defined either as a primitive value, or a template. +// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int) +// then a value will be created and set as the value property. +func (o *ObjectTemplate) Set(name string, val interface{}, attributes ...PropertyAttribute) error { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + + var attrs PropertyAttribute + for _, a := range attributes { + attrs |= a + } + + switch v := val.(type) { + case string, int32, uint32, int64, uint64, float64, bool, *big.Int: + newVal, err := NewValue(o.iso, v) + if err != nil { + return fmt.Errorf("v8go: unable to create new value: %v", err) + } + C.ObjectTemplateSetValue(o.ptr, cname, newVal.ptr, C.int(attrs)) + case *ObjectTemplate: + C.ObjectTemplateSetObjectTemplate(o.ptr, cname, v.ptr, C.int(attrs)) + case *Value: + if v.IsObject() || v.IsExternal() { + return errors.New("v8go: unsupported property: value type must be a primitive or use a template") + } + C.ObjectTemplateSetValue(o.ptr, cname, v.ptr, C.int(attrs)) + default: + return fmt.Errorf("v8go: unsupported property type `%T`, must be one of string, int32, uint32, int64, uint64, float64, *big.Int, *v8go.Value or *v8go.ObjectTemplate", v) + } + + return nil +} + +func (o *ObjectTemplate) apply(opts *contextOptions) { + opts.gTmpl = o +} + +func (o *ObjectTemplate) finalizer() { + C.ObjectTemplateDispose(o.ptr) + o.ptr = nil + runtime.SetFinalizer(o, nil) +} diff --git a/object_template_test.go b/object_template_test.go new file mode 100644 index 000000000..66e6226ef --- /dev/null +++ b/object_template_test.go @@ -0,0 +1,112 @@ +package v8go_test + +import ( + "math/big" + "testing" + + "rogchap.com/v8go" +) + +func TestObjectTemplate(t *testing.T) { + t.Parallel() + _, err := v8go.NewObjectTemplate(nil) + if err == nil { + t.Fatal("expected error but got ") + } + iso, _ := v8go.NewIsolate() + obj, err := v8go.NewObjectTemplate(iso) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + setError := func(t *testing.T, err error) { + if err != nil { + t.Errorf("failed to set property: %v", err) + } + } + + val, _ := v8go.NewValue(iso, "bar") + objVal, _ := v8go.NewObjectTemplate(iso) + bigbigint, _ := new(big.Int).SetString("36893488147419099136", 10) // larger than a single word size (64bit) + + tests := [...]struct { + name string + value interface{} + }{ + {"str", "foo"}, + {"i32", int32(1)}, + {"u32", uint32(1)}, + {"i64", int64(1)}, + {"u64", uint64(1)}, + {"float64", float64(1)}, + {"bigint", big.NewInt(1)}, + {"bigbigint", bigbigint}, + {"bool", true}, + {"val", val}, + {"obj", objVal}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + setError(t, obj.Set(tt.name, tt.value, 0)) + }) + } +} + +func TestGlobalObjectTemplate(t *testing.T) { + t.Parallel() + iso, _ := v8go.NewIsolate() + tests := [...]struct { + global func() *v8go.ObjectTemplate + source string + validate func(t *testing.T, val *v8go.Value) + }{ + { + func() *v8go.ObjectTemplate { + gbl, _ := v8go.NewObjectTemplate(iso) + gbl.Set("foo", "bar") + return gbl + }, + "foo", + func(t *testing.T, val *v8go.Value) { + if !val.IsString() { + t.Errorf("expect value %q to be of type String", val) + return + } + if val.String() != "bar" { + t.Errorf("unexpected value: %v", val) + } + }, + }, + { + func() *v8go.ObjectTemplate { + foo, _ := v8go.NewObjectTemplate(iso) + foo.Set("bar", "baz") + gbl, _ := v8go.NewObjectTemplate(iso) + gbl.Set("foo", foo) + return gbl + }, + "foo.bar", + func(t *testing.T, val *v8go.Value) { + if val.String() != "baz" { + t.Errorf("unexpected value: %v", val) + } + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.source, func(t *testing.T) { + t.Parallel() + ctx, _ := v8go.NewContext(iso, tt.global()) + val, err := ctx.RunScript(tt.source, "test.js") + if err != nil { + t.Fatalf("unexpected error runing script: %v", err) + } + tt.validate(t, val) + }) + } +} diff --git a/v8go.cc b/v8go.cc index 5c0a5a841..eef49001a 100644 --- a/v8go.cc +++ b/v8go.cc @@ -23,9 +23,15 @@ typedef struct { typedef struct { Persistent ptr; + Isolate* iso; m_ctx* ctx_ptr; } m_value; +typedef struct { + Persistent ptr; + Isolate* iso; +} m_object_template; + const char* CopyString(std::string str) { int len = str.length(); char* mem = (char*)malloc(len + 1); @@ -87,6 +93,12 @@ extern "C" { /********** Isolate **********/ +#define ISOLATE_SCOPE(iso_ptr) \ + Isolate* iso = static_cast(iso_ptr); \ + Locker locker(iso); \ + Isolate::Scope isolate_scope(iso); \ + HandleScope handle_scope(iso); \ + void Init() { #ifdef _WIN32 V8::InitializeExternalStartupData("."); @@ -136,18 +148,75 @@ IsolateHStatistics IsolationGetHeapStatistics(IsolatePtr ptr) { hs.number_of_detached_contexts()}; } +/********** ObjectTemplate **********/ + +#define LOCAL_OBJECT_TEMPLATE(ptr) \ + m_object_template* ot = static_cast(ptr); \ + Isolate* iso = ot->iso; \ + Locker locker(iso); \ + Isolate::Scope isolate_scope(iso); \ + HandleScope handle_scope(iso); \ + Local object_template = ot->ptr.Get(iso); + +ObjectTemplatePtr NewObjectTemplate(IsolatePtr iso_ptr) { + Isolate* iso = static_cast(iso_ptr); + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + + m_object_template* ot = new m_object_template; + ot->iso = iso; + ot->ptr.Reset(iso, ObjectTemplate::New(iso)); + return static_cast(ot); +} + +void ObjectTemplateDispose(ObjectTemplatePtr ptr) { + delete static_cast(ptr); +} + +void ObjectTemplateSetValue(ObjectTemplatePtr ptr, + const char* name, + ValuePtr val_ptr, + int attributes) { + LOCAL_OBJECT_TEMPLATE(ptr); + + Local prop_name = + String::NewFromUtf8(iso, name, NewStringType::kNormal).ToLocalChecked(); + m_value* val = static_cast(val_ptr); + object_template->Set(prop_name, val->ptr.Get(iso), (PropertyAttribute)attributes); +} + +void ObjectTemplateSetObjectTemplate(ObjectTemplatePtr ptr, const char* name, ObjectTemplatePtr obj_ptr, int attributes) { + LOCAL_OBJECT_TEMPLATE(ptr); + + Local prop_name = + String::NewFromUtf8(iso, name, NewStringType::kNormal).ToLocalChecked(); + m_object_template* obj = static_cast(obj_ptr); + object_template->Set(prop_name, obj->ptr.Get(iso), (PropertyAttribute)attributes); +} + /********** Context **********/ -ContextPtr NewContext(IsolatePtr ptr) { - Isolate* iso = static_cast(ptr); +ContextPtr NewContext(IsolatePtr iso_ptr, + ObjectTemplatePtr global_template_ptr) { + Isolate* iso = static_cast(iso_ptr); Locker locker(iso); Isolate::Scope isolate_scope(iso); HandleScope handle_scope(iso); + Local global_template; + if (global_template_ptr != nullptr) { + m_object_template* ob = + static_cast(global_template_ptr); + global_template = ob->ptr.Get(iso); + } else { + global_template = ObjectTemplate::New(iso); + } + iso->SetCaptureStackTraceForUncaughtExceptions(true); m_ctx* ctx = new m_ctx; - ctx->ptr.Reset(iso, Context::New(iso)); + ctx->ptr.Reset(iso, Context::New(iso, nullptr, global_template)); ctx->iso = iso; return static_cast(ctx); } @@ -182,6 +251,7 @@ RtnValue RunScript(ContextPtr ctx_ptr, const char* source, const char* origin) { return rtn; } m_value* val = new m_value; + val->iso = iso; val->ctx_ptr = ctx; val->ptr.Reset(iso, Persistent(iso, result.ToLocalChecked())); @@ -206,13 +276,85 @@ void ContextDispose(ContextPtr ptr) { #define LOCAL_VALUE(ptr) \ m_value* val = static_cast(ptr); \ m_ctx* ctx = val->ctx_ptr; \ - Isolate* iso = ctx->iso; \ + Isolate* iso = val->iso; \ Locker locker(iso); \ Isolate::Scope isolate_scope(iso); \ HandleScope handle_scope(iso); \ Context::Scope context_scope(ctx->ptr.Get(iso)); \ Local value = val->ptr.Get(iso); +ValuePtr NewValueInteger(IsolatePtr iso_ptr, int32_t v) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ptr.Reset(iso, Persistent(iso, Integer::New(iso, v))); + return static_cast(val); +} + +ValuePtr NewValueIntegerFromUnsigned(IsolatePtr iso_ptr, uint32_t v) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ptr.Reset(iso, Persistent(iso, Integer::NewFromUnsigned(iso, v))); + return static_cast(val); +} + +ValuePtr NewValueString(IsolatePtr iso_ptr, const char* v) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ptr.Reset(iso, Persistent(iso, String::NewFromUtf8(iso, v).ToLocalChecked())); + return static_cast(val); +} + +ValuePtr NewValueBoolean(IsolatePtr iso_ptr, int v) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ptr.Reset(iso, Persistent(iso, Boolean::New(iso, v))); + return static_cast(val); +} + +ValuePtr NewValueNumber(IsolatePtr iso_ptr, double v) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ptr.Reset(iso, Persistent(iso, Number::New(iso, v))); + return static_cast(val); +} + +ValuePtr NewValueBigInt(IsolatePtr iso_ptr, int64_t v) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ptr.Reset(iso, Persistent(iso, BigInt::New(iso, v))); + return static_cast(val); +} + +ValuePtr NewValueBigIntFromUnsigned(IsolatePtr iso_ptr, uint64_t v) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ptr.Reset(iso, Persistent(iso, BigInt::NewFromUnsigned(iso, v))); + return static_cast(val); +} + +ValuePtr NewValueBigIntFromWords(IsolatePtr iso_ptr, int sign_bit, int word_count, const uint64_t* words) { + ISOLATE_SCOPE(iso_ptr); + m_value* val = new m_value; + val->iso = iso; + + // V8::BigInt::NewFromWords requires a context, which is different from all the other V8::Primitive types + // It seems that the implementation just gets the Isolate from the Context and nothing else, so this function + // should really only need the Isolate. + // We'll have to create a temp context to hold the Isolate to create the BigInt. + Local ctx = Context::New(iso); + + MaybeLocal bigint = BigInt::NewFromWords(ctx, sign_bit, word_count, words); + val->ptr.Reset(iso, Persistent(iso, bigint.ToLocalChecked())); + return static_cast(val); +} + void ValueDispose(ValuePtr ptr) { delete static_cast(ptr); } diff --git a/v8go.h b/v8go.h index d74f78a91..575b4e55d 100644 --- a/v8go.h +++ b/v8go.h @@ -10,6 +10,7 @@ extern "C" { typedef void* IsolatePtr; typedef void* ContextPtr; typedef void* ValuePtr; +typedef void* ObjectTemplatePtr; typedef struct { const char* msg; @@ -48,12 +49,29 @@ extern void IsolateDispose(IsolatePtr ptr); extern void IsolateTerminateExecution(IsolatePtr ptr); extern IsolateHStatistics IsolationGetHeapStatistics(IsolatePtr ptr); -extern ContextPtr NewContext(IsolatePtr prt); +extern ContextPtr NewContext(IsolatePtr iso_ptr, + ObjectTemplatePtr global_template_ptr); extern void ContextDispose(ContextPtr ptr); extern RtnValue RunScript(ContextPtr ctx_ptr, const char* source, const char* origin); +extern ObjectTemplatePtr NewObjectTemplate(IsolatePtr iso_ptr); +extern void ObjectTemplateDispose(ObjectTemplatePtr ptr); +extern void ObjectTemplateSetValue(ObjectTemplatePtr ptr, + const char* name, + ValuePtr val_ptr, + int attributes); +extern void ObjectTemplateSetObjectTemplate(ObjectTemplatePtr ptr, const char* name, ObjectTemplatePtr obj_ptr, int attributes); + +extern ValuePtr NewValueInteger(IsolatePtr iso_ptr, int32_t v); +extern ValuePtr NewValueIntegerFromUnsigned(IsolatePtr iso_ptr, uint32_t v); +extern ValuePtr NewValueString(IsolatePtr iso_ptr, const char* v); +extern ValuePtr NewValueBoolean(IsolatePtr iso_ptr, int v); +extern ValuePtr NewValueNumber(IsolatePtr iso_ptr, double v); +extern ValuePtr NewValueBigInt(IsolatePtr iso_ptr, int64_t v); +extern ValuePtr NewValueBigIntFromUnsigned(IsolatePtr iso_ptr, uint64_t v); +extern ValuePtr NewValueBigIntFromWords(IsolatePtr iso_ptr, int sign_bit, int word_count, const uint64_t* words); extern void ValueDispose(ValuePtr ptr); const char* ValueToString(ValuePtr ptr); const uint32_t* ValueToArrayIndex(ValuePtr ptr); diff --git a/value.go b/value.go index 9d17a5e09..cfa24449f 100644 --- a/value.go +++ b/value.go @@ -4,6 +4,7 @@ package v8go // #include "v8go.h" import "C" import ( + "errors" "fmt" "io" "math/big" @@ -14,6 +15,94 @@ import ( // Value represents all Javascript values and objects type Value struct { ptr C.ValuePtr + ctx *Context +} + +// NewValue will create a primitive value. Supported values types to create are: +// string -> V8::String +// int32 -> V8::Integer +// uint32 -> V8::Integer +// bool -> V8::Boolean +// int64 -> V8::BigInt +// uint64 -> V8::BigInt +// bool -> V8::Boolean +// *big.Int -> V8::BigInt +func NewValue(iso *Isolate, val interface{}) (*Value, error) { + if iso == nil { + return nil, errors.New("v8go: failed to create new Value: Isolate cannot be ") + } + + var rtnVal *Value + + switch v := val.(type) { + case string: + cstr := C.CString(v) + defer C.free(unsafe.Pointer(cstr)) + rtnVal = &Value{ + ptr: C.NewValueString(iso.ptr, cstr), + } + case int32: + rtnVal = &Value{ + ptr: C.NewValueInteger(iso.ptr, C.int(v)), + } + case uint32: + rtnVal = &Value{ + ptr: C.NewValueIntegerFromUnsigned(iso.ptr, C.uint(v)), + } + case int64: + rtnVal = &Value{ + ptr: C.NewValueBigInt(iso.ptr, C.int64_t(v)), + } + case uint64: + rtnVal = &Value{ + ptr: C.NewValueBigIntFromUnsigned(iso.ptr, C.uint64_t(v)), + } + case bool: + var b int + if v { + b = 1 + } + rtnVal = &Value{ + ptr: C.NewValueBoolean(iso.ptr, C.int(b)), + } + case float64: + rtnVal = &Value{ + ptr: C.NewValueNumber(iso.ptr, C.double(v)), + } + case *big.Int: + if v.IsInt64() { + rtnVal = &Value{ + ptr: C.NewValueBigInt(iso.ptr, C.int64_t(v.Int64())), + } + } + + if v.IsUint64() { + rtnVal = &Value{ + ptr: C.NewValueBigIntFromUnsigned(iso.ptr, C.uint64_t(v.Uint64())), + } + } + + var sign, count int + if v.Sign() == -1 { + sign = 1 + } + bits := v.Bits() + count = len(bits) + + words := make([]C.uint64_t, count, count) + for idx, word := range bits { + words[idx] = C.uint64_t(word) + } + + rtnVal = &Value{ + ptr: C.NewValueBigIntFromWords(iso.ptr, C.int(sign), C.int(count), &words[0]), + } + default: + return nil, fmt.Errorf("v8go: unsupported value type `%T`", v) + } + + runtime.SetFinalizer(rtnVal, (*Value).finalizer) + return rtnVal, nil } // Format implements the fmt.Formatter interface to provide a custom formatter @@ -176,7 +265,7 @@ func (v *Value) IsFunction() bool { // IsObject returns true if this value is an object. func (v *Value) IsObject() bool { - return C.ValueIsObject(v.ptr) != 0 + return v.ctx != nil && C.ValueIsObject(v.ptr) != 0 } // IsBigInt returns true if this value is a bigint. @@ -200,7 +289,7 @@ func (v *Value) IsNumber() bool { // IsExternal returns true if this value is an `External` object. func (v *Value) IsExternal() bool { // TODO(rogchap): requires test case - return C.ValueIsExternal(v.ptr) != 0 + return v.ctx != nil && C.ValueIsExternal(v.ptr) != 0 } // IsInt32 returns true if this value is a 32-bit signed integer.