Skip to content

Commit

Permalink
Support creating a global ObjectTemplate into a context (rogchap#64)
Browse files Browse the repository at this point in the history
* Support global Object Template into a context

* add all supported primitive values to Object Template

* add some examples to Context and document new API

* don't convert to long types

* add finalizer to new values created

* update API for ObjectTemplate Set and other minor fixes
  • Loading branch information
rogchap authored Jan 25, 2021
1 parent f47296a commit b35f871
Show file tree
Hide file tree
Showing 9 changed files with 542 additions and 22 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 33 additions & 15 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
}
Expand Down
36 changes: 36 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 6 additions & 0 deletions isolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -80,3 +82,7 @@ func (i *Isolate) finalizer() {
func (i *Isolate) Close() {
i.finalizer()
}

func (i *Isolate) apply(opts *contextOptions) {
opts.iso = i
}
93 changes: 93 additions & 0 deletions object_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package v8go

// #include <stdlib.h>
// #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 <nil>")
}
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)
}
112 changes: 112 additions & 0 deletions object_template_test.go
Original file line number Diff line number Diff line change
@@ -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 <nil>")
}
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)
})
}
}
Loading

0 comments on commit b35f871

Please sign in to comment.