diff --git a/CHANGELOG.md b/CHANGELOG.md index 48df81263..1b4aa28ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add support for "External" values, using cgo handles referencing internal values. This is a more Go friendly alternative where in V8, they are designed to contain C++ pointers. + ### Changed ## [v0.34.0] - 2025-10-07 diff --git a/example_new_value_external_handle_test.go b/example_new_value_external_handle_test.go new file mode 100644 index 000000000..1382bc195 --- /dev/null +++ b/example_new_value_external_handle_test.go @@ -0,0 +1,106 @@ +package v8go_test + +import ( + "fmt" + "runtime/cgo" + + v8 "github.com/tommie/v8go" +) + +type Calculator struct { + stack []int32 +} + +func (c *Calculator) Peek() int32 { + l := len(c.stack) + if l > 0 { + return c.stack[l-1] + } else { + return 0 + } +} +func (c *Calculator) Push(v int32) { c.stack = append(c.stack, v) } +func (c *Calculator) Add() { + l := len(c.stack) + if l < 2 { + panic("Not enough elements") + } + a := c.stack[l-1] + b := c.stack[l-2] + c.stack[l-2] = a + b + c.stack = c.stack[0 : l-1] +} + +// getInstance retrieves the go Calculator instance that is stored in an +// _internal field_. +func getInstance(info *v8.FunctionCallbackInfo) *Calculator { + var internalField *v8.Value = info.This().GetInternalField(0) + var handle cgo.Handle = internalField.ExternalHandle() + calculator, ok := handle.Value().(*Calculator) + if !ok { + panic("Not a calculator") + } + return calculator +} + +func CreateJSCalculator(iso *v8.Isolate) *v8.FunctionTemplate { + constructor := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value { + calculator := &Calculator{} + handle := cgo.NewHandle(calculator) + info.This().SetInternalField(0, + v8.NewValueExternalHandle(iso, handle)) + return nil + }) + + // Set the internal field count on the **instance template**. + instanceTemplate := constructor.InstanceTemplate() + instanceTemplate.SetInternalFieldCount(1) + + // Create the methods on the **prototype template**. + prototypeTemplate := constructor.PrototypeTemplate() + prototypeTemplate.Set("push", + v8.NewFunctionTemplate(iso, + func(info *v8.FunctionCallbackInfo) *v8.Value { + calculator := getInstance(info) + calculator.Push(info.Args()[0].Int32()) + return nil + })) + prototypeTemplate.Set("add", + v8.NewFunctionTemplate(iso, + func(info *v8.FunctionCallbackInfo) *v8.Value { + calculator := getInstance(info) + calculator.Add() + return nil + })) + prototypeTemplate.Set("peek", + v8.NewFunctionTemplateWithError(iso, + func(info *v8.FunctionCallbackInfo) (*v8.Value, error) { + calculator := getInstance(info) + return v8.NewValue(iso, calculator.Peek()) + })) + return constructor +} + +func ExampleNewValueExternalHandle() { + iso := v8.NewIsolate() + defer iso.Dispose() + + global := v8.NewObjectTemplate(iso) + global.Set("Calculator", CreateJSCalculator(iso)) + ctx := v8.NewContext(iso, global) + + defer ctx.Close() + v, err := ctx.RunScript(` + const calculator = new Calculator() + calculator.push(7) + calculator.push(35) + calculator.add() + calculator.peek()`, "") + if err != nil { + fmt.Println("Error running script", err.Error()) + return + } + fmt.Printf("The result is: %d\n", v.Integer()) + // Output: + // The result is: 42 +} diff --git a/object.go b/object.go index 93b051776..ca596713b 100644 --- a/object.go +++ b/object.go @@ -93,8 +93,25 @@ func (o *Object) SetIdx(idx uint32, val interface{}) error { return nil } -// SetInternalField sets the value of an internal field for an ObjectTemplate instance. -// Panics if the index isn't in the range set by (*ObjectTemplate).SetInternalFieldCount. +// SetInternalField sets the value of an internal field for an ObjectTemplate +// instance. The object must be created from an ObjectTemplate, either from a +// call to [ObjectTemplate.NewInstance], or as a new instance of a class. In +// which case the object template is the [FunctionTemplate.InstanceTemplate] +// of the constructor. +// +// Before setting the internal field, is is necessary to call +// [ObjectTemplate.SetInternalFieldCount] indicating how many internal fields +// exist. +// +// The function panics if the object is not created from an object template, or +// the index is outside the range of internal field count. +// +// Example use cases: +// - An object implementing a [javascript iterator] can store the current index being iterated. +// - An object that exposes a native Go object to script code can store a +// reference. See also [NewValueExternalHandle] for this case +// +// [javascript iterator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols func (o *Object) SetInternalField(idx uint32, val interface{}) error { value, err := coerceValue(o.ctx.iso, val) diff --git a/value.cc b/value.cc index 101cb3ce7..a5a336b28 100644 --- a/value.cc +++ b/value.cc @@ -1,13 +1,16 @@ #include "value.h" +#include #include #include "context.h" #include "deps/include/v8-context.h" #include "deps/include/v8-exception.h" +#include "deps/include/v8-external.h" #include "errors.h" #include "isolate-macros.h" #include "value-macros.h" +#include "value.h" #define ISOLATE_SCOPE_INTERNAL_CONTEXT(iso) \ ISOLATE_SCOPE(iso); \ @@ -214,6 +217,20 @@ ValuePtr NewValueError(IsolatePtr iso, return tracked_value(ctx, val); } +ValuePtr NewValueExternal(IsolatePtr iso, void* v) { + ISOLATE_SCOPE_INTERNAL_CONTEXT(iso); + m_value* val = new m_value; + val->id = 0; + val->iso = iso; + val->ctx = ctx; + val->ptr = Global(iso, External::New(iso, v)); + return tracked_value(ctx, val); +} + +ValuePtr NewValueExternalUintptr(IsolatePtr iso, uintptr_t v) { + return NewValueExternal(iso, (void*)v); +} + const uint32_t* ValueToArrayIndex(ValuePtr ptr) { LOCAL_VALUE(ptr); Local array_index; @@ -226,6 +243,15 @@ const uint32_t* ValueToArrayIndex(ValuePtr ptr) { return idx; } +void* ValueToExternal(ValuePtr ptr) { + LOCAL_VALUE(ptr); + return value.As()->Value(); +} + +uintptr_t ValueToExternalUintptr(ValuePtr ptr) { + return (uintptr_t)ValueToExternal(ptr); +} + int ValueToBoolean(ValuePtr ptr) { LOCAL_VALUE(ptr); return value->BooleanValue(iso); diff --git a/value.go b/value.go index 49fd84a83..7fc2b8e72 100644 --- a/value.go +++ b/value.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "math/big" + "runtime/cgo" "unsafe" ) @@ -139,6 +140,66 @@ func NewValue(iso *Isolate, val interface{}) (*Value, error) { return rtnVal, nil } +// NewValueExternal allows storing an [unsafe.Pointer] in a value. This function +// is discouraged, prefer using [NewValueExternalHandle] instead. This function +// exists primarily for code that already uses unsafe pointers. +// +// An unsafe pointer can be read using [Value.External] +func NewValueExternal(iso *Isolate, val unsafe.Pointer) *Value { + return &Value{ + ptr: C.NewValueExternal(iso.ptr, val), + } +} + +// NewValueExternalHandle can store a reference to a Go object as an "external" +// v8 value, by using a [cgo.Handle]. The primary use case is when exposing +// native Go objects to JavaScript code. +// +// Native external values can be stored as "internal fields" on v8 objects; +// using [Object.SetInternalField]. +// +// Warning: A cgo handle should be deleted through a call to [cgo.Handle.Delete] +// when you are done with the object. Unfortunately v8go doesn't yet support +// a callback when a JavaScript object is garbage collected. +// +// For a v8 context that is not short lived, this will cause a memory leak if +// new objects are created continuously. For a short-lived context, be sure to +// delete the cgo handles when the context is disposed. +func NewValueExternalHandle(iso *Isolate, val cgo.Handle) *Value { + return &Value{ + ptr: C.NewValueExternalUintptr(iso.ptr, C.uintptr_t(val)), + } +} + +// External retrieves an [unsafe.Pointer]. This value must have been created +// using [NewValueExternal]. +// +// The use of this pair of functions is discouraged. Prefer using +// [NewValueExternalHandle]/[Value.ExternalHandle] instead. +// +// This will return nil, if the value does not contain an external value. +func (v *Value) External() unsafe.Pointer { + if !v.IsExternal() { + return nil + } + return C.ValueToExternal(v.ptr) +} + +// ExternalHandle retrieves the [cgo.Handle] from a [Value] that was created +// using [NewValueExternalHandle]. +// +// This will return an zero handle if the value is not an external value. +// +// Warning, reading a value that was created using [NewValueExternal] is +// invalid, but will not be detected by v8go. Prefer using only handles if +// possible. +func (v *Value) ExternalHandle() cgo.Handle { + if !v.IsExternal() { + return 0 + } + return cgo.Handle(C.ValueToExternalUintptr(v.ptr)) +} + // Format implements the fmt.Formatter interface to provide a custom formatter // primarily to output the detail string (for debugging) with `%+v` verb. func (v *Value) Format(s fmt.State, verb rune) { @@ -338,7 +399,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 v.ctx != nil && C.ValueIsExternal(v.ptr) != 0 + return C.ValueIsExternal(v.ptr) != 0 } // IsInt32 returns true if this value is a 32-bit signed integer. diff --git a/value.h b/value.h index fcb439292..f4a71cfc1 100644 --- a/value.h +++ b/value.h @@ -54,6 +54,8 @@ typedef struct { void ValueRelease(ValuePtr ptr); void RtnStringRelease(RtnString rtnString); +extern void* ValueToExternal(ValuePtr prt); +extern uintptr_t ValueToExternalUintptr(ValuePtr prt); extern RtnString ValueToString(ValuePtr ptr); extern RtnString ValueTypeOf(ValuePtr ptr); const uint32_t* ValueToArrayIndex(ValuePtr ptr); @@ -138,7 +140,8 @@ extern RtnValue NewValueBigIntFromWords(IsolatePtr iso_ptr, extern ValuePtr NewValueError(IsolatePtr iso_ptr, ErrorTypeIndex idx, const char* message); - +extern ValuePtr NewValueExternal(IsolatePtr iso_ptr, void* v); +extern ValuePtr NewValueExternalUintptr(IsolatePtr iso_ptr, uintptr_t v); const char* ExceptionGetMessageString(ValuePtr ptr); extern void ObjectSet(ValuePtr ptr, const char* key, ValuePtr val_ptr); diff --git a/value_test.go b/value_test.go index f3cef3b5c..304c1c3cd 100644 --- a/value_test.go +++ b/value_test.go @@ -6,11 +6,13 @@ package v8go_test import ( "bytes" + "errors" "fmt" "math" "math/big" "reflect" "runtime" + "runtime/cgo" "testing" v8 "github.com/tommie/v8go" @@ -889,3 +891,55 @@ func TestValueTypeOf(t *testing.T) { t.Errorf("TypeOf(0.01): expected number, got %s", got) } } + +type InternalValue struct { + value int +} + +func (v *InternalValue) Increment() { v.value++ } + +func TestValueExternalHandle(t *testing.T) { + t.Parallel() + iso := v8.NewIsolate() + defer iso.Dispose() + + ctx := v8.NewContext(iso) + defer ctx.Close() + + internal := &InternalValue{} + handle := cgo.NewHandle(internal) + defer handle.Delete() + + ft := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value { + return nil + }) + ft.PrototypeTemplate(). + Set("increment", v8.NewFunctionTemplateWithError(iso, func(info *v8.FunctionCallbackInfo) (*v8.Value, error) { + handle := info.This().GetInternalField(0).ExternalHandle() + if handle == 0 { + return nil, errors.New("Invalid handle") + } + if instance, ok := handle.Value().(*InternalValue); ok { + instance.Increment() + return nil, nil + } else { + return nil, errors.New("Not the right type") + } + })) + ft.InstanceTemplate().SetInternalFieldCount(1) + instance, err := ft.InstanceTemplate().NewInstance(ctx) + if err != nil { + t.Fatal("Error creating instance") + } + value := v8.NewValueExternalHandle(iso, handle) + instance.SetInternalField(0, value) + + ctx.Global().Set("internal", instance) + _, err = ctx.RunScript("internal.increment(); internal.increment()", "") + if err != nil { + t.Error("Error running script", err) + } + if internal.value != 2 { + t.Errorf("Value not incremented. Expected 2, got %d", internal.value) + } +}