vm: add bindings for v8::CompileFunctionInContext

Adds a method compileFunction to the vm module, which serves as a
binding for v8::CompileFunctionInContext with appropriate args for
specifying the details, and provide params for the wrapper.

Eventually, we would be changing Module._compile to use this internally
over the standard Module.wrap

PR-URL: https://github.com/nodejs/node/pull/21571
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Ujjwal Sharma 2018-06-28 12:05:19 +05:30 committed by Michaël Zasso
parent 1a25f9639a
commit 1abbe0a212
No known key found for this signature in database
GPG Key ID: 770F7A9A5AE15600
5 changed files with 405 additions and 0 deletions

View File

@ -637,6 +637,34 @@ console.log(globalVar);
// 1000
```
## vm.compileFunction(code[, params[, options]])
<!-- YAML
added: REPLACEME
-->
* `code` {string} The body of the function to compile.
* `params` {string[]} An array of strings containing all parameters for the
function.
* `options` {Object}
* `filename` {string} Specifies the filename used in stack traces produced
by this script. **Default:** `''`.
* `lineOffset` {number} Specifies the line number offset that is displayed
in stack traces produced by this script. **Default:** `0`.
* `columnOffset` {number} Specifies the column number offset that is displayed
in stack traces produced by this script. **Default:** `0`.
* `cachedData` {Buffer} Provides an optional `Buffer` with V8's code cache
data for the supplied source.
* `produceCachedData` {boolean} Specifies whether to produce new cache data.
**Default:** `false`.
* `parsingContext` {Object} The sandbox/context in which the said function
should be compiled in.
* `contextExtensions` {Object[]} An array containing a collection of context
extensions (objects wrapping the current scope) to be applied while
compiling. **Default:** `[]`.
Compiles the given code into the provided context/sandbox (if no context is
supplied, the current context is used), and returns it wrapped inside a
function with the given `params`.
## vm.createContext([sandbox[, options]])
<!-- YAML
added: v0.3.1

View File

@ -26,12 +26,17 @@ const {
ContextifyScript,
makeContext,
isContext: _isContext,
compileFunction: _compileFunction
} = internalBinding('contextify');
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { isUint8Array } = require('internal/util/types');
const { validateInt32, validateUint32 } = require('internal/validators');
const kParsingContext = Symbol('script parsing context');
const ArrayForEach = Function.call.bind(Array.prototype.forEach);
const ArrayIsArray = Array.isArray;
class Script extends ContextifyScript {
constructor(code, options = {}) {
code = `${code}`;
@ -286,6 +291,94 @@ function runInThisContext(code, options) {
return createScript(code, options).runInThisContext(options);
}
function compileFunction(code, params, options = {}) {
if (typeof code !== 'string') {
throw new ERR_INVALID_ARG_TYPE('code', 'string', code);
}
if (params !== undefined) {
if (!ArrayIsArray(params)) {
throw new ERR_INVALID_ARG_TYPE('params', 'Array', params);
}
ArrayForEach(params, (param, i) => {
if (typeof param !== 'string') {
throw new ERR_INVALID_ARG_TYPE(`params[${i}]`, 'string', param);
}
});
}
const {
filename = '',
columnOffset = 0,
lineOffset = 0,
cachedData = undefined,
produceCachedData = false,
parsingContext = undefined,
contextExtensions = [],
} = options;
if (typeof filename !== 'string') {
throw new ERR_INVALID_ARG_TYPE('options.filename', 'string', filename);
}
validateUint32(columnOffset, 'options.columnOffset');
validateUint32(lineOffset, 'options.lineOffset');
if (cachedData !== undefined && !isUint8Array(cachedData)) {
throw new ERR_INVALID_ARG_TYPE(
'options.cachedData',
'Uint8Array',
cachedData
);
}
if (typeof produceCachedData !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE(
'options.produceCachedData',
'boolean',
produceCachedData
);
}
if (parsingContext !== undefined) {
if (
typeof parsingContext !== 'object' ||
parsingContext === null ||
!isContext(parsingContext)
) {
throw new ERR_INVALID_ARG_TYPE(
'options.parsingContext',
'Context',
parsingContext
);
}
}
if (!ArrayIsArray(contextExtensions)) {
throw new ERR_INVALID_ARG_TYPE(
'options.contextExtensions',
'Array',
contextExtensions
);
}
ArrayForEach(contextExtensions, (extension, i) => {
if (typeof extension !== 'object') {
throw new ERR_INVALID_ARG_TYPE(
`options.contextExtensions[${i}]`,
'object',
extension
);
}
});
return _compileFunction(
code,
filename,
lineOffset,
columnOffset,
cachedData,
produceCachedData,
parsingContext,
contextExtensions,
params
);
}
module.exports = {
Script,
createContext,
@ -294,6 +387,7 @@ module.exports = {
runInNewContext,
runInThisContext,
isContext,
compileFunction,
};
if (process.binding('config').experimentalVMModules) {

View File

@ -209,6 +209,7 @@ void ContextifyContext::Init(Environment* env, Local<Object> target) {
env->SetMethod(target, "makeContext", MakeContext);
env->SetMethod(target, "isContext", IsContext);
env->SetMethod(target, "compileFunction", CompileFunction);
}
@ -987,6 +988,144 @@ class ContextifyScript : public BaseObject {
};
void ContextifyContext::CompileFunction(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
// Argument 1: source code
CHECK(args[0]->IsString());
Local<String> code = args[0].As<String>();
// Argument 2: filename
CHECK(args[1]->IsString());
Local<String> filename = args[1].As<String>();
// Argument 3: line offset
CHECK(args[2]->IsNumber());
Local<Integer> line_offset = args[2].As<Integer>();
// Argument 4: column offset
CHECK(args[3]->IsNumber());
Local<Integer> column_offset = args[3].As<Integer>();
// Argument 5: cached data (optional)
Local<Uint8Array> cached_data_buf;
if (!args[4]->IsUndefined()) {
CHECK(args[4]->IsUint8Array());
cached_data_buf = args[4].As<Uint8Array>();
}
// Argument 6: produce cache data
CHECK(args[5]->IsBoolean());
bool produce_cached_data = args[5]->IsTrue();
// Argument 7: parsing context (optional)
Local<Context> parsing_context;
if (!args[6]->IsUndefined()) {
CHECK(args[6]->IsObject());
ContextifyContext* sandbox =
ContextifyContext::ContextFromContextifiedSandbox(
env, args[6].As<Object>());
CHECK_NOT_NULL(sandbox);
parsing_context = sandbox->context();
} else {
parsing_context = context;
}
// Argument 8: context extensions (optional)
Local<Array> context_extensions_buf;
if (!args[7]->IsUndefined()) {
CHECK(args[7]->IsArray());
context_extensions_buf = args[7].As<Array>();
}
// Argument 9: params for the function (optional)
Local<Array> params_buf;
if (!args[8]->IsUndefined()) {
CHECK(args[8]->IsArray());
params_buf = args[8].As<Array>();
}
// Read cache from cached data buffer
ScriptCompiler::CachedData* cached_data = nullptr;
if (!cached_data_buf.IsEmpty()) {
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());
}
ScriptOrigin origin(filename, line_offset, column_offset);
ScriptCompiler::Source source(code, origin, cached_data);
ScriptCompiler::CompileOptions options;
if (source.GetCachedData() == nullptr) {
options = ScriptCompiler::kNoCompileOptions;
} else {
options = ScriptCompiler::kConsumeCodeCache;
}
TryCatch try_catch(isolate);
Context::Scope scope(parsing_context);
// Read context extensions from buffer
std::vector<Local<Object>> context_extensions;
if (!context_extensions_buf.IsEmpty()) {
for (uint32_t n = 0; n < context_extensions_buf->Length(); n++) {
Local<Value> val;
if (!context_extensions_buf->Get(context, n).ToLocal(&val)) return;
CHECK(val->IsObject());
context_extensions.push_back(val.As<Object>());
}
}
// Read params from params buffer
std::vector<Local<String>> params;
if (!params_buf.IsEmpty()) {
for (uint32_t n = 0; n < params_buf->Length(); n++) {
Local<Value> val;
if (!params_buf->Get(context, n).ToLocal(&val)) return;
CHECK(val->IsString());
params.push_back(val.As<String>());
}
}
MaybeLocal<Function> maybe_fun = ScriptCompiler::CompileFunctionInContext(
context, &source, params.size(), params.data(),
context_extensions.size(), context_extensions.data(), options);
Local<Function> fun;
if (maybe_fun.IsEmpty() || !maybe_fun.ToLocal(&fun)) {
ContextifyScript::DecorateErrorStack(env, try_catch);
try_catch.ReThrow();
return;
}
if (produce_cached_data) {
const std::unique_ptr<ScriptCompiler::CachedData>
cached_data(ScriptCompiler::CreateCodeCacheForFunction(fun, code));
bool cached_data_produced = cached_data != nullptr;
if (cached_data_produced) {
MaybeLocal<Object> buf = Buffer::Copy(
env,
reinterpret_cast<const char*>(cached_data->data),
cached_data->length);
if (fun->Set(
parsing_context,
env->cached_data_string(),
buf.ToLocalChecked()).IsNothing()) return;
}
if (fun->Set(
parsing_context,
env->cached_data_produced_string(),
Boolean::New(isolate, cached_data_produced)).IsNothing()) return;
}
args.GetReturnValue().Set(fun);
}
void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {

View File

@ -58,6 +58,8 @@ class ContextifyContext {
private:
static void MakeContext(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsContext(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CompileFunction(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void WeakCallback(
const v8::WeakCallbackInfo<ContextifyContext>& data);
static void PropertyGetterCallback(

View File

@ -128,3 +128,145 @@ const vm = require('vm');
'Received type object'
});
});
// vm.compileFunction
{
assert.strictEqual(
vm.compileFunction('console.log("Hello, World!")').toString(),
'function () {\nconsole.log("Hello, World!")\n}'
);
assert.strictEqual(
vm.compileFunction(
'return p + q + r + s + t',
['p', 'q', 'r', 's', 't']
)('ab', 'cd', 'ef', 'gh', 'ij'),
'abcdefghij'
);
vm.compileFunction('return'); // Should not throw on 'return'
common.expectsError(() => {
vm.compileFunction(
'});\n\n(function() {\nconsole.log(1);\n})();\n\n(function() {'
);
}, {
type: SyntaxError,
message: 'Unexpected token }'
});
// Tests for failed argument validation
common.expectsError(() => vm.compileFunction(), {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "code" argument must be of type string. ' +
'Received type undefined'
});
vm.compileFunction(''); // Should pass without params or options
common.expectsError(() => vm.compileFunction('', null), {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "params" argument must be of type Array. ' +
'Received type object'
});
// vm.compileFunction('', undefined, null);
const optionTypes = {
'filename': 'string',
'columnOffset': 'number',
'lineOffset': 'number',
'cachedData': 'Uint8Array',
'produceCachedData': 'boolean',
};
for (const option in optionTypes) {
common.expectsError(() => {
vm.compileFunction('', undefined, { [option]: null });
}, {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: `The "options.${option}" property must be of type ` +
`${optionTypes[option]}. Received type object`
});
}
// Testing for context-based failures
[Boolean(), Number(), null, String(), Symbol(), {}].forEach(
(value) => {
common.expectsError(() => {
vm.compileFunction('', undefined, { parsingContext: value });
}, {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.parsingContext" property must be of type ' +
`Context. Received type ${typeof value}`
});
}
);
assert.strictEqual(
vm.compileFunction(
'return a;',
undefined,
{ contextExtensions: [{ a: 5 }] }
)(),
5
);
common.expectsError(() => {
vm.compileFunction('', undefined, { contextExtensions: null });
}, {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.contextExtensions" property must be of type Array' +
'. Received type object'
});
common.expectsError(() => {
vm.compileFunction('', undefined, { contextExtensions: [0] });
}, {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.contextExtensions[0]" property must be of type ' +
'object. Received type number'
});
const oldLimit = Error.stackTraceLimit;
// Setting value to run the last three tests
Error.stackTraceLimit = 1;
common.expectsError(() => {
vm.compileFunction('throw new Error("Sample Error")')();
}, {
message: 'Sample Error',
stack: 'Error: Sample Error\n at <anonymous>:1:7'
});
common.expectsError(() => {
vm.compileFunction(
'throw new Error("Sample Error")',
[],
{ lineOffset: 3 }
)();
}, {
message: 'Sample Error',
stack: 'Error: Sample Error\n at <anonymous>:4:7'
});
common.expectsError(() => {
vm.compileFunction(
'throw new Error("Sample Error")',
[],
{ columnOffset: 3 }
)();
}, {
message: 'Sample Error',
stack: 'Error: Sample Error\n at <anonymous>:1:10'
});
// Resetting value
Error.stackTraceLimit = oldLimit;
}