diff --git a/LIB_USAGE.md b/LIB_USAGE.md new file mode 100644 index 00000000000000..5e38c4267576f1 --- /dev/null +++ b/LIB_USAGE.md @@ -0,0 +1,315 @@ +# How to use Node.js as a shared library +## Limitations +* It is only possible to access Node.js from the same thread (no multi-threading). +* There can only be one Node.js per process at the same time. + +## Handling the Node.js event loop +There are two different ways of handling the Node.js event loop. +### C++ keeps control over thread +By calling `node::ProcessEvents()`, the Node.js event loop will be run once, handling the next pending event. The return value of the call specifies whether there are more events in the queue. + +### C++ gives control of the thread to Node.js +By calling `node::RunEventLoop(callback)`, the C++ host program gives up the control of the thread and allows the Node.js event loop to run until no more events are in the queue or `node::StopEventLoop()` is called. The `callback` parameter in the `RunEventLoop` function is called once per iteration of the event loop. This allows the C++ programmer to react to changes in the Node.js state and e.g. terminate Node.js preemptively. + +## Examples +In the following, a few examples demonstrate the usage of Node.js as a library. For more complex examples, including handling of the event loop, see the [node-embed](https://github.com/hpicgs/node-embed) repository. + +### (1) Evaluating in-line JavaScript code +This example evaluates multiple lines of JavaScript code in the global Node.js context. The result of `console.log` is piped to stdout. + +```C++ +node::Initialize(); +node::Evaluate("var helloMessage = 'Hello from Node.js!';"); +node::Evaluate("console.log(helloMessage);"); +``` + +### (2) Running a JavaScript file +This example evaluates a JavaScript file and lets Node handle all pending events until the event loop is empty. + +```C++ +node::Initialize(); +node::Run("cli.js"); +while (node::ProcessEvents()) { } +``` + +### (3) Including an NPM Module +This example uses the [fs](https://nodejs.org/api/fs.html) module to check whether a specific file exists. +```C++ +node::Initialize(); +auto fs = node::IncludeModule("fs"); +v8::Isolate *isolate = node::internal::isolate(); + +// Check if file cli.js exists in the current working directory. +auto result = node::Call(fs, "existsSync", {v8::String::NewFromUtf8(isolate, "file.txt")}); + +auto file_exists = v8::Local::Cast(result)->BooleanValue(); +std::cout << (file_exists ? "file.txt exists in cwd" : "file.txt does NOT exist in cwd") << std::endl; + +``` + +### (4) Advanced: Combining a Qt GUI with Node.js +This example, which is borrowed from the examples repository [node-embed](https://github.com/hpicgs/node-embed), fetches a RSS feed from the BBC and displays it in a Qt GUI. For this, the `feedparser` and `request` modules from NPM are utilized. + +#### main.cpp +```C++ +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "node.h" +#include "node_lib.h" + +#include "RssFeed.h" + +int main(int argc, char* argv[]) { + // Locate the JavaScript file we want to embed: + const std::string js_file = "data/node-lib-qt-rss.js"; + const std::string data_path = cpplocate::locatePath(js_file); + const std::string js_path = data_path + "/" + js_file; + + // Initialize Qt: + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; + + // to be able to access the public slots of the RssFeed instance + // we inject a pointer to it in the QML context: + engine.rootContext()->setContextProperty("rssFeed", &RssFeed::getInstance()); + + engine.load(QUrl(QLatin1String("qrc:/main.qml"))); + + // Initialize Node.js engine: + node::Initialize(); + + // Register C++ methods to be used within JavaScript + // The third parameter binds the C++ module to that name, + // allowing the functions to be called like this: "cppQtGui.clearFeed(...)" + node::RegisterModule("cpp-qt-gui", { + {"addFeedItem", RssFeed::addFeedItem}, + {"clearFeed", RssFeed::clearFeed}, + {"redraw", RssFeed::redrawGUI}, + }, "cppQtGui"); + + // Evaluate the JavaScript file once: + node::Run(js_path); + + // Load intial RSS feed to display it: + RssFeed::refreshFeed(); + + // Run the Qt application: + app.exec(); + + // After we are done, deinitialize the Node.js engine: + node::Deinitialize(); +} +``` + +#### RssFeed.h +```C++ +#pragma once + +#include +#include "node.h" + +/** + * @brief The RssFeed class retrieves an RSS feed from the Internet and + * provides its entries. + */ +class RssFeed : public QObject { + + Q_OBJECT + + Q_PROPERTY(QStringList entries READ getEntries NOTIFY entriesChanged) + +private: + /** + * @brief Creates a new RssFeed with a given QObject as its parent. + * + * @param parent The parent object. + */ + explicit RssFeed(QObject* parent=nullptr); + +public: + /** + * @brief Returns the singleton instance for this class. + * + * @return The singlton instance for this class. + */ + static RssFeed& getInstance(); + + /** + * @brief This method is called from the embedded JavaScript. + * It is used for deleting all items from this RSS feed. + * + * @param args The arguments passed from the embedded JavaScript. + * Hint: This method does not expect any arguments. + */ + static void clearFeed(const v8::FunctionCallbackInfo& args); + + /** + * @brief This method is called from the embedded JavaScript. + * It is used to add an entry to this RSS feed. + * + * @param args The arguments passed from the embedded JavaScript. + * Hint: This method expects an object, which contains the RSS feed item. + */ + static void addFeedItem(const v8::FunctionCallbackInfo& args); + + /** + * @brief This method is called from the embedded JavaScript. + * It is used to refresh the GUI, after all retrieved feed items have been + * appended. + * + * @param args The arguments passed from the embedded JavaScript. + * Hint: This method does not expect any arguments. + */ + static void redrawGUI(const v8::FunctionCallbackInfo& args); + + /** + * @brief This method updates the feed by calling a JS function + * and running the Node.js main loop until the whole feed was received. + */ + Q_INVOKABLE static void refreshFeed(); + +private: + static RssFeed* instance; /*!< The singleton instance for this class. */ + QStringList entries; /*!< The list of RSS feeds to display. */ + +signals: + void entriesChanged(); /*!< Emitted after entries were added or removed. */ + +public slots: + /** + * @brief getEntries returns the entries of the RSS feed + * + * @return a list of text entries + */ + QStringList getEntries() const; +}; +``` + +#### RssFeed.cpp +```C++ +#include "RssFeed.h" + +#include +#include +#include "node_lib.h" + +RssFeed* RssFeed::instance = nullptr; + +RssFeed::RssFeed(QObject* parent) + : QObject(parent) +{ + +} + +RssFeed& RssFeed::getInstance(){ + if (instance == nullptr){ + instance = new RssFeed(); + } + return *instance; +} + +QStringList RssFeed::getEntries() const { + return entries; +} + +void RssFeed::clearFeed(const v8::FunctionCallbackInfo& args) { + getInstance().entries.clear(); +} + +void RssFeed::redrawGUI(const v8::FunctionCallbackInfo& args) { + emit getInstance().entriesChanged(); +} + +void RssFeed::refreshFeed() { + // invoke the embedded JavaScript in order to fetch new RSS feeds: + node::Evaluate("emitRequest()"); + + // wait for the embedded JavaScript to finish its execution + // meanwhile, process any QT events + node::RunEventLoop([](){ QGuiApplication::processEvents(); }); +} + +void RssFeed::addFeedItem(const v8::FunctionCallbackInfo& args) { + // check, whether this method was called as expected + // therefore, we need to make sure, that the first argument exists + // and that it is an object + v8::Isolate* isolate = args.GetIsolate(); + if (args.Length() < 1 || !args[0]->IsObject()) { + isolate->ThrowException(v8::Exception::TypeError( + v8::String::NewFromUtf8(isolate, "Error: One object expected"))); + return; + } + + v8::Local context = isolate->GetCurrentContext(); + v8::Local obj = args[0]->ToObject(context).ToLocalChecked(); + + // we want to get all properties of our input object: + v8::Local props = obj->GetOwnPropertyNames(context).ToLocalChecked(); + + for (int i = 0, l = props->Length(); i < l; i++) { + v8::Local localKey = props->Get(i); + v8::Local localVal = obj->Get(context, localKey).ToLocalChecked(); + std::string key = *v8::String::Utf8Value(localKey); + std::string val = *v8::String::Utf8Value(localVal); + + // append the RSS feed body to the list of RSS feeds: + getInstance().entries << QString::fromStdString(val); + } +} +``` + +#### node-lib-qt-rss.js +```JS +var FeedParser = require('feedparser'); +var request = require('request'); // for fetching the feed + +var emitRequest = function () { + console.log("Refreshing feeds...") + var feedparser = new FeedParser([]); + var req = request('http://feeds.bbci.co.uk/news/world/rss.xml') + + req.on('error', function (error) { + // catch all request errors but don't handle them in this demo + }); + + req.on('response', function (res) { + var stream = this; // `this` is `req`, which is a stream + + if (res.statusCode !== 200) { + this.emit('error', new Error('Bad status code')); + } + else { + cppQtGui.clearFeed(); + stream.pipe(feedparser); + } + }); + + feedparser.on('error', function (error) { + // catch all parser errors but don't handle them in this demo + }); + + feedparser.on('readable', function () { + var stream = this; // `this` is `feedparser`, which is a stream + var item = stream.read(); + + if (item) { + var itemString = item['title'] + '\n' + item['description']; + cppQtGui.addFeedItem( { item: itemString } ); + } + }); + + feedparser.on('end', function (){ + cppQtGui.redraw(); + }); +} +``` diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index f85e557d5efdf8..b987d40e60372e 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -202,8 +202,15 @@ preloadModules(); perf.markMilestone( NODE_PERFORMANCE_MILESTONE_PRELOAD_MODULE_LOAD_END); - // If -i or --interactive were passed, or stdin is a TTY. - if (process._forceRepl || NativeModule.require('tty').isatty(0)) { + if (!process._evaluateStdin) { + // Calling evalScript is necessary, because it sets + // globals.require to the require function. + // Otherwise, require doesn't work. + // TODO(cmfcmf): Find a cleaner way to get require to work. + evalScript('[eval]'); + } else if (process._forceRepl || + NativeModule.require('tty').isatty(0)) { + // If -i or --interactive were passed, or stdin is a TTY // REPL const cliRepl = NativeModule.require('internal/repl'); cliRepl.createInternalRepl(process.env, function(err, repl) { diff --git a/node.gyp b/node.gyp index f0e51d7a84af36..1f32cbc7299988 100644 --- a/node.gyp +++ b/node.gyp @@ -370,6 +370,7 @@ 'src/node_http2_state.h', 'src/node_internals.h', 'src/node_javascript.h', + 'src/node_lib.h', 'src/node_mutex.h', 'src/node_perf.h', 'src/node_perf_common.h', diff --git a/src/node.cc b/src/node.cc index 0d62c89b030703..09f1e03b8b498a 100644 --- a/src/node.cc +++ b/src/node.cc @@ -19,6 +19,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +#include "node_lib.h" #include "node_buffer.h" #include "node_constants.h" #include "node_javascript.h" @@ -86,6 +87,9 @@ #include #include +#include +#include +#include #if defined(NODE_HAVE_I18N_SUPPORT) #include @@ -3275,7 +3279,7 @@ static void RawDebug(const FunctionCallbackInfo& args) { fflush(stderr); } -void LoadEnvironment(Environment* env) { +void LoadEnvironment(Environment* env, const bool evaluate_stdin) { HandleScope handle_scope(env->isolate()); TryCatch try_catch(env->isolate()); @@ -3338,6 +3342,20 @@ void LoadEnvironment(Environment* env) { // like Node's I/O bindings may want to replace 'f' with their own function. Local arg = env->process_object(); + // add evaluate_stdin parameter to JS process so we can use it in + // bootstrap_node.js. + // If adding the parameter fails, bootstrapping will fail too, + // so we can cleanup and return before we even bootstrap. + bool successfully_set_evaluate_stdin = Object::Cast(*arg)->Set( + env->context(), + String::NewFromUtf8(env->isolate(), "_evaluateStdin"), + Boolean::New(env->isolate(), evaluate_stdin)).FromJust(); + + if (!successfully_set_evaluate_stdin) { + env->async_hooks()->clear_async_id_stack(); + return; + } + auto ret = f->Call(env->context(), Null(env->isolate()), 1, &arg); // If there was an error during bootstrap then it was either handled by the // FatalException handler or it's unrecoverable (e.g. max call stack @@ -4331,67 +4349,173 @@ Local NewContext(Isolate* isolate, return context; } +inline static bool TickEventLoop(Environment* env, + node::UvLoopBehavior behavior) { + uv_run(env->event_loop(), static_cast(behavior)); -inline int Start(Isolate* isolate, IsolateData* isolate_data, - int argc, const char* const* argv, - int exec_argc, const char* const* exec_argv) { - HandleScope handle_scope(isolate); - Local context = NewContext(isolate); - Context::Scope context_scope(context); - Environment env(isolate_data, context); - env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling); + if (uv_loop_alive(env->event_loop())) { + return true; + } - const char* path = argc > 1 ? argv[1] : nullptr; - StartInspector(&env, path, debug_options); + v8_platform.DrainVMTasks(env->isolate()); - if (debug_options.inspector_enabled() && !v8_platform.InspectorStarted(&env)) - return 12; // Signal internal error. + if (uv_loop_alive(env->event_loop())) { + return true; + } - env.set_abort_on_uncaught_exception(abort_on_uncaught_exception); + EmitBeforeExit(env); - if (no_force_async_hooks_checks) { - env.async_hooks()->no_force_checks(); - } + // Emit `beforeExit` if the loop became alive either after emitting + // event, or after running some callbacks. + return uv_loop_alive(env->event_loop()); +} - { - Environment::AsyncCallbackScope callback_scope(&env); - env.async_hooks()->push_async_ids(1, 0); - LoadEnvironment(&env); - env.async_hooks()->pop_async_id(1); - } +// Call built-in modules' _register_ function to +// do module registration explicitly. +void RegisterBuiltinModules() { +#define V(modname) _register_##modname(); + NODE_BUILTIN_MODULES(V) +#undef V +} - env.set_trace_sync_io(trace_sync_io); +/** + * @brief The CmdArgs class is a container for argc and argv. + */ +class CmdArgs { + public: + /** + * @brief CmdArgs creates valid argc and argv variables from a program name + * and arguments. + * + * The argv buffer is a contiguous, adjacent char buffer and contains the + * program name as its first item followed by the provided arguments. argc + * is the number of arguments + 1 (the program name). + * The resulting argv buffer should not be modified. + * + * @param program_name the name of the executable + * @param arguments the arguments for the program + */ + CmdArgs(const std::string& program_name, + const std::vector& arguments) + : argc(0), + argv(nullptr) { + size_t total_size = 0; + total_size += program_name.size() + 1; + for (const auto& argument : arguments) { + total_size += argument.size() + 1; + } - { - SealHandleScope seal(isolate); - bool more; - PERFORMANCE_MARK(&env, LOOP_START); - do { - uv_run(env.event_loop(), UV_RUN_DEFAULT); + std::vector offsets; + argument_data_.reserve(total_size); + offsets.push_back(argument_data_.size()); + argument_data_ += program_name; + argument_data_ += static_cast(0x0); + for (const auto& argument : arguments) { + offsets.push_back(argument_data_.size()); + argument_data_ += argument; + argument_data_ += static_cast(0x0); + } - v8_platform.DrainVMTasks(isolate); + argument_pointers_.resize(offsets.size()); + for (std::size_t i=0; i < argument_pointers_.size(); ++i) { + argument_pointers_[i] = argument_data_.data() + offsets[i]; + } + argc = argument_pointers_.size(); + argv = argument_pointers_.data(); + } + + ~CmdArgs() = default; + + /** + * @brief argc is the number of arguments + 1 (the program name) + */ + int argc; + /** + * @brief argv is an array containing pointers to the arguments (and the + * program name), it should not be modified + */ + const char** argv; + + private: + /** + * @brief argument_data contains the program name and the arguments separated + * by null bytes + */ + std::string argument_data_; + /** + * @brief argument_pointers contains pointers to the beginnings of the + * strings in argument_data + */ + std::vector argument_pointers_; +}; - more = uv_loop_alive(env.event_loop()); - if (more) - continue; +class HandleScopeHeapWrapper { + public: + explicit HandleScopeHeapWrapper(v8::Isolate* isolate) + : scope_(isolate) { } + private: + HandleScope scope_; +}; - RunBeforeExit(&env); +ArrayBufferAllocator* allocator; +Locker* locker; +IsolateData* isolate_data; +HandleScopeHeapWrapper* _handle_scope_wrapper = nullptr; +Local context; +bool request_stop = false; +CmdArgs* cmd_args = nullptr; +bool _event_loop_running = false; +Environment* _environment = nullptr; - // Emit `beforeExit` if the loop became alive either after emitting - // event, or after running some callbacks. - more = uv_loop_alive(env.event_loop()); - } while (more == true); - PERFORMANCE_MARK(&env, LOOP_EXIT); +bool eventLoopIsRunning() { + return _event_loop_running; +} + +namespace internal { + v8::Isolate* isolate() { + return node_isolate; + } + + Environment* environment() { + return _environment; + } +} + +void _RegisterModuleCallback(v8::Local exports, + v8::Local /*module*/, + v8::Local /*context*/, + void* priv) { + auto module_fns = static_cast*>(priv); + if (!module_fns) { + fprintf(stderr, "_RegisterModuleCallback: module_functions is null"); + return; + } + for (std::pair element : *module_fns) { + NODE_SET_METHOD(exports, element.first.c_str(), element.second); + } + delete module_fns; +} + +namespace deinitialize { + +void _DeleteCmdArgs() { + if (!cmd_args) { + return; } + delete cmd_args; + cmd_args = nullptr; +} - env.set_trace_sync_io(false); +int _ExitV8() { + _environment->set_trace_sync_io(false); - const int exit_code = EmitExit(&env); - RunAtExit(&env); + int exit_code = EmitExit(_environment); + RunAtExit(_environment); - v8_platform.DrainVMTasks(isolate); - v8_platform.CancelVMTasks(isolate); - WaitForInspectorDisconnect(&env); + v8_platform.DrainVMTasks(_environment->isolate()); + v8_platform.CancelVMTasks(_environment->isolate()); + WaitForInspectorDisconnect(_environment); #if defined(LEAK_SANITIZER) __lsan_do_leak_check(); #endif @@ -4399,19 +4523,82 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data, return exit_code; } -inline int Start(uv_loop_t* event_loop, - int argc, const char* const* argv, - int exec_argc, const char* const* exec_argv) { +void _StopEnv() { + delete _environment; + _environment = nullptr; + + context->Exit(); +} + +void _DeleteIsolate() { + delete isolate_data; + isolate_data = nullptr; + + delete _handle_scope_wrapper; + _handle_scope_wrapper = nullptr; + + node_isolate->Exit(); + + delete locker; + locker = nullptr; + + { + Mutex::ScopedLock scoped_lock(node_isolate_mutex); + node_isolate->Dispose(); + node_isolate = nullptr; + } + + + delete allocator; + allocator = nullptr; +} + +void _DeinitV8() { + if (trace_enabled) { + v8_platform.StopTracingAgent(); + } + v8_initialized = false; + V8::Dispose(); + + // uv_run cannot be called from the time before the beforeExit callback + // runs until the program exits unless the event loop has any referenced + // handles after beforeExit terminates. This prevents unrefed timers + // that happen to terminate during shutdown from being run unsafely. + // Since uv_run cannot be called, uv_async handles held by the platform + // will never be fully cleaned up. + v8_platform.Dispose(); +} + +} // namespace deinitialize + + +namespace initialize { + +void _InitV8() { + v8_platform.Initialize(v8_thread_pool_size); + // Enable tracing when argv has --trace-events-enabled. + if (trace_enabled) { + fprintf(stderr, "Warning: Trace event is an experimental feature " + "and could change at any time.\n"); + v8_platform.StartTracingAgent(); + } + V8::Initialize(); + node::performance::performance_v8_start = PERFORMANCE_NOW(); + v8_initialized = true; +} + +int _CreateIsolate() { Isolate::CreateParams params; - ArrayBufferAllocator allocator; - params.array_buffer_allocator = &allocator; + allocator = new ArrayBufferAllocator(); + params.array_buffer_allocator = allocator; #ifdef NODE_ENABLE_VTUNE_PROFILING params.code_event_handler = vTune::GetVtuneCodeEventHandler(); #endif Isolate* const isolate = Isolate::New(params); - if (isolate == nullptr) + if (isolate == nullptr) { return 12; // Signal internal error. + } isolate->AddMessageListener(OnMessage); isolate->SetAbortOnUncaughtExceptionCallback(ShouldAbortOnUncaughtException); @@ -4424,49 +4611,34 @@ inline int Start(uv_loop_t* event_loop, node_isolate = isolate; } - int exit_code; - { - Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - IsolateData isolate_data( - isolate, - event_loop, - v8_platform.Platform(), - allocator.zero_fill_field()); - if (track_heap_objects) { - isolate->GetHeapProfiler()->StartTrackingHeapObjects(true); - } - exit_code = Start(isolate, &isolate_data, argc, argv, exec_argc, exec_argv); - } - - { - Mutex::ScopedLock scoped_lock(node_isolate_mutex); - CHECK_EQ(node_isolate, isolate); - node_isolate = nullptr; - } - - isolate->Dispose(); - - return exit_code; + return 0; } -int Start(int argc, char** argv) { - atexit([] () { uv_tty_reset_mode(); }); - PlatformInit(); - node::performance::performance_node_start = PERFORMANCE_NOW(); +void _CreateInitialEnvironment() { + locker = new Locker(node_isolate); + node_isolate->Enter(); + _handle_scope_wrapper = new HandleScopeHeapWrapper(node_isolate); - CHECK_GT(argc, 0); + isolate_data = new IsolateData( + node_isolate, + uv_default_loop(), + v8_platform.Platform(), + allocator->zero_fill_field()); - // Hack around with the argv pointer. Used for process.title = "blah". - argv = uv_setup_args(argc, argv); + if (track_heap_objects) { + node_isolate->GetHeapProfiler()->StartTrackingHeapObjects(true); + } - // This needs to run *before* V8::Initialize(). The const_cast is not - // optional, in case you're wondering. - int exec_argc; - const char** exec_argv; - Init(&argc, const_cast(argv), &exec_argc, &exec_argv); + // TODO(justus-hildebrand): in the initial Start functions, + // two handle scopes were created + // (one in Start() 2 and one in Start() 3). Currently, we have no idea why. + // HandleScope handle_scope(isolate); + context = NewContext(node_isolate); + context->Enter(); + _environment = new Environment(isolate_data, context); +} +void _ConfigureOpenSsl() { #if HAVE_OPENSSL { std::string extra_ca_certs; @@ -4482,45 +4654,359 @@ int Start(int argc, char** argv) { // OpenSSL's pool. V8::SetEntropySource(crypto::EntropySource); #endif // HAVE_OPENSSL +} - v8_platform.Initialize(v8_thread_pool_size); - // Enable tracing when argv has --trace-events-enabled. - if (trace_enabled) { - fprintf(stderr, "Warning: Trace event is an experimental feature " - "and could change at any time.\n"); - v8_platform.StartTracingAgent(); +int _StartEnv(int argc, + const char* const* argv, + int v8_argc, + const char* const* v8_argv, + const bool evaluate_stdin) { + _environment->Start(argc, argv, v8_argc, v8_argv, v8_is_profiling); + + const char* path = argc > 1 ? argv[1] : nullptr; + StartInspector(_environment, path, debug_options); + + if (debug_options.inspector_enabled() && + !v8_platform.InspectorStarted(_environment)) { + return 12; // Signal internal error. } - V8::Initialize(); - node::performance::performance_v8_start = PERFORMANCE_NOW(); - v8_initialized = true; - const int exit_code = - Start(uv_default_loop(), argc, argv, exec_argc, exec_argv); - if (trace_enabled) { - v8_platform.StopTracingAgent(); + + _environment->set_abort_on_uncaught_exception(abort_on_uncaught_exception); + + if (no_force_async_hooks_checks) { + _environment->async_hooks()->no_force_checks(); } - v8_initialized = false; - V8::Dispose(); - // uv_run cannot be called from the time before the beforeExit callback - // runs until the program exits unless the event loop has any referenced - // handles after beforeExit terminates. This prevents unrefed timers - // that happen to terminate during shutdown from being run unsafely. - // Since uv_run cannot be called, uv_async handles held by the platform - // will never be fully cleaned up. - v8_platform.Dispose(); + { + Environment::AsyncCallbackScope callback_scope(_environment); + _environment->async_hooks()->push_async_ids(1, 0); + LoadEnvironment(_environment, evaluate_stdin); + _environment->async_hooks()->pop_async_id(1); + } + + _environment->set_trace_sync_io(trace_sync_io); - delete[] exec_argv; - exec_argv = nullptr; + return 0; +} + +} // namespace initialize + +int Initialize(const std::string& program_name, + const std::vector& node_args, + const bool evaluate_stdin) { + cmd_args = new CmdArgs(program_name, node_args); + return Initialize(cmd_args->argc, cmd_args->argv, evaluate_stdin); +} +int Initialize(int argc, const char** argv, const bool evaluate_stdin) { + atexit([] () { uv_tty_reset_mode(); }); + PlatformInit(); + node::performance::performance_node_start = PERFORMANCE_NOW(); + + // Hack around with the argv pointer. Used for process.title = "blah --args". + // argv won't be modified + const char** custom_argv = const_cast( + uv_setup_args(argc, const_cast(argv))); + + // This needs to run *before* V8::Initialize(). + // Init() puts the v8 specific cmd args in exec_argc and exec_argv. + int exec_argc = 0; + const char** exec_argv = nullptr; + Init(&argc, custom_argv, &exec_argc, &exec_argv); + + initialize::_ConfigureOpenSsl(); + initialize::_InitV8(); + + auto exit_code = initialize::_CreateIsolate(); + if (exit_code != 0) { + return exit_code; + } + + initialize::_CreateInitialEnvironment(); + exit_code = initialize::_StartEnv(argc, + custom_argv, + exec_argc, + exec_argv, + evaluate_stdin); return exit_code; } -// Call built-in modules' _register_ function to -// do module registration explicitly. -void RegisterBuiltinModules() { -#define V(modname) _register_##modname(); - NODE_BUILTIN_MODULES(V) -#undef V +int Deinitialize() { + // Empty event queue + // TODO(cmfcmf): Investigate when this is needed. + // Evaluate("process.exit();"); + // while (ProcessEvents()) { } + + auto exit_code = deinitialize::_ExitV8(); + + deinitialize::_StopEnv(); + deinitialize::_DeleteIsolate(); + deinitialize::_DeinitV8(); + // TODO(Hannes01071995): Do we need to tear down OpenSsl? + deinitialize::_DeleteCmdArgs(); + + return exit_code; +} + +v8::MaybeLocal Run(const std::string& path) { + return Run(_environment, path); +} + +v8::MaybeLocal Run(Environment* env, const std::string& path) { + // TODO(cmfcmf) Read entire file into string. + // There is most certainly a better way + // https://stackoverflow.com/a/2602258/2560557 + std::ifstream t(path); + std::stringstream buffer; + buffer << t.rdbuf(); + + return Evaluate(_environment, buffer.str()); +} + +v8::MaybeLocal Evaluate(const std::string& js_code) { + return Evaluate(_environment, js_code); +} + +v8::MaybeLocal Evaluate(Environment* env, + const std::string& js_code) { + EscapableHandleScope scope(env->isolate()); + TryCatch try_catch(env->isolate()); + + // try_catch must be nonverbose to disable FatalException() handler, + // we will handle exceptions ourself. + try_catch.SetVerbose(false); + + // TODO(justus-hildebrand): set reasonable ScriptOrigin. + // This is used for debugging + // ScriptOrigin origin(filename); + MaybeLocal script = v8::Script::Compile( + env->context(), + v8::String::NewFromUtf8(env->isolate(), js_code.c_str()) + /*removed param: origin*/); + + if (script.IsEmpty()) { + ReportException(env, try_catch); + return MaybeLocal(); + } + + return MaybeLocal(scope.Escape(script.ToLocalChecked()->Run())); +} + +void RunEventLoop(const std::function& callback, + UvLoopBehavior behavior) { + if (_event_loop_running) { + return; // TODO(luminosuslight): return error + } + // TODO(justus-hildebrand): this was missing after building + // RunEventLoop from the Start() functions. + // We are not sure why the sealed scope is necessary. + // Please investigate. + // SealHandleScope seal(isolate); + bool more = false; + _event_loop_running = true; + request_stop = false; + do { + more = ProcessEvents(behavior); + callback(); + } while (more && !request_stop); + request_stop = false; + _event_loop_running = false; +} + +v8::MaybeLocal GetRootObject() { + return GetRootObject(_environment); +} + +v8::MaybeLocal GetRootObject(Environment* env) { + return env->context()->Global(); +} + +v8::MaybeLocal Call(v8::Local receiver, + v8::Local function, + const std::vector>& args) { + return Call(_environment, receiver, function, args); +} + +v8::MaybeLocal Call(Environment* env, + v8::Local receiver, + v8::Local function, + const std::vector>& args) { + return function->Call(receiver, + args.size(), + const_cast*>(&args[0])); +} + +v8::MaybeLocal Call(v8::Local object, + const std::string& function_name, + const std::vector>& args) { + return Call(_environment, object, function_name, args); +} + +v8::MaybeLocal Call(Environment* env, + v8::Local object, + const std::string& function_name, + const std::vector>& args) { + MaybeLocal maybe_function_name = + v8::String::NewFromUtf8(env->isolate(), function_name.c_str()); + + Local v8_function_name; + + if (!maybe_function_name.ToLocal(&v8_function_name)) { + // cannot create v8 string. + return MaybeLocal(); + } + + MaybeLocal maybe_value = object->Get(v8_function_name); + Local value; + + if (!maybe_value.ToLocal(&value)) { + // cannot get member of object + return MaybeLocal(); + } else if (!value->IsFunction()) { + // cannot execute non-function + return MaybeLocal(); + } + + return Call(env, object, v8::Local::Cast(value), args); +} + +v8::MaybeLocal IncludeModule(const std::string& name) { + return IncludeModule(_environment, name); +} + +v8::MaybeLocal IncludeModule(Environment* env, + const std::string& name) { + MaybeLocal maybe_arg = + v8::String::NewFromUtf8(env->isolate(), name.c_str()); + + Local arg; + + if (!maybe_arg.ToLocal(&arg)) { + // cannot create v8 string + return MaybeLocal(); + } + + Local root_object; + + if (!GetRootObject(env).ToLocal(&root_object)) { + // cannot get root object + return MaybeLocal(); + } + + std::vector> args = { arg }; + + MaybeLocal maybe_module = Call(env, root_object, "require", args); + Local module; + + if (!maybe_module.ToLocal(&module)) { + // cannot get module + return MaybeLocal(); + } + + return MaybeLocal(Local::Cast(module)); +} + +v8::MaybeLocal GetValue(v8::Local object, + const std::string& value_name) { + return GetValue(_environment, object, value_name); +} + +v8::MaybeLocal GetValue(Environment* env, + v8::Local object, + const std::string& value_name) { + MaybeLocal maybe_key = + v8::String::NewFromUtf8(env->isolate(), value_name.c_str()); + + Local key; + + if (!maybe_key.ToLocal(&key)) { + // cannot create v8::String + return MaybeLocal(); + } + + return object->Get(context, key); +} + +void RegisterModule(const std::string& name, + const addon_context_register_func& callback, + void* priv, + const std::string& target) { + RegisterModule(_environment, name, callback, priv, target); +} + +void RegisterModule(Environment* env, + const std::string& name, + const addon_context_register_func& callback, + void* priv, + const std::string& target) { + node::node_module* module = new node::node_module(); + + module->nm_version = NODE_MODULE_VERSION; + module->nm_flags = NM_F_BUILTIN; + module->nm_filename = __FILE__; + module->nm_context_register_func = callback; + module->nm_modname = name.c_str(); + module->nm_priv = priv; + + node_module_register(module); + + if (target != "") { + Evaluate(env, "const " + target + " = process.binding('" + name + "')"); + } +} + +void RegisterModule(const std::string& name, + const std::map& module_functions, + const std::string& target) { + RegisterModule(_environment, + name, + module_functions, + target); +} + +void RegisterModule(Environment* env, + const std::string& name, + const std::map& module_functions, + const std::string& target) { + auto map_on_heap = new const std::map(module_functions); + + RegisterModule(env, + name, + node::_RegisterModuleCallback, + const_cast*>(map_on_heap), + target); +} + +void StopEventLoop() { + if (!_event_loop_running) { + return; + } + request_stop = true; +} + +bool ProcessEvents(UvLoopBehavior behavior) { + return TickEventLoop(_environment, behavior); +} + +int Start(int argc, char** argv) { + auto exit_code = Initialize(argc, const_cast(argv), true); + if (exit_code != 0) { + return exit_code; + } + + { + SealHandleScope seal(_environment->isolate()); + PERFORMANCE_MARK(_environment, LOOP_START); + RunEventLoop([] () {}, UvLoopBehavior::RUN_DEFAULT); + PERFORMANCE_MARK(_environment, LOOP_EXIT); + } + exit_code = Deinitialize(); + + return exit_code; } } // namespace node diff --git a/src/node.h b/src/node.h index cb4346893014ef..85394bc1a7ac40 100644 --- a/src/node.h +++ b/src/node.h @@ -248,7 +248,8 @@ NODE_EXTERN Environment* CreateEnvironment(IsolateData* isolate_data, int exec_argc, const char* const* exec_argv); -NODE_EXTERN void LoadEnvironment(Environment* env); +NODE_EXTERN void LoadEnvironment(Environment* env, + const bool evaluate_stdin = true); NODE_EXTERN void FreeEnvironment(Environment* env); NODE_EXTERN MultiIsolatePlatform* CreatePlatform( diff --git a/src/node_lib.h b/src/node_lib.h new file mode 100644 index 00000000000000..0d5baedf4f8953 --- /dev/null +++ b/src/node_lib.h @@ -0,0 +1,457 @@ +#ifndef SRC_NODE_LIB_H_ +#define SRC_NODE_LIB_H_ + +#include +#include +#include +#include +#include +#include "v8.h" +#include "uv.h" +#include "node.h" + +namespace node { + +namespace internal { // internals, provided for experienced users + +/** + * @brief Returns the `v8::Isolate` for Node.js. + * + * Returns a pointer to the currently used `v8::Isolate`, if the Node.js engine + * is initialized already. + * *Important* Use with caution, changing this object might break Node.js. + * @return Pointer to the `v8::Isolate`. + */ +v8::Isolate* isolate(); + +/** + * @brief Returns the `node::Environment` for Node.js. + * + * Returns a pointer to the currently used `node::Environment`, if the Node.js + * engine is initialized already. + * *Important* Use with caution, changing this object might break Node.js. + * @return Pointer to the `node::Environment`. + */ +Environment* environment(); + +} // namespace internal + +/** + * @brief Configures the uv loop behavior, which is used within the Node.js + * event loop. + * + * Contains various behavior patterns for the uv loop, which is used within + * the Node.js event loop. + * *Important*: Contains the same values as `uv_run_mode`. + */ +enum class UvLoopBehavior : int { + RUN_DEFAULT = UV_RUN_DEFAULT, + ///< Processes events as long as events are available. + + RUN_ONCE = UV_RUN_ONCE, + ///< Processes events once from the uv loop. + ///< If there are currently no events, the loop will wait until at least + ///< one event appeared. + + RUN_NOWAIT = UV_RUN_NOWAIT, + ///< Processes events once from the uv loop. If there are currently no events, + ///< the loop will *not* wait und return immediately. +}; + +/** + * @brief Indicates if the Node.js event loop is executed by `RunEventLoop`. + * @return True, if the Node.js event loop is executed by `RunEventLoop`. + * False otherwise. + */ +bool eventLoopIsRunning(); + +/********************************************************* + * Start Node.js engine + *********************************************************/ + +/** + * @brief Starts the Node.js engine without executing a concrete script. + * + * Starts the Node.js engine by executing bootstrap code. + * This is required in order to load scripts (e.g. `Run`) or evaluate + * JavaScript code (e.g. `Evaluate`). + * Additionally, Node.js will not process any pending events caused by the + * JavaScript execution as long as `ProcessEvents` or `RunEventLoop` is + * not called. + * @param program_name The name for the Node.js application. + * @param node_args List of arguments for the Node.js engine. + * @param evaluate_stdin Controls whether stdin is evaluated. + * @return Potential errors are indicated by a return value not equal to 0. + */ +NODE_EXTERN int Initialize(const std::string& program_name = "node_lib", + const std::vector& node_args = {}, + const bool evaluate_stdin = false); + +/** + * @brief Starts the Node.js engine. + * + * Starts the Node.js engine by executing bootstrap code. + * This is required in order to load scripts (e.g. `Run`) or evaluate + * JavaScript code (e.g. `Evaluate`). + * Additionally, Node.js will not process any pending events caused by the + * JavaScript execution as long as `ProcessEvents` or `RunEventLoop` is + * not called. + * @param argc The number of arguments. + * @param argv List of arguments for the Node.js engine, + * where the first argument needs to be the program name. + * The number of arguments must correspond to argc. + * @param evaluate_stdin Controls whether stdin is evaluated. + * @return Potential errors are indicated by a return value not equal to 0. + */ +NODE_EXTERN int Initialize(int argc, + const char** argv, + const bool evaluate_stdin = false); + + +/** + * @brief Stops the Node.js engine and destroys all current state. + * + * Stops the Node.js engine. + * This is done in two steps: + * 1. Issues the Node.js event loop to no longer accept any incoming events. + * 2. Waits for the event loop to be empty and then executes clean up code. + */ +NODE_EXTERN int Deinitialize(); + +/********************************************************* + * Handle JavaScript events + *********************************************************/ + +/** + * @brief Executes the Node.js event loop once. + * + * Processes all currently pending events in the Node.js event loop. + * This method returns immediately if there are no pending events. + * @param behavior The uv event loop behavior. + * @return True, if more events need to be processed. False otherwise. + */ +NODE_EXTERN bool ProcessEvents( + UvLoopBehavior behavior = UvLoopBehavior::RUN_NOWAIT); + +/** + * @brief Starts the execution of the Node.js event loop. Calling the given + * callback once per loop tick. + * + * Executes the Node.js event loop as long as events keep coming. + * Once per loop execution, after events were processed, the given callback + * is executed. The event loop can be paused by calling `StopEventLoop`. + * @param behavior The uv event loop behavior. + * @param callback The callback, which should be executed periodically while + * the calling thread is blocked. + */ +NODE_EXTERN void RunEventLoop( + const std::function & callback, + UvLoopBehavior behavior = UvLoopBehavior::RUN_NOWAIT); + +/********************************************************* + * Stop Node.js engine + *********************************************************/ + +/** + * @brief Issues the Node.js event loop to stop. + * + * Issues the Node.js event loop to stop. + * The event loop will finish its current execution. This means, that the loop + * is not guaranteed to have stopped when this method returns. + * The execution can be resumed by using `RunEventLoop` again. + */ +NODE_EXTERN void StopEventLoop(); + +/********************************************************* + * Basic operations + *********************************************************/ + +/** + * @brief Executes the content of a given JavaScript file. + * + * Loads and executes the content of the given file. + * This method returns after the script was evaluated once. + * This means, that any pending events will not be processed as long as + * `ProcessEvents` or `RunEventLoop` is not called. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param path The path to the JavaScript file. + * @return The return value of the given JavaScript file. + */ +NODE_EXTERN v8::MaybeLocal Run(const std::string& path); + +/** + * @brief Executes the content of a given JavaScript file. + * + * Loads and executes the content of the given file. + * This method returns after the script was evaluated once. + * This means, that any pending events will not be processed as long as + * `ProcessEvents` or `RunEventLoop` is not called. + * @param env The environment where this call should be executed. + * @param path The path to the JavaScript file. + * @return The return value of the given JavaScript file. + */ +NODE_EXTERN v8::MaybeLocal Run(Environment* env, + const std::string& path); + +/** + * @brief Evaluates the given JavaScript code. + * + * Parses and runs the given JavaScipt code. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param java_script_code The code to evaluate. + * @return The return value of the evaluated code. + */ +NODE_EXTERN v8::MaybeLocal Evaluate(const std::string& js_code); + +/** + * @brief Evaluates the given JavaScript code. + * + * Parses and runs the given JavaScipt code. + * @param env The environment where this call should be executed. + * @param java_script_code The code to evaluate. + * @return The return value of the evaluated code. + */ +NODE_EXTERN v8::MaybeLocal Evaluate(Environment* env, + const std::string& js_code); + +/** + * @brief Returns the JavaScript root object. + * + * Returns the global root object for the current JavaScript context. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @return The global root object. + */ +NODE_EXTERN v8::MaybeLocal GetRootObject(); +/** + * @brief Returns the JavaScript root object. + * + * Returns the global root object for the current JavaScript context. + * @param env The environment where this call should be executed. + * @return The global root object. + */ +NODE_EXTERN v8::MaybeLocal GetRootObject(Environment* env); + +/** + * @brief Registers a native C++ module. + * + * Adds a native module to the Node.js engine. + * The module is initialized within the given callback. Additionally, private + * data can be included in the module using the priv pointer. + * The module can be used in JavaScript by calling + * `let cpp_module = process.binding('module_name')`. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param name The name for the module. + * @param callback The method, which initializes the module (e.g. by adding + * methods to the module). + * @param priv Any private data, which should be included within the module. + * @param target The name for the module within the JavaScript context. (e.g. + * `const target = process.binding(module_name)`) If empty, the module + * will *not* be registered within the global JavaScript context automatically. + */ +NODE_EXTERN void RegisterModule(const std::string& name, + const addon_context_register_func& callback, + void* priv = nullptr, + const std::string& target = ""); +/** + * @brief Registers a native C++ module. + * + * Adds a native module to the Node.js engine. + * The module is initialized within the given callback. Additionally, private + * data can be included in the module using the priv pointer. + * The module can be used in JavaScript by calling + * `let cpp_module = process.binding('module_name')`. + * @param env The environment where this call should be executed. + * @param name The name for the module. + * @param callback The method, which initializes the module (e.g. by adding + * methods to the module). + * @param priv Any private data, which should be included within the module. + * @param target The name for the module within the JavaScript context. (e.g. + * `const target = process.binding(module_name)`) If empty, the module + * will *not* be registered within the global JavaScript context automatically. + */ +NODE_EXTERN void RegisterModule(Environment* env, + const std::string& name, + const addon_context_register_func& callback, + void* priv = nullptr, + const std::string& target = ""); + +/** + * @brief Registers a native C++ module. + * + * Adds a native module to the Node.js engine. + * Additionally, this method adds the given methods to the module. + * The module can be used in JavaScript by calling + * `let cpp_module = process.binding('module_name')`. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param name The name for the module. + * @param module_functions A list of functions and their names for the module. + * @param target The name for the module within the JavaScript context. (e.g. + * `const target = process.binding(module_name)`) If empty, the module + * will *not* be registered within the global JavaScript context automatically. + */ +NODE_EXTERN void RegisterModule( + const std::string& name, + const std::map& module_functions, + const std::string& target = ""); + +/** + * @brief Registers a native C++ module. + * + * Adds a native module to the Node.js engine. + * Additionally, this method adds the given methods to the module. + * The module can be used in JavaScript by calling + * `let cpp_module = process.binding('module_name')`. + * @param env The environment where this call should be executed. + * @param name The name for the module. + * @param module_functions A list of functions and their names for the module. + * @param target The name for the module within the JavaScript context. (e.g. + * `const target = process.binding(module_name)`) If empty, the module + * will *not* be registered within the global JavaScript context automatically. + */ +NODE_EXTERN void RegisterModule( + Environment* env, + const std::string& name, + const std::map& module_functions, + const std::string& target = ""); + + +/********************************************************* + * Convenience operations + *********************************************************/ + +/** + * @brief Adds a NPM module to the current JavaScript context. + * + * Adds a given NPM module to the JavaScript context. + * This is achieved by calling `require('module_name')`. + * *Important* Make sure the NPM module is installed before using this method. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param name The name of the NPM module. + * When using just the modules name, the "node_modules" directory should be + * located within the working directory. You can also load modules from + * different locations by providing the full path to the module. + * @return The export object of the NPM module. + */ +NODE_EXTERN v8::MaybeLocal IncludeModule(const std::string& name); + +/** + * @brief Adds a NPM module to the current JavaScript context. + * + * Adds a given NPM module to the JavaScript context. + * This is achieved by calling `require('module_name')`. + * *Important* Make sure the NPM module is installed before using this method. + * @param env The environment where this call should be executed. + * @param name The name of the NPM module. + * When using just the modules name, the "node_modules" directory should be + * located within the working directory. You can also load modules from + * different locations by providing the full path to the module. + * @return The export object of the NPM module. + */ +NODE_EXTERN v8::MaybeLocal IncludeModule(Environment* env, + const std::string& name); + +/** + * @brief Returns a member of the given object. + * + * Returns a member of the given object, specified by the members name. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param object The container for the requested value. + * @param value_name The name of the requested value. + * @return The requested value. + */ +NODE_EXTERN v8::MaybeLocal GetValue(v8::Local object, + const std::string& value_name); + +/** + * @brief Returns a member of the given object. + * + * Returns a member of the given object, specified by the members name. + * @param env The environment where this call should be executed. + * @param object The container for the requested value. + * @param value_name The name of the requested value. + * @return The requested value. + */ +NODE_EXTERN v8::MaybeLocal GetValue(Environment* env, + v8::Local object, + const std::string& value_name); + +/** + * @brief Calls a method on a given object. + * + * Calls a method on a given object. + * The function is retrieved by using the functions name. + * Additionally, a list of parameters is passed to the called function. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param object The container of the called function. + * @param function_name The name of the function to call. + * @param args The parameters to pass to the called function. + * @return The return value of the called function. + */ +NODE_EXTERN v8::MaybeLocal Call( + v8::Local object, + const std::string& function_name, + const std::vector>& args = {}); + +/** + * @brief Calls a method on a given object. + * + * Calls a method on a given object. + * The function is retrieved by using the functions name. + * Additionally, a list of parameters is passed to the called function. + * @param env The environment where this call should be executed. + * @param object The container of the called function. + * @param function_name The name of the function to call. + * @param args The parameters to pass to the called function. + * @return The return value of the called function. + */ +NODE_EXTERN v8::MaybeLocal Call( + Environment* env, + v8::Local object, + const std::string& function_name, + const std::vector>& args = {}); + +/** + * @brief Calls a given method on a given object. + * + * Calls a given method on a given object. + * Additionally, a list of parameters is passed to the called function. + * Note: This method is executed in the environment created by the + * Initialize() function. + * @param object The receiver of the given function. + * @param function The function to be called. + * @param args The parameters to pass to the called function. + * @return The return value of the called function. + */ +NODE_EXTERN v8::MaybeLocal Call( + v8::Local receiver, + v8::Local function, + const std::vector>& args = {}); + +/** + * @brief Calls a given method on a given object. + * + * Calls a given method on a given object. + * Additionally, a list of parameters is passed to the called function. + * @param env The environment where this call should be executed. + * @param object The receiver of the given function. + * @param function The function to be called. + * @param args The parameters to pass to the called function. + * @return The return value of the called function. + */ +NODE_EXTERN v8::MaybeLocal Call( + Environment* env, + v8::Local receiver, + v8::Local function, + const std::vector>& args = {}); + +} // namespace node + +#endif // SRC_NODE_LIB_H_