// Copyright 2016-2017, Pulumi Corporation. All rights reserved. #include <cstring> #include <vector> #include <node.h> #include <src/api.h> // v8 internal APIs #include <src/objects.h> // v8 internal APIs #include <src/contexts.h> // v8 internal APIs #if NODE_MAJOR_VERSION != 6 || NODE_MINOR_VERSION != 10 #error "The Pulumi Fabric SDK only supports Node.js 6.10.x at the moment" #endif namespace nativeruntime { using v8::Array; using v8::Context; using v8::Exception; using v8::Function; using v8::FunctionCallbackInfo; using v8::Integer; using v8::Isolate; using v8::Local; using v8::MaybeLocal; using v8::Null; using v8::Object; using v8::Script; using v8::String; using v8::Value; // CreateClosure allocates a new closure object which matches the definition on the JavaScript side. Local<Object> CreateClosure(Isolate* isolate, Local<String> code, Local<Object> environment) { Local<Object> closure = Object::New(isolate); closure->Set(String::NewFromUtf8(isolate, "code"), code); closure->Set(String::NewFromUtf8(isolate, "runtime"), String::NewFromUtf8(isolate, "nodejs")); closure->Set(String::NewFromUtf8(isolate, "environment"), environment); return closure; } // Lookup restores a context and looks up a variable name inside of it. Local<Value> Lookup(Isolate* isolate, v8::internal::Handle<v8::internal::Context> context, Local<String> name) { // First perform the lookup in the current chain. This unfortunately requires accessing internal // V8 APIs so that we can inspect the chain with the necessary flags and resulting objects. int index; v8::internal::PropertyAttributes attributes; v8::internal::BindingFlags bflags; v8::internal::Handle<v8::internal::String> hackname( reinterpret_cast<v8::internal::String**>(const_cast<String*>(*name))); v8::internal::Handle<v8::internal::Object> lookup = context->Lookup( hackname, v8::internal::ContextLookupFlags::FOLLOW_CHAINS, &index, &attributes, &bflags); // Now check the result. There are several legal possibilities. if (!lookup.is_null()) { if (lookup->IsContext()) { // The result was found in a context; index contains the slot number within that context. v8::internal::Isolate* hackiso = reinterpret_cast<v8::internal::Isolate*>(isolate); return v8::Utils::Convert<v8::internal::Object, Object>( v8::internal::FixedArray::get(v8::internal::Context::cast(*lookup), index, hackiso)); } else if (lookup->IsJSObject()) { // The result was a named property inside of a context extension (such as eval); we can return it as-is. return v8::Utils::Convert<v8::internal::Object, Object>(lookup); } } // If we fell through, either the lookup is null, or the object wasn't of the expected type. In either case, // this is an error (possibly a bug), and we will throw and return undefined so we can keep going. char namestr[255]; name->WriteUtf8(namestr, 255); Local<String> errormsg = String::Concat( String::NewFromUtf8(isolate, "Unexpected missing variable in closure environment: "), String::NewFromUtf8(isolate, namestr)); isolate->ThrowException(Exception::Error(errormsg)); return Local<Value>(); } Local<String> SerializeFunctionCode(Isolate *isolate, Local<Function> func) { // Serialize the code simply by calling toString on the Function. auto toString = Local<Function>::Cast(func->Get(String::NewFromUtf8(isolate, "toString"))); auto v8CodeString = Local<String>::Cast(toString->Call(func, 0, nullptr)); v8::String::Utf8Value utf8CodeString(v8CodeString->ToString()); std::string code = std::string(*utf8CodeString); std::string badPrefix("[Function:"); if (code.compare(0, badPrefix.length(), badPrefix) == 0) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Cannot serialize non-expression functions (such as definitions and generators)"))); } // Ensure that the code is a function expression (including arrows), and not a definition, etc. std::string openParen("("); std::string funcString("function"); if (code.compare(0, openParen.length(), openParen) == 0 || code.compare(0, funcString.length(), funcString) == 0) { // lambda or simple function expression. i.e. '() => { ... }' or 'function () { }' // wrap with parens to make into an expression that can be parsed at the top level // of a JS file. code = "(" + code + ")"; } else { // We got a method here. Which v8 represents like 'foo() { }'. So we wrap as // '(functoin foo() { })' so it can be parsed at the top level of a JS file. code = "(function " + code + ")"; } return String::NewFromUtf8(isolate, code.c_str()); } // SerializeFunction serializes a JavaScript function expression and its associated closure environment. Local<Value> SerializeFunction(Isolate *isolate, Local<Function> func, Local<Function> freeVarsFunc, Local<Function> envEntryFunc, Local<Object> envEntryCache) { // Get at the innards of the function. Unfortunately, we need to use internal V8 APIs to do this, // as the closest public function, CreationContext, intentionally returns the non-closure Context for // Function objects (it returns the constructor context, which is not what we want). v8::internal::Handle<v8::internal::JSFunction> hackfunc( reinterpret_cast<v8::internal::JSFunction**>(const_cast<Function*>(*func))); v8::internal::Handle<v8::internal::Context> lexical(hackfunc->context()); // Get the code as a string. Local<String> code = SerializeFunctionCode(isolate, func); // Compute the free variables by invoking the callback. const unsigned freeVarsArgc = 1; Local<Value> freeVarsArgv[freeVarsArgc] = { code }; Local<Value> freeVarsRet = freeVarsFunc->Call(Null(isolate), freeVarsArgc, freeVarsArgv); if (freeVarsRet.IsEmpty()) { // Only empty if the function threw an exception. Return early to propagate it. return Local<Value>(); } else if (!freeVarsRet->IsArray()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Free variables return expected to be an Array"))); return Local<Value>(); } // Now check all elements and produce a vector we can use below. std::vector<Local<String>> freeVars; Local<Array> freeVarsArray = Local<Array>::Cast(freeVarsRet); for (uint32_t i = 0; i < freeVarsArray->Length(); i++) { Local<Integer> index = Integer::New(isolate, i); Local<Value> elem = freeVarsArray->Get(index); if (elem.IsEmpty() || !elem->IsString()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Free variable Array must contain only String elements"))); return Local<Value>(); } freeVars.push_back(Local<String>::Cast(elem)); } // Next, serialize all free variables as they exist in the function's original lexical environment. Local<Object> environment = Object::New(isolate); for (std::vector<Local<String>>::iterator it = freeVars.begin(); it != freeVars.end(); ++it) { // Look up the variable in the lexical closure of the function and then serialize it. Local<String> freevar = *it; Local<Value> v = Lookup(isolate, lexical, freevar); if (v.IsEmpty()) { // Only empty if an error was thrown; bail eagerly to propagate it. return Local<Value>(); } const unsigned envEntryArgc = 2; Local<Value> envEntryArgv[envEntryArgc] = { v, envEntryCache }; Local<Value> envEntry = envEntryFunc->Call(Null(isolate), envEntryArgc, envEntryArgv); if (envEntry.IsEmpty()) { return Local<Value>(); } environment->Set(freevar, envEntry); } // Finally, produce a closure object with all the appropriate records, and return it. return CreateClosure(isolate, code, environment); } // serializeClosure serializes a function and its closure environment into a form that is amenable to persistence // as simple JSON. Like toString, it includes the full text of the function's source code, suitable for execution. // Unlike toString, it actually includes information about the captured environment. void SerializeClosure(const FunctionCallbackInfo<Value>& args) { Isolate* isolate = args.GetIsolate(); // Ensure the first argument is a proper function expression object. if (args.Length() < 1 || args[0]->IsUndefined()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Missing required function argument"))); return; } else if (!args[0]->IsFunction()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Function argument must be a Function object"))); return; } Local<Function> func = Local<Function>::Cast(args[0]); // And that the second is a callback we can use to compute the free variables list. if (args.Length() < 2 || args[1]->IsUndefined()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Missing required free variables calculator"))); return; } else if (!args[1]->IsFunction()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Free variables argument must be a Function object"))); return; } Local<Function> freeVarsFunc = Local<Function>::Cast(args[1]); // And that the third is a callback we can use to serialize environment entries. if (args.Length() < 3 || args[2]->IsUndefined()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Missing required env-entry serializer function"))); return; } else if (!args[2]->IsFunction()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Env-entry serializer argument must be a Function object"))); return; } Local<Function> envEntryFunc = Local<Function>::Cast(args[2]); // And that the fourth is an entry cache used to backstop mutually recursive captures. if (args.Length() < 4 || args[3]->IsUndefined()) { isolate->ThrowException(Exception::TypeError( String::NewFromUtf8(isolate, "Missing required env-entry cache"))); return; } Local<Object> envEntryCache = Local<Object>::Cast(args[3]); // Now go ahead and serialize it, and return the result. Local<Value> closure = SerializeFunction(isolate, func, freeVarsFunc, envEntryFunc, envEntryCache); if (!closure.IsEmpty()) { args.GetReturnValue().Set(closure); } } void init(Local<Object> exports) { NODE_SET_METHOD(exports, "serializeClosure", SerializeClosure); } NODE_MODULE(nativeruntime, init) } // namespace nativeruntime