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:
parent
1a25f9639a
commit
1abbe0a212
@ -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
|
||||
|
94
lib/vm.js
94
lib/vm.js
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user