Skip to content

Commit

Permalink
Adding ArrayBuffer support along the same lines as Object and Functio…
Browse files Browse the repository at this point in the history
…n. This commit allows Go code to directly access the raw ArrayBuffer bytes. A next step could be to add typed View objects to mirror those found in JS. Feedback welcome.
  • Loading branch information
teuget committed Jun 16, 2021
1 parent 788b16f commit 76db6dd
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 27 deletions.
39 changes: 15 additions & 24 deletions array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,9 @@ import (
"testing"
)

type NativeObject interface {
GetReverseUint8ArrayFunctionCallback() FunctionCallback
}

type nativeObject struct {
}

func NewNativeObject() NativeObject {
return &nativeObject{}
}
type uint8ArrayTester struct{}

func (nto *nativeObject) GetReverseUint8ArrayFunctionCallback() FunctionCallback {
func (u *uint8ArrayTester) GetReverseUint8ArrayFunctionCallback() FunctionCallback {
return func(info *FunctionCallbackInfo) *Value {
iso, err := info.Context().Isolate()
if err != nil {
Expand All @@ -45,54 +36,54 @@ func (nto *nativeObject) GetReverseUint8ArrayFunctionCallback() FunctionCallback
}
}

func injectNativeObject(ctx *Context) error {
func injectUint8ArrayTester(ctx *Context) error {
if ctx == nil {
return errors.New("injectNativeObject: ctx is required")
return errors.New("injectUint8ArrayTester: ctx is required")
}

iso, err := ctx.Isolate()
if err != nil {
return fmt.Errorf("injectNativeObject: %v", err)
return fmt.Errorf("injectUint8ArrayTester: %v", err)
}

c := NewNativeObject()
c := &uint8ArrayTester{}

con, err := NewObjectTemplate(iso)
if err != nil {
return fmt.Errorf("injectNativeObject: %v", err)
return fmt.Errorf("injectUint8ArrayTester: %v", err)
}

reverseFn, err := NewFunctionTemplate(iso, c.GetReverseUint8ArrayFunctionCallback())
if err != nil {
return fmt.Errorf("injectNativeObject: %v", err)
return fmt.Errorf("injectUint8ArrayTester: %v", err)
}

if err := con.Set("reverseUint8Array", reverseFn, ReadOnly); err != nil {
return fmt.Errorf("injectNativeObject: %v", err)
return fmt.Errorf("injectUint8ArrayTester: %v", err)
}

nativeObj, err := con.NewInstance(ctx)
if err != nil {
return fmt.Errorf("injectNativeObject: %v", err)
return fmt.Errorf("injectUint8ArrayTester: %v", err)
}

global := ctx.Global()

if err := global.Set("native", nativeObj); err != nil {
return fmt.Errorf("injectNativeObject: %v", err)
return fmt.Errorf("injectUint8ArrayTester: %v", err)
}

return nil
}

// Test that a script can call a go function to reverse a []uint8 array
func TestNativeUint8Array(t *testing.T) {
func TestUint8Array(t *testing.T) {
t.Parallel()

iso, _ := NewIsolate()
ctx, _ := NewContext(iso)

if err := injectNativeObject(ctx); err != nil {
if err := injectUint8ArrayTester(ctx); err != nil {
t.Error(err)
}

Expand All @@ -116,13 +107,13 @@ func TestNativeUint8Array(t *testing.T) {
}

// Test that a native go function can throw exceptions that make it back to the script runner
func TestNativeUint8ArrayException(t *testing.T) {
func TestUint8ArrayException(t *testing.T) {
t.Parallel()

iso, _ := NewIsolate()
ctx, _ := NewContext(iso)

if err := injectNativeObject(ctx); err != nil {
if err := injectUint8ArrayTester(ctx); err != nil {
t.Error(err)
}

Expand Down
30 changes: 30 additions & 0 deletions arraybuffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package v8go

// #include <stdlib.h>
// #include "v8go.h"
import "C"
import "unsafe"

type ArrayBuffer struct {
*Value
}

func NewArrayBuffer(ctx *Context, len int64) *ArrayBuffer {
return &ArrayBuffer{&Value{C.NewArrayBuffer(ctx.iso.ptr, C.size_t(len)), ctx}}
}

func (ab *ArrayBuffer) ByteLength() int64 {
return int64(C.ArrayBufferByteLength(ab.ptr))
}

func (ab *ArrayBuffer) GetBytes() []uint8 {
len := C.ArrayBufferByteLength(ab.ptr)
cbytes := unsafe.Pointer(C.GetArrayBufferBytes(ab.ptr)) // points into BackingStore
return C.GoBytes(cbytes, C.int(len))
}

func (ab *ArrayBuffer) PutBytes(bytes []uint8) {
cbytes := C.CBytes(bytes) //FIXME is there really no way to avoid this malloc+memcpy?
defer C.free(cbytes)
C.PutArrayBufferBytes(ab.ptr, 0, (*C.char)(cbytes), C.size_t(len(bytes)))
}
173 changes: 173 additions & 0 deletions arraybuffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package v8go

import (
"errors"
"fmt"
"log"
"testing"
)

type arrayBufferTester struct{}

func (a *arrayBufferTester) GetReverseArrayBufferFunctionCallback() FunctionCallback {
return func(info *FunctionCallbackInfo) *Value {
iso, err := info.Context().Isolate()
if err != nil {
log.Fatalf("Could not get isolate from context: %v\n", err)
}
args := info.Args()
if len(args) != 1 {
return iso.ThrowException("Function ReverseArrayBuffer expects 1 parameter")
}
if !args[0].IsArrayBuffer() {
return iso.ThrowException("Function ReverseArrayBuffer expects ArrayBuffer parameter")
}
ab := args[0].ArrayBuffer() // "cast" to ArrayBuffer
length := int(ab.ByteLength())
bytes := ab.GetBytes() // get a copy of the bytes from the ArrayBuffer
reversed := make([]uint8, length)
for i := 0; i < length; i++ {
reversed[i] = bytes[length-i-1]
}
ab.PutBytes(reversed) // update the bytes in the ArrayBuffer (length must match!)
return nil
}
}

func (a *arrayBufferTester) GetCreateArrayBufferFunctionCallback() FunctionCallback {
return func(info *FunctionCallbackInfo) *Value {
iso, err := info.Context().Isolate()
if err != nil {
log.Fatalf("Could not get isolate from context: %v\n", err)
}
args := info.Args()
if len(args) != 1 {
return iso.ThrowException("Function CreateArrayBuffer expects 1 parameter")
}
if !args[0].IsInt32() {
return iso.ThrowException("Function CreateArrayBuffer expects Int32 parameter")
}
length := args[0].Int32()
ab := NewArrayBuffer(info.Context(), int64(length)) // create ArrayBuffer object of given length
bytes := make([]uint8, length)
for i := uint8(0); i < uint8(length); i++ {
bytes[i] = i
}
ab.PutBytes(bytes) // copy these bytes into it. Caller is responsible for avoiding overruns!
return ab.Value // return the ArrayBuffer to javascript
}
}

func injectArrayBufferTester(ctx *Context, funcName string, funcCb FunctionCallback) error {
if ctx == nil {
return errors.New("injectArrayBufferTester: ctx is required")
}

iso, err := ctx.Isolate()
if err != nil {
return fmt.Errorf("injectArrayBufferTester: %v", err)
}

con, err := NewObjectTemplate(iso)
if err != nil {
return fmt.Errorf("injectArrayBufferTester: %v", err)
}

funcTempl, err := NewFunctionTemplate(iso, funcCb)
if err != nil {
return fmt.Errorf("injectArrayBufferTester: %v", err)
}

if err := con.Set(funcName, funcTempl, ReadOnly); err != nil {
return fmt.Errorf("injectArrayBufferTester: %v", err)
}

nativeObj, err := con.NewInstance(ctx)
if err != nil {
return fmt.Errorf("injectArrayBufferTester: %v", err)
}

global := ctx.Global()

if err := global.Set("native", nativeObj); err != nil {
return fmt.Errorf("injectArrayBufferTester: %v", err)
}

return nil
}

// Test that a script can call a go function to reverse an ArrayBuffer.
// The function reverses the ArrayBuffer in-place, i.e. this is a call-by-reference.
func TestModifyArrayBuffer(t *testing.T) {
t.Parallel()

iso, _ := NewIsolate()
ctx, _ := NewContext(iso)
c := &arrayBufferTester{}

if err := injectArrayBufferTester(ctx, "reverseArrayBuffer", c.GetReverseArrayBufferFunctionCallback()); err != nil {
t.Error(err)
}

js := `
let ab = new ArrayBuffer(10);
let view = new Uint8Array(ab);
for (let i = 0; i < 10; i++) view[i] = i;
native.reverseArrayBuffer(ab);
ab;
`

if val, err := ctx.RunScript(js, ""); err != nil {
t.Error(err)
} else {
if !val.IsArrayBuffer() {
t.Errorf("Expected ArrayBuffer return value")
}
ab := val.ArrayBuffer()
if ab.ByteLength() != 10 {
t.Errorf("Got wrong ArrayBuffer length %d, expected 10", ab.ByteLength())
}
bytes := ab.GetBytes()
fmt.Printf("Got reversed ArrayBuffer from script: %v\n", bytes)
for i := int64(0); i < ab.ByteLength(); i++ {
if bytes[i] != uint8(10-i-1) {
t.Errorf("Incorrect byte at index %d (whole array: %v)", i, ab)
}
}
}
}

func TestCreateArrayBuffer(t *testing.T) {
t.Parallel()

iso, _ := NewIsolate()
ctx, _ := NewContext(iso)
c := &arrayBufferTester{}

if err := injectArrayBufferTester(ctx, "createArrayBuffer", c.GetCreateArrayBufferFunctionCallback()); err != nil {
t.Error(err)
}

js := `
native.createArrayBuffer(16);
`

if val, err := ctx.RunScript(js, ""); err != nil {
t.Error(err)
} else {
if !val.IsArrayBuffer() {
t.Errorf("Expected ArrayBuffer return value")
}
ab := val.ArrayBuffer()
if ab.ByteLength() != 16 {
t.Errorf("Got wrong ArrayBuffer length %d, expected 16", ab.ByteLength())
}
bytes := ab.GetBytes()
fmt.Printf("Got ArrayBuffer from script: %v\n", bytes)
for i := int64(0); i < ab.ByteLength(); i++ {
if bytes[i] != uint8(i) {
t.Errorf("Incorrect byte at index %d (whole array: %v)", i, bytes)
}
}
}
}
3 changes: 2 additions & 1 deletion isolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ func (i *Isolate) getCallback(ref int) FunctionCallback {
return i.cbs[ref]
}

func (i *Isolate) ThrowException(msg string) *Value { // TwinTag added
// Throw an exception into javascript land from within a go function callback
func (i *Isolate) ThrowException(msg string) *Value {
cmsg := C.CString(msg)
defer C.free(unsafe.Pointer(cmsg))
C.ThrowException(i.ptr, cmsg)
Expand Down
51 changes: 51 additions & 0 deletions v8go.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1335,4 +1335,55 @@ const char* Version() {
void SetFlags(const char* flags) {
V8::SetFlagsFromString(flags);
}

/************** ArrayBuffer support *****************/

// Create a new ArrayBuffer value of the requested size
ValuePtr NewArrayBuffer(IsolatePtr iso_ptr, size_t byte_length) {
ISOLATE_SCOPE_INTERNAL_CONTEXT(iso_ptr);
Local<Context> c = ctx->ptr.Get(iso);

// The Context::Enter/Exit is only needed when calling this code from low-level unit tests,
// otherwise ArrayBuffer::New() trips over missing context.
// They are not needed when this code gets called through an executing script.
c->Enter();

std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore(iso, byte_length);
Local<ArrayBuffer> arbuf = ArrayBuffer::New(iso, std::move(bs));

m_value* val = new m_value;
val->iso = iso;
val->ctx = ctx;
val->ptr = Persistent<Value, CopyablePersistentTraits<Value>>(iso, arbuf);

c->Exit(); // see comment above

return tracked_value(ctx, val);
}

// Obtain length in bytes of this ArrayBuffer
size_t ArrayBufferByteLength(ValuePtr ptr) {
LOCAL_VALUE(ptr);
Local<ArrayBuffer> ab = value.As<ArrayBuffer>();
return ab->ByteLength();
}

// Returns pointer into ArrayBuffer's BackingStore.
// The caller is supposed to have a ref on the ArrayBuffer so that the BackingStore stays valid.
void* GetArrayBufferBytes(ValuePtr ptr) {
LOCAL_VALUE(ptr);
Local<ArrayBuffer> ab = value.As<ArrayBuffer>();
return ab->GetBackingStore()->Data();
}

// Writes into the ArrayBuffer's BackingStore.
// The caller is responsible for respecting buffer boundaries.
// The caller is also supposed to have a ref on the ArrayBuffer so that the BackingStore stays valid.
void PutArrayBufferBytes(ValuePtr ptr, size_t byteOffset, const char *bytes, size_t byteLength) {
LOCAL_VALUE(ptr);
Local<ArrayBuffer> ab = value.As<ArrayBuffer>();
uint8_t *data = (uint8_t*) ab->GetBackingStore()->Data();
memcpy(data+byteOffset, bytes, byteLength);
}

}
6 changes: 6 additions & 0 deletions v8go.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ extern ValuePtr ExceptionTypeError(IsolatePtr iso_ptr, const char* message);
const char* Version();
extern void SetFlags(const char* flags);

// ArrayBuffer support
extern ValuePtr NewArrayBuffer(IsolatePtr iso_ptr, size_t byte_length);
extern size_t ArrayBufferByteLength(ValuePtr val_ptr);
extern void* GetArrayBufferBytes(ValuePtr val_ptr); // returns pointer into BackingStore data buffer
extern void PutArrayBufferBytes(ValuePtr val_ptr, size_t byteOffset, const char *bytes, size_t byteLength); // writes byteLength bytes into BackingStore data buffer at byteOffset

#ifdef __cplusplus
} // extern "C"
#endif
Expand Down
Loading

0 comments on commit 76db6dd

Please sign in to comment.