Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions example_new_value_external_handle_test.go

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest moving this into example/ (and skipping the example_ prefix). The root is getting pretty large.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on the "pretty large" - but does it work the same? Is the example embedded in the documentation the same way?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right. No, that won't work.

This example feels large enough that it might make sense to keep it separate anyway. If it could be made into 30 lines, I think having it next to the function is reasonable (and then we wouldn't have one file per example case).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "one file per example" is a special case where the file is included in its entirety which I chose for this as the Calculator is a crucial part of the example - it's about how to create a JS wrapper object on top of a native type - a crucial part for my own use case.

Of course, a different example could be created using an internal go type, e.g., create a stack, where the value is a *[]v8.Value or something - then the example function could stand on it's own. The calculator was just the use case I came up with when writing the example.

The name of the example function dictates where in the docs it's included - right now it's associated with NewValueExternalHandle - but maybe it shouldn't be assiciated with this function specifically, as the use case requires the use of Set/GetInternalValue and InternalValueCount - and how to create the values in the constructor.

E.g., this is from my own godoc server running

image

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, looking at the example, I notice something that should probably be handled differently

    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")
    }

If the field can't be looked up, it should return a TypeError (I guess that's the appropriate return value). An example JS code that would be affected

const notACalculator = { __proto__: Calculator.prototype }
notACalculator.push(1)

As it's setting the prototype, it would call the go callback, but as this wasn't created through the constructor, it wouldn't have the internal field. I experienced a similar bug in my own code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g., in Firefox:
image

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of the example function dictates where in the docs it's included - right now it's associated with NewValueExternalHandle - but maybe it shouldn't be assiciated with this function specifically, as the use case requires the use of Set/GetInternalValue and InternalValueCount - and how to create the values in the constructor.

Yeah, I think the example is too long for being tied to a single function. It feels unfocused, even if the example is good. Perhaps flipping it so the relevant function invocation is first would help shake that feeling.

Original file line number Diff line number Diff line change
@@ -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()

@stroiman stroiman May 20, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handle needs to be deleted!

Specifically as this serves as a public example - it should be clear that handles need releasing.

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
}
21 changes: 19 additions & 2 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
26 changes: 26 additions & 0 deletions value.cc
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#include "value.h"

#include <stdint.h>
#include <stdlib.h>

#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); \
Expand Down Expand Up @@ -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<Value>(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<Uint32> array_index;
Expand All @@ -226,6 +243,15 @@ const uint32_t* ValueToArrayIndex(ValuePtr ptr) {
return idx;
}

void* ValueToExternal(ValuePtr ptr) {
LOCAL_VALUE(ptr);
return value.As<External>()->Value();
}

uintptr_t ValueToExternalUintptr(ValuePtr ptr) {
return (uintptr_t)ValueToExternal(ptr);
}

int ValueToBoolean(ValuePtr ptr) {
LOCAL_VALUE(ptr);
return value->BooleanValue(iso);
Expand Down
63 changes: 62 additions & 1 deletion value.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"fmt"
"io"
"math/big"
"runtime/cgo"
"unsafe"
)

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion value.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
54 changes: 54 additions & 0 deletions value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ package v8go_test

import (
"bytes"
"errors"
"fmt"
"math"
"math/big"
"reflect"
"runtime"
"runtime/cgo"
"testing"

v8 "github.com/tommie/v8go"
Expand Down Expand Up @@ -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)
}
}
Loading