Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SnapshotCreator API #281

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
24 changes: 24 additions & 0 deletions context.go
Expand Up @@ -76,6 +76,30 @@ func NewContext(opt ...ContextOption) *Context {
return ctx
}

// NewContextFromSnapshot creates a new JavaScript context from the Isolate startup data;
// error will be of type `JSError` if not nil.
func NewContextFromSnapshot(iso *Isolate, snapshot_index int) (*Context, error) {
GustavoCaso marked this conversation as resolved.
Show resolved Hide resolved
ctxMutex.Lock()
ctxSeq++
ref := ctxSeq
ctxMutex.Unlock()

rtn := C.NewContextFromSnapshot(iso.ptr, C.size_t(snapshot_index), C.int(ref))

if rtn.context == nil {
return nil, newJSError(rtn.error)
}

ctx := &Context{
ref: ref,
ptr: rtn.context,
iso: iso,
}

ctx.register()
return ctx, nil
}

// Isolate gets the current context's parent isolate.An error is returned
// if the isolate has been terninated.
func (c *Context) Isolate() *Isolate {
Expand Down
55 changes: 55 additions & 0 deletions context_test.go
Expand Up @@ -39,6 +39,61 @@ func TestContextExec(t *testing.T) {
}
}

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

iso := v8.NewIsolate()
defer iso.Dispose()

ctx, err := v8.NewContextFromSnapshot(iso, 1)

if ctx != nil {
t.Errorf("expected nil context got: %+v", ctx)
}
if err == nil {
t.Error("error expected but was <nil>")
}
}

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

snapshotCreator := v8.NewSnapshotCreator()
snapshotCreatorIso, err := snapshotCreator.GetIsolate()
fatalIf(t, err)

snapshotCreatorCtx := v8.NewContext(snapshotCreatorIso)
defer snapshotCreatorCtx.Close()

snapshotCreatorCtx.RunScript(`const add = (a, b) => a + b`, "add.js")
snapshotCreatorCtx.RunScript(`function run() { return add(3, 4); }`, "main.js")
err = snapshotCreator.SetDefaultContext(snapshotCreatorCtx)
fatalIf(t, err)

snapshotCreatorCtx2 := v8.NewContext(snapshotCreatorIso)
defer snapshotCreatorCtx2.Close()

snapshotCreatorCtx2.RunScript(`const multiply = (a, b) => a * b`, "add.js")
snapshotCreatorCtx2.RunScript(`function run() { return multiply(3, 4); }`, "main.js")
index, err := snapshotCreator.AddContext(snapshotCreatorCtx2)
fatalIf(t, err)

data, err := snapshotCreator.Create(v8.FunctionCodeHandlingClear)
fatalIf(t, err)

iso := v8.NewIsolate(v8.WithStartupData(data))
defer iso.Dispose()

ctx, err := v8.NewContextFromSnapshot(iso, index+1)

if ctx != nil {
t.Errorf("expected nil context got: %+v", ctx)
}
if err == nil {
t.Error("error expected but was <nil>")
}
GustavoCaso marked this conversation as resolved.
Show resolved Hide resolved
}

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

Expand Down
40 changes: 35 additions & 5 deletions isolate.go
Expand Up @@ -25,8 +25,9 @@ type Isolate struct {
cbSeq int
cbs map[int]FunctionCallback

null *Value
undefined *Value
null *Value
undefined *Value
createParams *CreateParams
}

// HeapStatistics represents V8 isolate heap statistics
Expand All @@ -44,20 +45,48 @@ type HeapStatistics struct {
NumberOfDetachedContexts uint64
}

type createOptions func(*CreateParams)

func WithStartupData(startupData *StartupData) createOptions {
return func(params *CreateParams) {
params.startupData = startupData
}
}

type CreateParams struct {
startupData *StartupData
}

