Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
vm: add code cache support for SourceTextModule
PR-URL: #31278
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
  • Loading branch information
devsnek authored and targos committed Apr 28, 2020
1 parent de3603f commit eda6665
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 10 deletions.
10 changes: 10 additions & 0 deletions doc/api/errors.md
Expand Up @@ -1999,6 +1999,16 @@ the following reasons:
* It is being linked (`linkingStatus` is `'linking'`)
* Linking has failed for this module (`linkingStatus` is `'errored'`)

<a id="ERR_VM_MODULE_CACHED_DATA_REJECTED"></a>
### `ERR_VM_MODULE_CACHED_DATA_REJECTED`

The `cachedData` option passed to a module constructor is invalid.

<a id="ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA"></a>
### `ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA`

Cached data cannot be created for modules which have already been evaluated.

<a id="ERR_VM_MODULE_DIFFERENT_CONTEXT"></a>
### `ERR_VM_MODULE_DIFFERENT_CONTEXT`

Expand Down
26 changes: 26 additions & 0 deletions doc/api/vm.md
Expand Up @@ -563,6 +563,10 @@ defined in the ECMAScript specification.
* `identifier` {string} String used in stack traces.
**Default:** `'vm:module(i)'` where `i` is a context-specific ascending
index.
* `cachedData` {Buffer|TypedArray|DataView} Provides an optional `Buffer` or
`TypedArray`, or `DataView` with V8's code cache data for the supplied
source. The `code` must be the same as the module from which this
`cachedData` was created.
* `context` {Object} The [contextified][] object as returned by the
`vm.createContext()` method, to compile and evaluate this `Module` in.
* `lineOffset` {integer} Specifies the line number offset that is displayed
Expand Down Expand Up @@ -618,6 +622,28 @@ const contextifiedObject = vm.createContext({ secret: 42 });
})();
```

### `sourceTextModule.createCachedData()`
<!-- YAML
added: REPLACEME
-->

* Returns: {Buffer}

Creates a code cache that can be used with the SourceTextModule constructor's
`cachedData` option. Returns a Buffer. This method may be called any number
of times before the module has been evaluated.

```js
// Create an initial module
const module = new vm.SourceTextModule('const a = 1;');

// Create cached data from this module
const cachedData = module.createCachedData();

