From 6305aabc78cd7b128d6df9493019b53fc50e7b8c Mon Sep 17 00:00:00 2001 From: Roger Chapman Date: Mon, 8 Feb 2021 13:57:12 +1100 Subject: [PATCH] Extend Value into the Object type (#74) * basic return of an Object from value * implement object set property * get the global object and get object instance from template * Object Get methods and locking in the API for this PR * fix C++ failing issues * basic tests for the Object methods * value tests and iso fix * update changelog * address comments from review --- CHANGELOG.md | 4 ++ context.go | 14 +++++ json.go | 6 +-- json_test.go | 8 +++ object.go | 103 +++++++++++++++++++++++++++++++++++ object_template.go | 10 ++++ object_template_test.go | 17 ++++++ object_test.go | 116 ++++++++++++++++++++++++++++++++++++++++ v8go.cc | 114 +++++++++++++++++++++++++++++++++++++++ v8go.h | 13 +++++ value.go | 29 ++++++++-- value_test.go | 13 +++++ 12 files changed, 441 insertions(+), 6 deletions(-) create mode 100644 object.go create mode 100644 object_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b8730320..3aee271c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 +- Value to Object type, including Get/Set/Has/Delete methods +- Get Global Object from the Context +- Convert a Object Template to an instance of an Object ### 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 - Deprecated `iso.Close()` in favor of `iso.Dispose()` to keep consistancy with the C++ API - Upgraded V8 to 8.8.278.14 +- Licence BSD 3-Clause (same as V8 and Go) ## [v0.4.0] - 2021-01-14 diff --git a/context.go b/context.go index 986c6d03..3d7d01ed 100644 --- a/context.go +++ b/context.go @@ -106,6 +106,20 @@ func (c *Context) RunScript(source string, origin string) (*Value, error) { return getValue(c, rtn), getError(rtn) } +// Global returns the global proxy object. +// Global proxy object is a thin wrapper whose prototype points to actual +// context's global object with the properties like Object, etc. This is +// done that way for security reasons. +// Please note that changes to global proxy object prototype most probably +// would break the VM — V8 expects only global object as a prototype of +// global proxy object. +func (c *Context) Global() *Object { + valPtr := C.ContextGlobal(c.ptr) + v := &Value{valPtr, c} + runtime.SetFinalizer(v, (*Value).finalizer) + return &Object{v} +} + // Close will dispose the context and free the memory. func (c *Context) Close() { c.finalizer() diff --git a/json.go b/json.go index 541dbf2a..a2ca5aff 100644 --- a/json.go +++ b/json.go @@ -26,8 +26,8 @@ func JSONParse(ctx *Context, str string) (*Value, error) { } // JSONStringify tries to stringify the JSON-serializable object value and returns it as string. -func JSONStringify(ctx *Context, val *Value) (string, error) { - if val == nil { +func JSONStringify(ctx *Context, val Valuer) (string, error) { + if val == nil || val.value() == nil { return "", errors.New("v8go: Value is required") } // If a nil context is passed we'll use the context/isolate that created the value. @@ -36,7 +36,7 @@ func JSONStringify(ctx *Context, val *Value) (string, error) { ctxPtr = ctx.ptr } - str := C.JSONStringify(ctxPtr, val.ptr) + str := C.JSONStringify(ctxPtr, val.value().ptr) defer C.free(unsafe.Pointer(str)) return C.GoString(str), nil } diff --git a/json_test.go b/json_test.go index dc8184fb..6fab3cd1 100644 --- a/json_test.go +++ b/json_test.go @@ -27,7 +27,15 @@ func TestJSONParse(t *testing.T) { if _, ok := err.(*v8go.JSError); !ok { t.Errorf("expected error to be of type JSError, got: %T", err) } +} + +func TestJSONStringify(t *testing.T) { + t.Parallel() + ctx, _ := v8go.NewContext() + if _, err := v8go.JSONStringify(ctx, nil); err == nil { + t.Error("expected error but got ") + } } func ExampleJSONParse() { diff --git a/object.go b/object.go new file mode 100644 index 00000000..4c1dc5c5 --- /dev/null +++ b/object.go @@ -0,0 +1,103 @@ +// 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 + +// #include +// #include "v8go.h" +import "C" +import ( + "errors" + "fmt" + "math/big" + "unsafe" +) + +// Object is a JavaScript object (ECMA-262, 4.3.3) +type Object struct { + *Value +} + +// Set will set a property on the Object to a given value. +// Supports all value types, eg: Object, Array, Date, Set, Map etc +// 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 *Object) Set(key string, val interface{}) error { + if len(key) == 0 { + return errors.New("v8go: You must provide a valid property key") + } + return set(o, key, 0, val) +} + +// Set will set a given index on the Object to a given value. +// Supports all value types, eg: Object, Array, Date, Set, Map etc +// 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 *Object) SetIdx(idx uint32, val interface{}) error { + return set(o, "", idx, val) +} + +func set(o *Object, key string, idx uint32, val interface{}) error { + var value *Value + switch v := val.(type) { + case string, int32, uint32, int64, uint64, float64, bool, *big.Int: + // ignoring error as code cannot reach the error state as we are already + // validating the new value types in this case statement + value, _ = NewValue(o.ctx.iso, v) + case Valuer: + value = v.value() + default: + return fmt.Errorf("v8go: unsupported object property type `%T`", v) + } + + if len(key) > 0 { + ckey := C.CString(key) + defer C.free(unsafe.Pointer(ckey)) + C.ObjectSet(o.ptr, ckey, value.ptr) + return nil + } + + C.ObjectSetIdx(o.ptr, C.uint32_t(idx), value.ptr) + return nil +} + +// Get tries to get a Value for a given Object property key. +func (o *Object) Get(key string) (*Value, error) { + ckey := C.CString(key) + defer C.free(unsafe.Pointer(ckey)) + + rtn := C.ObjectGet(o.ptr, ckey) + return getValue(o.ctx, rtn), getError(rtn) +} + +// GetIdx tries to get a Value at a give Object index. +func (o *Object) GetIdx(idx uint32) (*Value, error) { + rtn := C.ObjectGetIdx(o.ptr, C.uint32_t(idx)) + return getValue(o.ctx, rtn), getError(rtn) +} + +// Has calls the abstract operation HasProperty(O, P) described in ECMA-262, 7.3.10. +// Returns true, if the object has the property, either own or on the prototype chain. +func (o *Object) Has(key string) bool { + ckey := C.CString(key) + defer C.free(unsafe.Pointer(ckey)) + return C.ObjectHas(o.ptr, ckey) != 0 +} + +// HasIdx returns true if the object has a value at the given index. +func (o *Object) HasIdx(idx uint32) bool { + return C.ObjectHasIdx(o.ptr, C.uint32_t(idx)) != 0 +} + +// Delete returns true if successful in deleting a named property on the object. +func (o *Object) Delete(key string) bool { + ckey := C.CString(key) + defer C.free(unsafe.Pointer(ckey)) + return C.ObjectDelete(o.ptr, ckey) != 0 +} + +// DeleteIdx returns true if successful in deleting a value at a given index of the object. +func (o *Object) DeleteIdx(idx uint32) bool { + return C.ObjectDeleteIdx(o.ptr, C.uint32_t(idx)) != 0 +} diff --git a/object_template.go b/object_template.go index ab5904fa..a55ed7ac 100644 --- a/object_template.go +++ b/object_template.go @@ -49,6 +49,16 @@ func NewObjectTemplate(iso *Isolate) (*ObjectTemplate, error) { return &ObjectTemplate{tmpl}, nil } +// NewInstance creates a new Object based on the template. +func (o *ObjectTemplate) NewInstance(ctx *Context) (*Object, error) { + if ctx == nil { + return nil, errors.New("v8go: Context cannot be ") + } + + valPtr := C.ObjectTemplateNewInstance(o.ptr, ctx.ptr) + return &Object{&Value{valPtr, ctx}}, nil +} + func (o *ObjectTemplate) apply(opts *contextOptions) { opts.gTmpl = o } diff --git a/object_template_test.go b/object_template_test.go index 98135b4c..a34299bb 100644 --- a/object_template_test.go +++ b/object_template_test.go @@ -117,3 +117,20 @@ func TestGlobalObjectTemplate(t *testing.T) { }) } } + +func TestObjectTemplateNewInstance(t *testing.T) { + t.Parallel() + iso, _ := v8go.NewIsolate() + tmpl, _ := v8go.NewObjectTemplate(iso) + if _, err := tmpl.NewInstance(nil); err == nil { + t.Error("expected error but got ") + } + + tmpl.Set("foo", "bar") + ctx, _ := v8go.NewContext(iso) + obj, _ := tmpl.NewInstance(ctx) + if foo, _ := obj.Get("foo"); foo.String() != "bar" { + t.Errorf("unexpected value for object property: %v", foo) + } + +} diff --git a/object_test.go b/object_test.go new file mode 100644 index 00000000..875d783b --- /dev/null +++ b/object_test.go @@ -0,0 +1,116 @@ +// 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 ( + "fmt" + "testing" + + "rogchap.com/v8go" +) + +func TestObjectSet(t *testing.T) { + t.Parallel() + + ctx, _ := v8go.NewContext() + val, _ := ctx.RunScript("const foo = {}; foo", "") + obj, _ := val.AsObject() + obj.Set("bar", "baz") + baz, _ := ctx.RunScript("foo.bar", "") + if baz.String() != "baz" { + t.Errorf("unexpected value: %q", baz) + } + if err := obj.Set("", nil); err == nil { + t.Error("expected error but got ") + } + if err := obj.Set("a", 0); err == nil { + t.Error("expected error but got ") + } + obj.SetIdx(10, "ten") + if ten, _ := ctx.RunScript("foo[10]", ""); ten.String() != "ten" { + t.Errorf("unexpected value: %q", ten) + } +} + +func TestObjectGet(t *testing.T) { + t.Parallel() + + ctx, _ := v8go.NewContext() + val, _ := ctx.RunScript("const foo = { bar: 'baz'}; foo", "") + obj, _ := val.AsObject() + if bar, _ := obj.Get("bar"); bar.String() != "baz" { + t.Errorf("unexpected value: %q", bar) + } + if baz, _ := obj.Get("baz"); !baz.IsUndefined() { + t.Errorf("unexpected value: %q", baz) + } + ctx.RunScript("foo[5] = 5", "") + if five, _ := obj.GetIdx(5); five.Integer() != 5 { + t.Errorf("unexpected value: %q", five) + } + if u, _ := obj.GetIdx(55); !u.IsUndefined() { + t.Errorf("unexpected value: %q", u) + } +} + +func TestObjectHas(t *testing.T) { + t.Parallel() + + ctx, _ := v8go.NewContext() + val, _ := ctx.RunScript("const foo = {a: 1, '2': 2}; foo", "") + obj, _ := val.AsObject() + if !obj.Has("a") { + t.Error("expected true, got false") + } + if obj.Has("c") { + t.Error("expected false, got true") + } + if !obj.HasIdx(2) { + t.Error("expected true, got false") + } + if obj.HasIdx(1) { + t.Error("expected false, got true") + } +} + +func TestObjectDelete(t *testing.T) { + t.Parallel() + + ctx, _ := v8go.NewContext() + val, _ := ctx.RunScript("const foo = { bar: 'baz', '2': 2}; foo", "") + obj, _ := val.AsObject() + if !obj.Has("bar") { + t.Error("expected property to exist") + } + if !obj.Delete("bar") { + t.Error("expected delete to return true, got false") + } + if obj.Has("bar") { + t.Error("expected property to be deleted") + } + if !obj.DeleteIdx(2) { + t.Error("expected delete to return true, got false") + } + +} + +func ExampleObject_global() { + iso, _ := v8go.NewIsolate() + ctx, _ := v8go.NewContext(iso) + global := ctx.Global() + + console, _ := v8go.NewObjectTemplate(iso) + logfn, _ := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + fmt.Println(info.Args()[0]) + return nil + }) + console.Set("log", logfn) + consoleObj, _ := console.NewInstance(ctx) + + global.Set("console", consoleObj) + ctx.RunScript("console.log('foo')", "") + // Output: + // foo +} diff --git a/v8go.cc b/v8go.cc index eb50c8c7..6478e1ad 100644 --- a/v8go.cc +++ b/v8go.cc @@ -220,6 +220,23 @@ TemplatePtr NewObjectTemplate(IsolatePtr iso_ptr) { ot->ptr.Reset(iso, ObjectTemplate::New(iso)); return static_cast(ot); } + +ValuePtr ObjectTemplateNewInstance(TemplatePtr ptr, ContextPtr ctx_ptr) { + LOCAL_TEMPLATE(ptr); + m_ctx* ctx = static_cast(ctx_ptr); + Local local_ctx = ctx->ptr.Get(iso); + Context::Scope context_scope(local_ctx); + + Local obj_tmpl = tmpl.As(); + MaybeLocal obj = obj_tmpl->NewInstance(local_ctx); + + m_value* val = new m_value; + val->iso = iso; + val->ctx.Reset(iso, local_ctx); + val->ptr.Reset(iso, Persistent(iso, obj.ToLocalChecked())); + return static_cast(val); +} + /********** FunctionTemplate **********/ static void FunctionTemplateCallback(const FunctionCallbackInfo& info) { @@ -416,6 +433,15 @@ const char* JSONStringify(ContextPtr ctx_ptr, ValuePtr val_ptr) { return CopyString(json); } +ValuePtr ContextGlobal(ContextPtr ctx_ptr) { + LOCAL_CONTEXT(ctx_ptr); + m_value* val = new m_value; + val->iso = iso; + val->ctx.Reset(iso, local_ctx); + val->ptr.Reset(iso, Persistent(iso, local_ctx->Global())); + return static_cast(val); +} + /********** Value **********/ #define LOCAL_VALUE(ptr) \ @@ -424,6 +450,7 @@ const char* JSONStringify(ContextPtr ctx_ptr, ValuePtr val_ptr) { Locker locker(iso); \ Isolate::Scope isolate_scope(iso); \ HandleScope handle_scope(iso); \ + TryCatch try_catch(iso); \ Local local_ctx = val->ctx.Get(iso); \ if (local_ctx.IsEmpty()) { \ m_ctx* ctx = static_cast(iso->GetData(0)); \ @@ -576,6 +603,15 @@ ValueBigInt ValueToBigInt(ValuePtr ptr) { return rtn; } +ValuePtr ValueToObject(ValuePtr ptr) { + LOCAL_VALUE(ptr); + m_value* new_val = new m_value; + new_val->iso = iso; + new_val->ctx.Reset(iso, local_ctx); + new_val->ptr.Reset(iso, Persistent(iso, value->ToObject(local_ctx).ToLocalChecked())); + return static_cast(new_val); +} + int ValueIsUndefined(ValuePtr ptr) { LOCAL_VALUE(ptr); return value->IsUndefined(); @@ -846,6 +882,84 @@ int ValueIsModuleNamespaceObject(ValuePtr ptr) { return value->IsModuleNamespaceObject(); } +/********** Object **********/ + +#define LOCAL_OBJECT(ptr) \ + LOCAL_VALUE(ptr) \ + Local obj = value.As() \ + +void ObjectSet(ValuePtr ptr, const char* key, ValuePtr val_ptr) { + LOCAL_OBJECT(ptr); + Local key_val = String::NewFromUtf8(iso, key, NewStringType::kNormal).ToLocalChecked(); + m_value* prop_val = static_cast(val_ptr); + obj->Set(local_ctx, key_val, prop_val->ptr.Get(iso)).Check(); +} + +void ObjectSetIdx(ValuePtr ptr, uint32_t idx, ValuePtr val_ptr) { + LOCAL_OBJECT(ptr); + m_value* prop_val = static_cast(val_ptr); + obj->Set(local_ctx, idx, prop_val->ptr.Get(iso)).Check(); +} + +RtnValue ObjectGet(ValuePtr ptr, const char* key) { + LOCAL_OBJECT(ptr); + RtnValue rtn = {nullptr, nullptr}; + + Local key_val = String::NewFromUtf8(iso, key, NewStringType::kNormal).ToLocalChecked(); + MaybeLocal result = obj->Get(local_ctx, key_val); + if (result.IsEmpty()) { + rtn.error = ExceptionError(try_catch, iso, local_ctx); + return rtn; + } + m_value* new_val = new m_value; + new_val->iso = iso; + new_val->ctx.Reset(iso, local_ctx); + new_val->ptr.Reset(iso, Persistent(iso, result.ToLocalChecked())); + + rtn.value = static_cast(new_val); + return rtn; +} + +RtnValue ObjectGetIdx(ValuePtr ptr, uint32_t idx) { + LOCAL_OBJECT(ptr); + RtnValue rtn = {nullptr, nullptr}; + + MaybeLocal result = obj->Get(local_ctx, idx); + if (result.IsEmpty()) { + rtn.error = ExceptionError(try_catch, iso, local_ctx); + return rtn; + } + m_value* new_val = new m_value; + new_val->iso = iso; + new_val->ctx.Reset(iso, local_ctx); + new_val->ptr.Reset(iso, Persistent(iso, result.ToLocalChecked())); + + rtn.value = static_cast(new_val); + return rtn; +} + +int ObjectHas(ValuePtr ptr, const char* key) { + LOCAL_OBJECT(ptr); + Local key_val = String::NewFromUtf8(iso, key, NewStringType::kNormal).ToLocalChecked(); + return obj->Has(local_ctx, key_val).ToChecked(); +} + +int ObjectHasIdx(ValuePtr ptr, uint32_t idx) { + LOCAL_OBJECT(ptr); + return obj->Has(local_ctx, idx).ToChecked(); +} + +int ObjectDelete(ValuePtr ptr, const char* key) { + LOCAL_OBJECT(ptr); + Local key_val = String::NewFromUtf8(iso, key, NewStringType::kNormal).ToLocalChecked(); + return obj->Delete(local_ctx, key_val).ToChecked(); +} + +int ObjectDeleteIdx(ValuePtr ptr, uint32_t idx) { + LOCAL_OBJECT(ptr); + return obj->Delete(local_ctx, idx).ToChecked(); +} + /********** Version **********/ const char* Version() { diff --git a/v8go.h b/v8go.h index dc0fb279..5db7342a 100644 --- a/v8go.h +++ b/v8go.h @@ -62,6 +62,7 @@ extern RtnValue RunScript(ContextPtr ctx_ptr, const char* origin); extern RtnValue JSONParse(ContextPtr ctx_ptr, const char* str); const char* JSONStringify(ContextPtr ctx_ptr, ValuePtr val_ptr); +extern ValuePtr ContextGlobal(ContextPtr ctx_ptr); extern void TemplateFree(TemplatePtr ptr); extern void TemplateSetValue(TemplatePtr ptr, @@ -74,6 +75,8 @@ extern void TemplateSetTemplate(TemplatePtr ptr, int attributes); extern TemplatePtr NewObjectTemplate(IsolatePtr iso_ptr); +extern ValuePtr ObjectTemplateNewInstance(TemplatePtr ptr, ContextPtr ctx_ptr); + extern TemplatePtr NewFunctionTemplate(IsolatePtr iso_ptr, int callback_ref); extern ValuePtr NewValueInteger(IsolatePtr iso_ptr, int32_t v); @@ -97,6 +100,7 @@ double ValueToNumber(ValuePtr ptr); const char* ValueToDetailString(ValuePtr ptr); uint32_t ValueToUint32(ValuePtr ptr); extern ValueBigInt ValueToBigInt(ValuePtr ptr); +extern ValuePtr ValueToObject(ValuePtr ptr); int ValueIsUndefined(ValuePtr ptr); int ValueIsNull(ValuePtr ptr); int ValueIsNullOrUndefined(ValuePtr ptr); @@ -152,6 +156,15 @@ int ValueIsProxy(ValuePtr ptr); int ValueIsWasmModuleObject(ValuePtr ptr); int ValueIsModuleNamespaceObject(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 RtnValue ObjectGet(ValuePtr ptr, const char* key); +extern RtnValue ObjectGetIdx(ValuePtr ptr, uint32_t idx); +int ObjectHas(ValuePtr ptr, const char* key); +int ObjectHasIdx(ValuePtr ptr, uint32_t idx); +int ObjectDelete(ValuePtr ptr, const char* key); +int ObjectDeleteIdx(ValuePtr ptr, uint32_t idx); + const char* Version(); #ifdef __cplusplus diff --git a/value.go b/value.go index a6e6ffd3..c31c2599 100644 --- a/value.go +++ b/value.go @@ -22,6 +22,16 @@ type Value struct { ctx *Context } +// Valuer is an interface that reperesents anything that extends from a Value +// eg. Object, Array, Date etc +type Valuer interface { + value() *Value +} + +func (v *Value) value() *Value { + return v +} + // NewValue will create a primitive value. Supported values types to create are: // string -> V8::String // int32 -> V8::Integer @@ -194,9 +204,12 @@ func (v *Value) Number() float64 { } // Object perform the equivalent of Object(value) in JS. -func (v *Value) object() struct{} { // *Object - //TODO(rogchap): implement and export public API - panic("not implemented") +// To just cast this value as an Object use AsObject() instead. +func (v *Value) Object() *Object { + ptr := C.ValueToObject(v.ptr) + val := &Value{ptr, v.ctx} + runtime.SetFinalizer(val, (*Value).finalizer) + return &Object{val} } // String perform the equivalent of `String(value)` in JS. Primitive values @@ -501,6 +514,16 @@ func (v *Value) IsModuleNamespaceObject() bool { return C.ValueIsModuleNamespaceObject(v.ptr) != 0 } +// AsObject will cast the value to the Object type. If the value is not an Object +// then an error is returned. Use `value.Object()` to do the JS equivalent of `Object(value)`. +func (v *Value) AsObject() (*Object, error) { + if !v.IsObject() { + return nil, errors.New("value: unable to cast to Object; value is not an Object") + } + + return &Object{v}, nil +} + func (v *Value) finalizer() { C.ValueFree(v.ptr) v.ptr = nil diff --git a/value_test.go b/value_test.go index 43d2294c..ae966a8c 100644 --- a/value_test.go +++ b/value_test.go @@ -351,6 +351,19 @@ func TestValueBigInt(t *testing.T) { } }) } +} + +func TestValueObject(t *testing.T) { + t.Parallel() + + ctx, _ := v8go.NewContext() + val, _ := ctx.RunScript("1", "") + if _, err := val.AsObject(); err == nil { + t.Error("Expected error but got ") + } + if obj := val.Object(); obj.String() != "1" { + t.Errorf("unexpected object value: %v", obj) + } }