// 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.
// When an isolate is no longer used its resources should be freed
// by calling iso.Dispose().
// If StartupData is passed as part of createOptions it will be use as part
// of the createParams. The StartupData will be use when creating new context
// from the Isolate.
// An *Isolate can be used as a v8go.ContextOption to create a new
// Context, rather than creating a new default Isolate.
func NewIsolate() *Isolate {
func NewIsolate(opts ...createOptions) *Isolate {
GustavoCaso marked this conversation as resolved.
Show resolved Hide resolved
v8once.Do(func() {
C.Init()
})
params := &CreateParams{}
for _, opt := range opts {
opt(params)
}

var cOptions C.IsolateOptions

if params.startupData != nil {
cOptions.snapshot_blob_data = (*C.char)(unsafe.Pointer(&params.startupData.data[0]))
cOptions.snapshot_blob_raw_size = params.startupData.raw_size
}

iso := &Isolate{
ptr: C.NewIsolate(),
cbs: make(map[int]FunctionCallback),
ptr: C.NewIsolate(cOptions),
cbs: make(map[int]FunctionCallback),
createParams: params,
}
iso.null = newValueNull(iso)
iso.undefined = newValueUndefined(iso)
Expand Down Expand Up @@ -146,6 +175,7 @@ func (i *Isolate) Dispose() {
return
}
C.IsolateDispose(i.ptr)
i.createParams = nil
i.ptr = nil
}

Expand Down
116 changes: 116 additions & 0 deletions snapshot_creator.go
@@ -0,0 +1,116 @@
// Copyright 2021 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 <stdlib.h>
// #include "v8go.h"
import "C"
import (
"errors"
"unsafe"
)

type FunctionCodeHandling int

// Clear - does not keeps any compiled data prior to serialization/deserialization/verify pass
GustavoCaso marked this conversation as resolved.
Show resolved Hide resolved
// Keep - keeps any compiled data prior to serialization/deserialization/verify pass
const (
FunctionCodeHandlingClear FunctionCodeHandling = iota
FunctionCodeHandlingKeep
)

// StartupData stores the snapshot blob data
type StartupData struct {
data []byte
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't we want to be able to persist or transfer the bytes so that the snapshot can be reused cross-process, similar to CompilerCachedData? To allow that, we need the bytes field to be exported so they can be used for IPC and so the struct can be constructed in another process.

Suggested change
data []byte
Bytes []byte

raw_size C.int
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems like this field could be removed, since the slice holds the data size

}

// SnapshotCreator allows creating snapshot.
type SnapshotCreator struct {
ptr C.SnapshotCreatorPtr
iso *Isolate
defaultContextAdded bool
}

// NewSnapshotCreator creates a new snapshot creator.
func NewSnapshotCreator() *SnapshotCreator {
v8once.Do(func() {
C.Init()
})

rtn := C.NewSnapshotCreator()

return &SnapshotCreator{
ptr: rtn.creator,
iso: &Isolate{ptr: rtn.iso},
defaultContextAdded: false,
}
}

// GetIsolate returns the Isolate associated with the SnapshotCreator.
// This Isolate must be use to create the contexts that later will be use to create the snapshot blob.
GustavoCaso marked this conversation as resolved.
Show resolved Hide resolved
func (s *SnapshotCreator) GetIsolate() (*Isolate, error) {
if s.ptr == nil {
return nil, errors.New("v8go: Cannot get Isolate after creating the blob")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be worth panicking here trying to call a function on a snapshot creator thats likely been disposed of (either through Dispose or from calling Create to make the blob)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems that the pattern among other functions #118 (comment)

Should we panic in all of the instances. It seems that it would make working with the Snapshot creator API much easier.

snapshotCreator := v8.NewSnapshotCreator()
snapshotCreatorIso := snapshotCreator.GetIsolate()

snapshotCreatorCtx := v8.NewContext(snapshotCreatorIso)
defer snapshotCreatorCtx.Close()

snapshotCreatorCtx.RunScript(`const add = (a, b) => a + b`, "add.js")
snapshotCreatorCtx.RunScript(`function run() { return add(3, 4); }`, "main.js")
snapshotCreator.SetDefaultContext(snapshotCreatorCtx)
data := snapshotCreator.Create(v8.FunctionCodeHandlingClear)

iso := v8.NewIsolate(v8.WithStartupData(data))
defer iso.Dispose()

ctx, err := v8.NewContextFromSnapshot(iso, index)
defer ctx.Close()

The only one that I'm not sure 100% we should panic is the one that errors when calling Create before adding a default context since this is something a developer can fix.

What do you think @genevieve?

Copy link
Collaborator

@genevieve genevieve Mar 15, 2022

Choose a reason for hiding this comment

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

Lets stick with the error

Copy link
Collaborator

Choose a reason for hiding this comment

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

We should return errors when we expect the caller to handle them. The V8 API already does that, so we can normally keep the API similar to V8 in that respect, except that we use a panic to protect against crashes. The API will especially be annoying when getters like this one return an error.

}

return s.iso, nil
}

// SetDefaultContext set the default context to be included in the snapshot blob.
func (s *SnapshotCreator) SetDefaultContext(ctx *Context) error {
if s.defaultContextAdded {
return errors.New("v8go: Cannot set multiple default context for snapshot creator")
}

C.SetDefaultContext(s.ptr, ctx.ptr)
s.defaultContextAdded = true
ctx.ptr = nil
genevieve marked this conversation as resolved.
Show resolved Hide resolved

return nil
}

// AddContext add additional context to be included in the snapshot blob.
// Returns the index of the context in the snapshot blob, that later can be use to call v8go.NewContextFromSnapshot.
func (s *SnapshotCreator) AddContext(ctx *Context) (int, error) {
if s.ptr == nil {
return 0, errors.New("v8go: Cannot add context to snapshot creator after creating the blob")
}

index := C.AddContext(s.ptr, ctx.ptr)
ctx.ptr = nil

return int(index), nil
}

// Create creates a snapshot data blob.
GustavoCaso marked this conversation as resolved.
Show resolved Hide resolved
func (s *SnapshotCreator) Create(functionCode FunctionCodeHandling) (*StartupData, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

StartupData is basically just a wrapper around a slice. It may as well be returned without the extra pointer indirection, since the error would indicate whether or not it is an error. Since V8 doesn't actually expose an error message, ok could be used for the error and the s.ptr == nil and !s.defaultContextAdded cases could result in a panic.

if s.ptr == nil {
return nil, errors.New("v8go: Cannot use snapshot creator after creating the blob")
}

if !s.defaultContextAdded {
return nil, errors.New("v8go: Cannot create a snapshot without a default context")
}

rtn := C.CreateBlob(s.ptr, C.int(functionCode))

s.ptr = nil
s.iso.ptr = nil
genevieve marked this conversation as resolved.
Show resolved Hide resolved

raw_size := rtn.raw_size
data := C.GoBytes(unsafe.Pointer(rtn.data), raw_size)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The v8::SnapshotCreator::CreateBlob documentation says it returns "{ nullptr, 0 } on failure". If this function is going to return an error, then I would definitely expect an error to be returned when V8 would return an error. Unfortunately, V8 is quite vague on what possible reasons there are for an error and the API doesn't expose an error message.


C.SnapshotBlobDelete(rtn)

return &StartupData{data: data, raw_size: raw_size}, nil
}

// Dispose deletes the reference to the SnapshotCreator.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems worth mentioning that this also disposes of the isolate associated with it

func (s *SnapshotCreator) Dispose() {
if s.ptr != nil {
C.DeleteSnapshotCreator(s.ptr)
}
GustavoCaso marked this conversation as resolved.
Show resolved Hide resolved
}