// Create a new module using the cached data. The code must be the same.
const module2 = new vm.SourceTextModule('const a = 1;', { cachedData });
```

## Class: `vm.SyntheticModule`
<!-- YAML
added: v12.16.0
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -1363,6 +1363,8 @@ E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
'A dynamic import callback was not specified.', TypeError);
E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error);
E('ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA',
'Cached data cannot be created for a module which has been evaluated', Error);
E('ERR_VM_MODULE_DIFFERENT_CONTEXT',
'Linked modules must use the same context', Error);
E('ERR_VM_MODULE_LINKING_ERRORED',
Expand Down
29 changes: 27 additions & 2 deletions lib/internal/vm/module.js
Expand Up @@ -11,7 +11,10 @@ const {
} = primordials;

const { isContext } = internalBinding('contextify');
const { isModuleNamespaceObject } = require('internal/util/types');
const {
isModuleNamespaceObject,
isArrayBufferView,
} = require('internal/util/types');
const {
getConstructorOf,
customInspectSymbol,
Expand All @@ -21,6 +24,7 @@ const {
ERR_INVALID_ARG_TYPE,
ERR_VM_MODULE_ALREADY_LINKED,
ERR_VM_MODULE_DIFFERENT_CONTEXT,
ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA,
ERR_VM_MODULE_LINKING_ERRORED,
ERR_VM_MODULE_NOT_MODULE,
ERR_VM_MODULE_STATUS,
Expand Down Expand Up @@ -107,7 +111,8 @@ class Module {

if (sourceText !== undefined) {
this[kWrap] = new ModuleWrap(identifier, context, sourceText,
options.lineOffset, options.columnOffset);
options.lineOffset, options.columnOffset,
options.cachedData);

binding.callbackMap.set(this[kWrap], {
initializeImportMeta: options.initializeImportMeta,
Expand Down Expand Up @@ -253,6 +258,7 @@ class SourceTextModule extends Module {
importModuleDynamically,
context,
identifier,
cachedData,
} = options;

validateInt32(lineOffset, 'options.lineOffset');
Expand All @@ -271,12 +277,21 @@ class SourceTextModule extends Module {
importModuleDynamically);
}

if (cachedData !== undefined && !isArrayBufferView(cachedData)) {
throw new ERR_INVALID_ARG_TYPE(
'options.cachedData',
['Buffer', 'TypedArray', 'DataView'],
cachedData
);
}

super({
sourceText,
context,
identifier,
lineOffset,
columnOffset,
cachedData,
initializeImportMeta,
importModuleDynamically,
});
Expand Down Expand Up @@ -348,6 +363,16 @@ class SourceTextModule extends Module {
}
return super.error;
}

createCachedData() {
const { status } = this;
if (status === 'evaluating' ||
status === 'evaluated' ||
status === 'errored') {
throw new ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA();
}
return this[kWrap].createCachedData();
}
}

class SyntheticModule extends Module {
Expand Down
75 changes: 67 additions & 8 deletions src/module_wrap.cc
Expand Up @@ -2,12 +2,13 @@

#include "env.h"
#include "memory_tracker-inl.h"
#include "node_contextify.h"
#include "node_errors.h"
#include "node_internals.h"
#include "node_process.h"
#include "node_url.h"
#include "util-inl.h"
#include "node_contextify.h"
#include "node_watchdog.h"
#include "node_process.h"
#include "util-inl.h"

#include <sys/stat.h> // S_IFDIR

Expand All @@ -22,6 +23,8 @@ using node::contextify::ContextifyContext;
using node::url::URL;
using node::url::URL_FLAGS_FAILED;
using v8::Array;
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::EscapableHandleScope;
using v8::Function;
Expand All @@ -46,6 +49,7 @@ using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::ScriptOrModule;
using v8::String;
using v8::UnboundModuleScript;
using v8::Undefined;
using v8::Value;

Expand Down Expand Up @@ -133,7 +137,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
// new ModuleWrap(url, context, exportNames, syntheticExecutionFunction)
CHECK(args[3]->IsFunction());
} else {
// new ModuleWrap(url, context, source, lineOffset, columOffset)
// new ModuleWrap(url, context, source, lineOffset, columOffset, cachedData)
CHECK(args[2]->IsString());
CHECK(args[3]->IsNumber());
line_offset = args[3].As<Integer>();
Expand Down Expand Up @@ -169,6 +173,18 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
module = Module::CreateSyntheticModule(isolate, url, export_names,
SyntheticModuleEvaluationStepsCallback);
} else {
ScriptCompiler::CachedData* cached_data = nullptr;
if (!args[5]->IsUndefined()) {
CHECK(args[5]->IsArrayBufferView());
Local<ArrayBufferView> cached_data_buf = args[5].As<ArrayBufferView>();
ArrayBuffer::Contents contents =
cached_data_buf->Buffer()->GetContents();
uint8_t* data = static_cast<uint8_t*>(contents.Data());
cached_data =
new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(),
cached_data_buf->ByteLength());
}

Local<String> source_text = args[2].As<String>();
ScriptOrigin origin(url,
line_offset, // line offset
Expand All @@ -180,8 +196,15 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
False(isolate), // is WASM
True(isolate), // is ES Module
host_defined_options);
ScriptCompiler::Source source(source_text, origin);
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
ScriptCompiler::Source source(source_text, origin, cached_data);
ScriptCompiler::CompileOptions options;
if (source.GetCachedData() == nullptr) {
options = ScriptCompiler::kNoCompileOptions;
} else {
options = ScriptCompiler::kConsumeCodeCache;
}
if (!ScriptCompiler::CompileModule(isolate, &source, options)
.ToLocal(&module)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
Expand All @@ -191,6 +214,13 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
}
return;
}
if (options == ScriptCompiler::kConsumeCodeCache &&
source.GetCachedData()->rejected) {
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
env, "cachedData buffer was rejected");
try_catch.ReThrow();
return;
}
}
}

Expand Down Expand Up @@ -1551,8 +1581,7 @@ MaybeLocal<Value> ModuleWrap::SyntheticModuleEvaluationStepsCallback(
return ret;
}

void ModuleWrap::SetSyntheticExport(
const v8::FunctionCallbackInfo<v8::Value>& args) {
void ModuleWrap::SetSyntheticExport(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Object> that = args.This();

Expand All @@ -1572,6 +1601,35 @@ void ModuleWrap::SetSyntheticExport(
USE(module->SetSyntheticModuleExport(isolate, export_name, export_value));
}

void ModuleWrap::CreateCachedData(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Object> that = args.This();

ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, that);

CHECK(!obj->synthetic_);

Local<Module> module = obj->module_.Get(isolate);

CHECK_LT(module->GetStatus(), v8::Module::Status::kEvaluating);

Local<UnboundModuleScript> unbound_module_script =
module->GetUnboundModuleScript();
std::unique_ptr<ScriptCompiler::CachedData> cached_data(
ScriptCompiler::CreateCodeCache(unbound_module_script));
Environment* env = Environment::GetCurrent(args);
if (!cached_data) {
args.GetReturnValue().Set(Buffer::New(env, 0).ToLocalChecked());
} else {
MaybeLocal<Object> buf =
Buffer::Copy(env,
reinterpret_cast<const char*>(cached_data->data),
cached_data->length);
args.GetReturnValue().Set(buf.ToLocalChecked());
}
}

void ModuleWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand All @@ -1587,6 +1645,7 @@ void ModuleWrap::Initialize(Local<Object> target,
env->SetProtoMethod(tpl, "instantiate", Instantiate);
env->SetProtoMethod(tpl, "evaluate", Evaluate);
env->SetProtoMethod(tpl, "setExport", SetSyntheticExport);
env->SetProtoMethodNoSideEffect(tpl, "createCachedData", CreateCachedData);
env->SetProtoMethodNoSideEffect(tpl, "getNamespace", GetNamespace);
env->SetProtoMethodNoSideEffect(tpl, "getStatus", GetStatus);
env->SetProtoMethodNoSideEffect(tpl, "getError", GetError);
Expand Down
1 change: 1 addition & 0 deletions src/module_wrap.h
Expand Up @@ -75,6 +75,7 @@ class ModuleWrap : public BaseObject {
v8::Local<v8::Context> context, v8::Local<v8::Module> module);
static void SetSyntheticExport(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void CreateCachedData(const v8::FunctionCallbackInfo<v8::Value>& args);

static v8::MaybeLocal<v8::Module> ResolveCallback(
v8::Local<v8::Context> context,
Expand Down
1 change: 1 addition & 0 deletions src/node_errors.h
Expand Up @@ -59,6 +59,7 @@ void OnFatalError(const char* location, const char* message);
V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, Error) \
V(ERR_VM_MODULE_CACHED_DATA_REJECTED, Error) \

#define V(code, type) \
inline v8::Local<v8::Value> code(v8::Isolate* isolate, \
Expand Down
30 changes: 30 additions & 0 deletions test/parallel/test-vm-module-cached-data.js
@@ -0,0 +1,30 @@
'use strict';

// Flags: --experimental-vm-modules

require('../common');

const assert = require('assert');
const { SourceTextModule } = require('vm');

{
const m = new SourceTextModule('const a = 1');
const cachedData = m.createCachedData();

new SourceTextModule('const a = 1', { cachedData });

assert.throws(() => {
new SourceTextModule('differentSource', { cachedData });
}, {
code: 'ERR_VM_MODULE_CACHED_DATA_REJECTED',
});
}

assert.rejects(async () => {
const m = new SourceTextModule('const a = 1');
await m.link(() => {});
m.evaluate();
m.createCachedData();
}, {
code: 'ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA',
});

0 comments on commit eda6665

Please sign in to comment.