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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip #1

Closed
wants to merge 18 commits into from
15 changes: 7 additions & 8 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,14 +651,13 @@ Module.prototype._compile = function(content, filename) {

content = stripShebang(content);

// create wrapper function
var wrapper = Module.wrap(content);

var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
const compiledWrapper = vm.compileFunction(content, [
'exports',
'require',
'module',
'__filename',
'__dirname',
], { filename });

var inspectorWrapper = null;
if (process._breakFirstLine && process._eval == null) {
Expand Down
32 changes: 32 additions & 0 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
kParsingContext,
makeContext,
isContext: _isContext,
compileFunction: _compileFunction
} = process.binding('contextify');

const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
Expand Down Expand Up @@ -286,6 +287,36 @@ function runInThisContext(code, options) {
return createScript(code, options).runInThisContext(options);
}

function compileFunction(code, params, options) {
const defaults = {
filename: '',
columnOffset: 0,
lineOffset: 0,
cachedData: undefined,
produceCachedData: false,
parsingContext: undefined
};
const {
filename,
columnOffset,
lineOffset,
cachedData,
produceCachedData,
parsingContext,
} = Object.assign(defaults, options);

return _compileFunction(
code,
filename,
lineOffset,
columnOffset,
cachedData,
produceCachedData,
parsingContext,
params
);
}

module.exports = {
Script,
createContext,
Expand All @@ -294,6 +325,7 @@ module.exports = {
runInNewContext,
runInThisContext,
isContext,
compileFunction,
};

if (process.binding('config').experimentalVMModules)
Expand Down
115 changes: 115 additions & 0 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ void ContextifyContext::Init(Environment* env, Local<Object> target) {

env->SetMethod(target, "makeContext", MakeContext);
env->SetMethod(target, "isContext", IsContext);
env->SetMethod(target, "compileFunction", CompileFunction);
}


Expand Down Expand Up @@ -951,6 +952,120 @@ 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
Copy link

Choose a reason for hiding this comment

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

i don't think we really need offsets

Choose a reason for hiding this comment

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

I think we’d probably want this for parity with New()

Copy link
Owner Author

@ryzokuken ryzokuken Jun 25, 2018

Choose a reason for hiding this comment

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

+1 to what @addaleax said. Added the additional args for parity.

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: params for the function (optional)
Local<Array> params_buf;
if (!args[7]->IsUndefined()) {
CHECK(args[7]->IsArray());
params_buf = args[7].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());

Choose a reason for hiding this comment

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

All this can really be done inside the if-block where you checked args[2]. That way you can also define cached_data_buf inside the block.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I did it in this particular fashion to mimic what New did, so I have no preferences here whatsoever. Will do this.

Choose a reason for hiding this comment

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

I see. In that case it's alright then.

}

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 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 = params_buf->Get(context, n).ToLocalChecked();

Choose a reason for hiding this comment

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

The earlier comment here about ToLocalChecked still stands, btw

Copy link
Owner Author

Choose a reason for hiding this comment

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

Umm, we're checking that each item in params is a string, so that's covered. ToLocalChecked can fail, so I guess we could return if it does. I hope there's no scope of a memory leak anymore, right?

Choose a reason for hiding this comment

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

Yes, we tested that it’s a string (although a sufficiently evil getter could also return a string while typechecking and then later return something else)

What I meant was that if it’s a getter, it can always fail, so we should return back to JS if it does

I hope there's no scope of a memory leak anymore, right?

I don’t think so, no. :)

CHECK(val->IsString());
params.push_back(val.As<String>());
}
}

MaybeLocal<Function> maybe_fun = ScriptCompiler::CompileFunctionInContext(
context, &source, params.size(), params.data(), 0, nullptr, 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 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);
fun->Set(env->cached_data_string(), buf.ToLocalChecked());

Choose a reason for hiding this comment

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

@addaleax do you have a suggestions as to where the resulting buffer could be better stored?

Alternatively, we don't offer this at all, and expose a separate binding for ScriptCompiler::CreateCodeCacheForFunction similar to how we also recently exposed ScriptCompiler::CreateCodeCache. That way this function gets a bit simpler, but then it would also lose symmetry to ContextifyScript::New.

Copy link

@devsnek devsnek Jun 23, 2018

Choose a reason for hiding this comment

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

since the option for ContextifyScript::New is deprecated in my pr i think its best not to mimic it.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@devsnek could you drop me a link to it? Currently it Sets the two on the returned function object itself, which sounds like similar (if not the same) to what we're doing in ContextifyScript::New. Would love to see any alternatives you have in mind.

Copy link

Choose a reason for hiding this comment

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

Copy link
Owner Author

Choose a reason for hiding this comment

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

@devsnek cool, this is exactly what @hashseed pointed out, I could do this. That said, it's still just a documentation-level deprecation, right?

Copy link

Choose a reason for hiding this comment

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

well... it will be docs-dep for 10, runtime dep for 11, and removed in 12.

Choose a reason for hiding this comment

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

In that case it would be reasonable to not introduce it here in the first place :)

}
fun->Set(
env->cached_data_produced_string(),
Boolean::New(isolate, cached_data_produced));

Choose a reason for hiding this comment

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

Both of these should use the non-deprecated overloads of Set() (and ideally also do error handling for these cases)

Copy link
Owner Author

Choose a reason for hiding this comment

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

I didn't check those out, will do this ASAP.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@addaleax I suppose that by non-deprecated you mean ones that take context, key, val instead of plain key, val? How do I know which functions are deprecated?

Choose a reason for hiding this comment

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

They are marked V8_DEPRECATE_SOON or V8_DEPRECATED in v8.h :)

Copy link
Owner Author

Choose a reason for hiding this comment

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

Got it, thanks.

}

args.GetReturnValue().Set(fun);
}


void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
Expand Down
2 changes: 2 additions & 0 deletions src/node_contextify.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class ContextifyContext {
private:
static void MakeContext(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsContext(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetWrappedFunction(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void WeakCallback(
const v8::WeakCallbackInfo<ContextifyContext>& data);
static void PropertyGetterCallback(
Expand Down
27 changes: 27 additions & 0 deletions test/parallel/test-vm-basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,30 @@ 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 }'
});
}

Choose a reason for hiding this comment

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

I thought you had more tests :)

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yeah, I commented out these as well, for now. Now that the existing tests all pass, will focus on covering as many cases as possible with tests.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yeah, I commented out these as well, for now. Now that the existing tests all pass, will focus on covering as many cases as possible with tests.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yeah, I commented out these as well, for now. Now that the existing tests all pass, will focus on covering as many cases as possible with tests.