diff --git a/CHANGELOG.md b/CHANGELOG.md index ab1b80bd4..f46fc2374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- NewIsolate accepts option arguments. Thanks to [@GustavoCaso](https://github.com/GustavoCaso) +- Add support for Snapshot creation. Thanks to [@GustavoCaso](https://github.com/GustavoCaso) ### Fixed - Use string length to ensure null character-containing strings in Go/JS are not terminated early. diff --git a/context.go b/context.go index 718757068..b9b5cc33e 100644 --- a/context.go +++ b/context.go @@ -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) { + 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 { diff --git a/context_test.go b/context_test.go index 10dad5d8e..989c0f22b 100644 --- a/context_test.go +++ b/context_test.go @@ -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 ") + } +} + +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 ") + } +} + func TestJSExceptions(t *testing.T) { t.Parallel() diff --git a/isolate.go b/isolate.go index 661fbec05..9a0f292a5 100644 --- a/isolate.go +++ b/isolate.go @@ -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 @@ -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 { 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(¶ms.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) @@ -146,6 +175,7 @@ func (i *Isolate) Dispose() { return } C.IsolateDispose(i.ptr) + i.createParams = nil i.ptr = nil } diff --git a/snapshot_creator.go b/snapshot_creator.go new file mode 100644 index 000000000..837d7b09e --- /dev/null +++ b/snapshot_creator.go @@ -0,0 +1,118 @@ +// 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 +// #include "v8go.h" +import "C" +import ( + "errors" + "unsafe" +) + +type FunctionCodeHandling int + +// Clear - does not keep any compiled data prior to serialization/deserialization/verify pass +// 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 + raw_size C.int +} + +// 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 used to create the contexts that later will be used to create the snapshot blob. +func (s *SnapshotCreator) GetIsolate() (*Isolate, error) { + if s.ptr == nil { + return nil, errors.New("v8go: Cannot get Isolate after creating the blob") + } + + 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 + + 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. +// The snapshot creator instance is unsable after creating the snapshot data blob. +func (s *SnapshotCreator) Create(functionCode FunctionCodeHandling) (*StartupData, error) { + 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 + + raw_size := rtn.raw_size + data := C.GoBytes(unsafe.Pointer(rtn.data), raw_size) + + C.SnapshotBlobDelete(rtn) + + return &StartupData{data: data, raw_size: raw_size}, nil +} + +// Dispose deletes the reference to the SnapshotCreator. +func (s *SnapshotCreator) Dispose() { + if s.ptr != nil { + C.DeleteSnapshotCreator(s.ptr) + s.ptr = nil + } +} diff --git a/snapshot_creator_test.go b/snapshot_creator_test.go new file mode 100644 index 000000000..900588981 --- /dev/null +++ b/snapshot_creator_test.go @@ -0,0 +1,180 @@ +// 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_test + +import ( + "testing" + + v8 "rogchap.com/v8go" +) + +func TestCreateSnapshot(t *testing.T) { + 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) + + data, err := snapshotCreator.Create(v8.FunctionCodeHandlingClear) + fatalIf(t, err) + + iso := v8.NewIsolate(v8.WithStartupData(data)) + defer iso.Dispose() + + ctx := v8.NewContext(iso) + defer ctx.Close() + + runVal, err := ctx.Global().Get("run") + fatalIf(t, err) + + fn, err := runVal.AsFunction() + fatalIf(t, err) + + val, err := fn.Call(v8.Undefined(iso)) + fatalIf(t, err) + + if val.String() != "7" { + t.Fatal("invalid val") + } +} + +func TestCreateSnapshotAndAddExtraContext(t *testing.T) { + 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) + + snapshotCreatorCtx3 := v8.NewContext(snapshotCreatorIso) + defer snapshotCreatorCtx3.Close() + + snapshotCreatorCtx3.RunScript(`const div = (a, b) => a / b`, "add.js") + snapshotCreatorCtx3.RunScript(`function run() { return div(6, 2); }`, "main.js") + index2, err := snapshotCreator.AddContext(snapshotCreatorCtx3) + 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) + fatalIf(t, err) + defer ctx.Close() + + runVal, err := ctx.Global().Get("run") + fatalIf(t, err) + + fn, err := runVal.AsFunction() + fatalIf(t, err) + + val, err := fn.Call(v8.Undefined(iso)) + fatalIf(t, err) + + if val.String() != "12" { + t.Fatal("invalid val") + } + + ctx, err = v8.NewContextFromSnapshot(iso, index2) + fatalIf(t, err) + + defer ctx.Close() + + runVal, err = ctx.Global().Get("run") + fatalIf(t, err) + + fn, err = runVal.AsFunction() + fatalIf(t, err) + + val, err = fn.Call(v8.Undefined(iso)) + fatalIf(t, err) + + if val.String() != "3" { + t.Fatal("invalid val") + } +} + +func TestCreateSnapshotErrorAfterAddingMultipleDefaultContext(t *testing.T) { + snapshotCreator := v8.NewSnapshotCreator() + defer snapshotCreator.Dispose() + 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) + + err = snapshotCreator.SetDefaultContext(snapshotCreatorCtx) + + if err == nil { + t.Error("setting another default context should have failed, got ") + } +} + +func TestCreateSnapshotErrorAfterSuccessfullCreate(t *testing.T) { + 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) + + _, err = snapshotCreator.Create(v8.FunctionCodeHandlingClear) + fatalIf(t, err) + + _, err = snapshotCreator.GetIsolate() + if err == nil { + t.Error("getting Isolate should have fail") + } + + _, err = snapshotCreator.AddContext(snapshotCreatorCtx) + if err == nil { + t.Error("adding context should have fail") + } + + _, err = snapshotCreator.Create(v8.FunctionCodeHandlingClear) + if err == nil { + t.Error("creating snapshot should have fail") + } +} + +func TestCreateSnapshotErrorIfNoDefaultContextIsAdded(t *testing.T) { + snapshotCreator := v8.NewSnapshotCreator() + defer snapshotCreator.Dispose() + + _, err := snapshotCreator.Create(v8.FunctionCodeHandlingClear) + + if err == nil { + t.Error("creating a snapshop should have fail") + } +} diff --git a/v8go.cc b/v8go.cc index 97390883e..27b220ee8 100644 --- a/v8go.cc +++ b/v8go.cc @@ -26,6 +26,7 @@ const int ScriptCompilerEagerCompile = ScriptCompiler::kEagerCompile; struct m_ctx { Isolate* iso; + StartupData* startup_data; std::vector vals; std::vector unboundScripts; Persistent ptr; @@ -152,9 +153,19 @@ void Init() { return; } -IsolatePtr NewIsolate() { +IsolatePtr NewIsolate(IsolateOptions options) { Isolate::CreateParams params; params.array_buffer_allocator = default_allocator; + + StartupData* startup_data; + if (options.snapshot_blob_data) { + startup_data = new StartupData{options.snapshot_blob_data, + options.snapshot_blob_raw_size}; + params.snapshot_blob = startup_data; + } else { + startup_data = nullptr; + } + Isolate* iso = Isolate::New(params); Locker locker(iso); Isolate::Scope isolate_scope(iso); @@ -166,6 +177,7 @@ IsolatePtr NewIsolate() { m_ctx* ctx = new m_ctx; ctx->ptr.Reset(iso, Context::New(iso)); ctx->iso = iso; + ctx->startup_data = startup_data; iso->SetData(0, ctx); return iso; @@ -264,6 +276,65 @@ RtnUnboundScript IsolateCompileUnboundScript(IsolatePtr iso, return rtn; } +/********** SnapshotCreator **********/ + +RtnSnapshotCreator NewSnapshotCreator() { + RtnSnapshotCreator rtn = {}; + SnapshotCreator* creator = new SnapshotCreator; + Isolate* iso = creator->GetIsolate(); + rtn.creator = creator; + rtn.iso = iso; + + return rtn; +} + +void DeleteSnapshotCreator(SnapshotCreatorPtr snapshotCreator) { + delete snapshotCreator; +} + +void SetDefaultContext(SnapshotCreatorPtr snapshotCreator, ContextPtr ctx) { + Isolate* iso = ctx->iso; + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + Local local_ctx = ctx->ptr.Get(iso); + Context::Scope context_scope(local_ctx); + + ContextFree(ctx); + + snapshotCreator->SetDefaultContext(local_ctx); +} + +size_t AddContext(SnapshotCreatorPtr snapshotCreator, ContextPtr ctx) { + Isolate* iso = ctx->iso; + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + Local local_ctx = ctx->ptr.Get(iso); + Context::Scope context_scope(local_ctx); + + ContextFree(ctx); + + return snapshotCreator->AddContext(local_ctx); +} + +RtnSnapshotBlob* CreateBlob(SnapshotCreatorPtr snapshotCreator, + int function_code_handling) { + StartupData startup_data = snapshotCreator->CreateBlob( + SnapshotCreator::FunctionCodeHandling(function_code_handling)); + + RtnSnapshotBlob* rtn = new RtnSnapshotBlob; + rtn->data = startup_data.data; + rtn->raw_size = startup_data.raw_size; + delete snapshotCreator; + return rtn; +} + +void SnapshotBlobDelete(RtnSnapshotBlob* ptr) { + delete[] ptr->data; + delete ptr; +} + /********** Exceptions & Errors **********/ ValuePtr IsolateThrowException(IsolatePtr iso, ValuePtr value) { @@ -593,9 +664,45 @@ ContextPtr NewContext(IsolatePtr iso, m_ctx* ctx = new m_ctx; ctx->ptr.Reset(iso, local_ctx); ctx->iso = iso; + ctx->startup_data = nullptr; return ctx; } +RtnContext NewContextFromSnapshot(IsolatePtr iso, + size_t snapshot_blob_index, + int ref) { + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + + RtnContext rtn = {}; + + // For function callbacks we need a reference to the context, but because of + // the complexities of C -> Go function pointers, we store a reference to the + // context as a simple integer identifier; this can then be used on the Go + // side to lookup the context in the context registry. We use slot 1 as slot 0 + // has special meaning for the Chrome debugger. + + Local local_ctx; + + if (!Context::FromSnapshot(iso, snapshot_blob_index).ToLocal(&local_ctx)) { + RtnError error = {nullptr, nullptr, nullptr}; + error.msg = CopyString("Failed to create context from snapshot index: " + + std::to_string(snapshot_blob_index)); + rtn.error = error; + return rtn; + } + + local_ctx->SetEmbedderData(1, Integer::New(iso, ref)); + + m_ctx* ctx = new m_ctx; + ctx->ptr.Reset(iso, local_ctx); + ctx->iso = iso; + ctx->startup_data = nullptr; + rtn.context = ctx; + return rtn; +} + void ContextFree(ContextPtr ctx) { if (ctx == nullptr) { return; @@ -612,6 +719,10 @@ void ContextFree(ContextPtr ctx) { delete us; } + if (ctx->startup_data) { + delete ctx->startup_data; + } + delete ctx; } diff --git a/v8go.h b/v8go.h index 7acaf0425..efe95eed2 100644 --- a/v8go.h +++ b/v8go.h @@ -15,6 +15,7 @@ typedef v8::CpuProfiler* CpuProfilerPtr; typedef v8::CpuProfile* CpuProfilePtr; typedef const v8::CpuProfileNode* CpuProfileNodePtr; typedef v8::ScriptCompiler::CachedData* ScriptCompilerCachedDataPtr; +typedef v8::SnapshotCreator* SnapshotCreatorPtr; extern "C" { #else @@ -22,6 +23,9 @@ extern "C" { typedef struct v8Isolate v8Isolate; typedef v8Isolate* IsolatePtr; +typedef struct v8SnapshotCreator v8SnapshotCreator; +typedef v8SnapshotCreator* SnapshotCreatorPtr; + typedef struct v8CpuProfiler v8CpuProfiler; typedef v8CpuProfiler* CpuProfilerPtr; @@ -72,11 +76,26 @@ typedef struct { int rejected; } ScriptCompilerCachedData; +typedef struct { + const char* data; + int raw_size; +} RtnSnapshotBlob; + +typedef struct { + SnapshotCreatorPtr creator; + IsolatePtr iso; +} RtnSnapshotCreator; + typedef struct { ScriptCompilerCachedData cachedData; int compileOption; } CompileOptions; +typedef struct { + char* snapshot_blob_data; + int snapshot_blob_raw_size; +} IsolateOptions; + typedef struct { CpuProfilerPtr ptr; IsolatePtr iso; @@ -105,6 +124,11 @@ typedef struct { RtnError error; } RtnValue; +typedef struct { + ContextPtr context; + RtnError error; +} RtnContext; + typedef struct { const char* data; int length; @@ -132,13 +156,22 @@ typedef struct { } ValueBigInt; extern void Init(); -extern IsolatePtr NewIsolate(); +extern IsolatePtr NewIsolate(IsolateOptions opts); extern void IsolatePerformMicrotaskCheckpoint(IsolatePtr ptr); extern void IsolateDispose(IsolatePtr ptr); extern void IsolateTerminateExecution(IsolatePtr ptr); extern int IsolateIsExecutionTerminating(IsolatePtr ptr); extern IsolateHStatistics IsolationGetHeapStatistics(IsolatePtr ptr); +extern void SnapshotBlobDelete(RtnSnapshotBlob* ptr); +extern RtnSnapshotCreator NewSnapshotCreator(); +extern void DeleteSnapshotCreator(SnapshotCreatorPtr snapshotCreator); +extern void SetDefaultContext(SnapshotCreatorPtr snapshotCreator, + ContextPtr ctx); +extern size_t AddContext(SnapshotCreatorPtr snapshotCreator, ContextPtr ctx); +extern RtnSnapshotBlob* CreateBlob(SnapshotCreatorPtr snapshotCreator, + int function_code_handling); + extern ValuePtr IsolateThrowException(IsolatePtr iso, ValuePtr value); extern RtnUnboundScript IsolateCompileUnboundScript(IsolatePtr iso_ptr, @@ -162,6 +195,9 @@ extern void CPUProfileDelete(CPUProfile* ptr); extern ContextPtr NewContext(IsolatePtr iso_ptr, TemplatePtr global_template_ptr, int ref); +extern RtnContext NewContextFromSnapshot(IsolatePtr iso, + size_t snapshot_blob_index, + int ref); extern void ContextFree(ContextPtr ptr); extern RtnValue RunScript(ContextPtr ctx_ptr, const char* source,