diff --git a/.clang-format b/.clang-format index 195c9c1..12002c4 100644 --- a/.clang-format +++ b/.clang-format @@ -33,7 +33,7 @@ BraceWrapping: BeforeElse: true IndentBraces: false BreakBeforeBinaryOperators: None -BreakBeforeBraces: Attach +BreakBeforeBraces: Custom BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false ColumnLimit: 0 @@ -72,7 +72,7 @@ PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Left ReflowComments: true -SortIncludes: true +SortIncludes: false SpaceAfterCStyleCast: false SpaceBeforeAssignmentOperators: true SpaceBeforeParens: ControlStatements diff --git a/.travis.yml b/.travis.yml index 9384c58..1b426af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,10 @@ language: node_js sudo: false -# enable c++11/14 builds addons: apt: sources: [ 'ubuntu-toolchain-r-test' ] - packages: [ 'libstdc++-4.9-dev' ] + packages: [ 'libstdc++-5-dev' ] install: - node -v @@ -35,7 +34,7 @@ matrix: ## ** Builds that are published ** - # linux cfi build node v6/release + # linux cfi build node v10/release - os: linux env: BUILDTYPE=release TOOLSET=cfi CXXFLAGS="-fsanitize=cfi -fvisibility=hidden" LDFLAGS="-fsanitize=cfi" node_js: 10 @@ -51,6 +50,22 @@ matrix: - os: linux env: BUILDTYPE=debug node_js: 10 + # linux publishable node v12/release + - os: linux + env: BUILDTYPE=release + node_js: 12 + # linux publishable node v12/debug + - os: linux + env: BUILDTYPE=debug + node_js: 12 + # linux publishable node v13/release + - os: linux + env: BUILDTYPE=release + node_js: 13 + # linux publishable node v13/debug + - os: linux + env: BUILDTYPE=debug + node_js: 13 # osx publishable node v10/release - os: osx osx_image: xcode9.3 @@ -61,7 +76,7 @@ matrix: osx_image: xcode9.3 env: BUILDTYPE=release node_js: 12 - # linux sanitizer build node v6/debug + # linux sanitizer build node v10/debug - os: linux env: BUILDTYPE=debug TOOLSET=asan node_js: 10 @@ -71,7 +86,8 @@ matrix: - make sanitize # Overrides `before_script` (tests are already run in `make sanitize`) before_script: - # osx sanitizer build node v6/debug + - true + # osx sanitizer build node v10/debug - os: osx env: BUILDTYPE=debug TOOLSET=asan node_js: 10 @@ -81,7 +97,7 @@ matrix: - make sanitize # Overrides `before_script` (tests are already run in `make sanitize`) before_script: - + - true ## ** Builds that do not get published ** # g++ build (default builds all use clang++) @@ -100,6 +116,7 @@ matrix: - make ${BUILDTYPE} # Overrides `script` to disable publishing script: + - true # Coverage build - os: linux env: BUILDTYPE=debug CXXFLAGS="--coverage" LDFLAGS="--coverage" @@ -108,8 +125,11 @@ matrix: script: - export PATH=$(pwd)/mason_packages/.link/bin/:${PATH} - which llvm-cov - - pip install --user codecov - - codecov --gcov-exec "llvm-cov gcov -l" + - curl -S -f https://codecov.io/bash -o codecov + - chmod +x codecov + # Workaround until we can avoid problem after https://github.com/travis-ci/travis-build/pull/1263 lands + - PATH=$(echo "$PATH" | sed 's/.\/node_modules\/.bin://') + - ./codecov -x "llvm-cov gcov" -Z # Clang format build - os: linux # can be generic since we don't need nodejs to run formatting @@ -123,8 +143,10 @@ matrix: - make format # Overrides `before_script`, no need to run tests before_script: + - true # Overrides `script` to disable publishing script: + - true # Clang tidy build - os: linux env: CLANG_TIDY @@ -138,5 +160,7 @@ matrix: - make tidy # Overrides `before_script`, no need to run tests before_script: + - true # Overrides `script` to disable publishing script: + - true diff --git a/Makefile b/Makefile index ba7bd3b..cec28e3 100644 --- a/Makefile +++ b/Makefile @@ -24,10 +24,10 @@ export WERROR ?= true # just typing `make` will call `make release` default: release -node_modules/nan: +node_modules/node-addon-api: npm install --ignore-scripts -mason_packages/headers: node_modules/nan +mason_packages/headers: node_modules/node-addon-api node_modules/.bin/mason-js install mason_packages/.link/include: mason_packages/headers diff --git a/README.md b/README.md index b59609a..3ecb144 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ ![dancing skel](https://mapbox.s3.amazonaws.com/cpp-assets/node-cpp-skel-readme_blue.png) +[![Build Status](https://travis-ci.org/mapbox/node-cpp-skel.svg?branch=master)](https://travis-ci.org/mapbox/node-cpp-skel) +[![codecov](https://codecov.io/gh/mapbox/node-cpp-skel/branch/master/graph/badge.svg)](https://codecov.io/gh/mapbox/node-cpp-skel) -A skeleton for binding C++ libraries to Node.js using [Nan](https://github.com/nodejs/nan). This is a small, helper repository that generates simple `HelloWorld` Javascript example constructors. The examples have a number of methods to demonstrate different ways to use Nan for building particular types of functionality (i.e. asynchronous functions). Use this repo as both a template for your own code as well as a learning tool if you're just starting to develop Node/C++ Addons. +A skeleton for building a C++ addon for Node.js. This is a small, helper repository that generates simple `HelloWorld` Javascript example constructors. The examples have a number of methods to demonstrate different ways to use the Node C+ API for building particular types of functionality (i.e. asynchronous functions). Use this repo as both a template for your own code as well as a learning tool if you're just starting to develop Node/C++ Addons. **Why port C++ to Node.js?**. That's a great question! C++ is a high performance language that allows you to execute operations without clogging up the event loop. Node.js is single-threaded, which blocks execution. Even in highly optimized javascript code it may be impossible to improve performance. Passing heavy operations into C++ and subsequently into C++ workers can greatly improve the overall runtime of the code. Porting C++ code to Node.js is also referred to as creating an ["Addon"](https://github.com/mapbox/cpp/blob/master/node-cpp.md). -**Nan**: Nan is used in many C++ => Node.js port projects, such as [node-mapnik](https://github.com/mapnik/node-mapnik), [node-osrm](https://github.com/Project-OSRM/node-osrm), and [node-osmium](https://github.com/osmcode/node-osmium). More examples of how to port C++ libraries to node can be found at [nodejs.org/api/addons.html](https://nodejs.org/api/addons.html). See https://nodesource.com/blog/c-add-ons-for-nodejs-v4/ for a detailed summary of the origins of Nan. - -[![Build Status](https://travis-ci.org/mapbox/node-cpp-skel.svg?branch=master)](https://travis-ci.org/mapbox/node-cpp-skel) -[![codecov](https://codecov.io/gh/mapbox/node-cpp-skel/branch/master/graph/badge.svg)](https://codecov.io/gh/mapbox/node-cpp-skel) +More examples of how to port C++ libraries to node can be found at [nodejs.org/api/addons.html](https://nodejs.org/api/addons.html). # What's in the box? :package: diff --git a/binding.gyp b/binding.gyp index 7c3dbe5..7460e14 100644 --- a/binding.gyp +++ b/binding.gyp @@ -15,7 +15,7 @@ # It's a variable to make easy to pass to # cflags (linux) and xcode (mac) 'system_includes': [ - "-isystem <(module_root_dir)/> ${SUPPRESSION_FILE} echo "leak:node::CreateEnvironment" >> ${SUPPRESSION_FILE} echo "leak:node::Start" >> ${SUPPRESSION_FILE} echo "leak:node::Init" >> ${SUPPRESSION_FILE} +# Suppress leak related to https://github.com/libuv/libuv/pull/2480 +echo "leak:uv__set_process_title_platform_init" >> ${SUPPRESSION_FILE} export ASAN_SYMBOLIZER_PATH=$(pwd)/mason_packages/.link/bin/llvm-symbolizer export MSAN_SYMBOLIZER_PATH=$(pwd)/mason_packages/.link/bin/llvm-symbolizer export UBSAN_OPTIONS=print_stacktrace=1 diff --git a/src/cpu_intensive_task.hpp b/src/cpu_intensive_task.hpp new file mode 100644 index 0000000..2723f5f --- /dev/null +++ b/src/cpu_intensive_task.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +namespace detail { + +// simulate CPU intensive task +inline std::unique_ptr> do_expensive_work(std::string const& name, bool louder) +{ + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::string str = "...threads are busy async bees...hello " + name; + std::unique_ptr> result = std::make_unique>(str.begin(), str.end()); + if (louder) + { + std::string extra{"!!!!"}; + std::copy(extra.c_str(), extra.c_str() + extra.length(), back_inserter(*result)); + } + return result; +} + +} // namespace detail diff --git a/src/module.cpp b/src/module.cpp index 12af606..f01d540 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -2,26 +2,24 @@ #include "object_sync/hello.hpp" #include "standalone/hello.hpp" #include "standalone_async/hello_async.hpp" -#include +#include // #include "your_code.hpp" -// "target" is a magic var that NAN_MODULE_INIT passes into a module's scope. -// When you write things to target, they become available to call from -// Javascript world. -NAN_MODULE_INIT(init) { - +Napi::Object init(Napi::Env env, Napi::Object exports) +{ // expose hello method - Nan::SetMethod(target, "hello", standalone::hello); + exports.Set(Napi::String::New(env, "hello"), Napi::Function::New(env, standalone::hello)); // expose helloAsync method - Nan::SetMethod(target, "helloAsync", standalone_async::helloAsync); + exports.Set(Napi::String::New(env, "helloAsync"), Napi::Function::New(env, standalone_async::helloAsync)); // expose HelloObject class - object_sync::HelloObject::Init(target); + object_sync::HelloObject::Init(env, exports); // expose HelloObjectAsync class - object_async::HelloObjectAsync::Init(target); + object_async::HelloObjectAsync::Init(env, exports); + return exports; /** * You may have noticed there are multiple "hello" functions as part of this * module. @@ -36,13 +34,8 @@ NAN_MODULE_INIT(init) { // Include your .hpp file at the top of this file. } -// Here we initialize the module (we only do this once) -// by attaching the init function to the module. This invokes -// a variety of magic from inside nodejs core that we don't need to -// worry about, but if you care the details are at https://github.com/nodejs/node/blob/34d1b1144e1af8382dad71c28c8d956ebf709801/src/node.h#L431-L518 -// We mark this NOLINT to avoid the clang-tidy checks -// warning about code inside nodejs that we don't control and can't -// directly change to avoid the warning. +// Initialize the module (we only do this once) +// Mark this NOLINT to avoid the clang-tidy checks // NODE_GYP_MODULE_NAME is the name of our module as defined in 'target_name' // variable in the 'binding.gyp', which is passed along as a compiler define -NODE_MODULE(NODE_GYP_MODULE_NAME, init) // NOLINT +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init) // NOLINT diff --git a/src/module_utils.hpp b/src/module_utils.hpp index ea6992f..6266441 100644 --- a/src/module_utils.hpp +++ b/src/module_utils.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include namespace utils { @@ -10,35 +10,24 @@ namespace utils { * throwing errors. * Usage: * -* v8::Local callback; -* return CallbackError("error message", callback); // "return" is important to -* prevent duplicate callbacks from being fired! -* -* -* "inline" is important here as well. See for more contex: -* - https://github.com/mapbox/cpp/blob/master/glossary.md#inline-keyword -* - https://github.com/mapbox/node-cpp-skel/pull/52#discussion_r126847394 for -* context +* Napi::CallbackInfo info; +* return CallbackError("error message", info); * */ -inline void CallbackError(std::string message, v8::Local func) { - Nan::Callback cb(func); - v8::Local argv[1] = {Nan::Error(message.c_str())}; - Nan::Call(cb, 1, argv); +inline Napi::Value CallbackError(std::string const& message, Napi::CallbackInfo const& info) +{ + Napi::Object obj = Napi::Object::New(info.Env()); + obj.Set("message", message); + auto func = info[info.Length() - 1].As(); + // ^^^ here we assume that info has a valid callback function + // TODO: consider changing either method signature or adding internal checks + return func.Call({obj}); } +} // namespace utils -inline Nan::MaybeLocal NewBufferFrom(std::unique_ptr&& ptr) { - Nan::MaybeLocal res = Nan::NewBuffer( - &(*ptr)[0], - ptr->size(), - [](char*, void* hint) { - delete static_cast(hint); - }, - ptr.get()); - if (!res.IsEmpty()) { - ptr.release(); // NOLINT ignore bugprone-unused-return-value - } - return res; -} +namespace gsl { +template +using owner = T; +} // namespace gsl -} // namespace utils +// ^^^ type alias required for clang-tidy (cppcoreguidelines-owning-memory) diff --git a/src/object_async/hello_async.cpp b/src/object_async/hello_async.cpp index 4bf6a17..a36ef63 100644 --- a/src/object_async/hello_async.cpp +++ b/src/object_async/hello_async.cpp @@ -1,11 +1,12 @@ #include "hello_async.hpp" +#include "../cpu_intensive_task.hpp" #include "../module_utils.hpp" #include -#include #include #include #include +#include /** * Asynchronous class, called HelloObjectAsync @@ -35,305 +36,220 @@ */ // If this was not defined within a namespace, it would be in the global scope. -namespace object_async { - -// Custom constructor, assigns custom name arg passed in from Javascript world. -// This constructor uses member init list via the colon, aka "direct -// initialization", which is more efficient than using assignment operators. -// This constructor is using move semantics to literally "move" the value of -// name to a new place in memory (to the "name_" variable). -// This avoids copying the value and duplicating memory allocation, which can -// negatively affect performance. -HelloObjectAsync::HelloObjectAsync(std::string&& name) - : name_(name) {} - -// Triggered from Javascript world when calling "new HelloObjectAsync(name)" -NAN_METHOD(HelloObjectAsync::New) { - if (info.IsConstructCall()) { - try { - if (info.Length() >= 1) { - if (info[0]->IsString()) { - // Don't want to risk passing a null string around, which might create unpredictable behavior. - Nan::Utf8String utf8_value(info[0]); - int len = utf8_value.length(); - if (len <= 0) { - return Nan::ThrowTypeError("arg must be a non-empty string"); - } - - /** - * This line converts a V8 string to a C++ std::string. - * In the background, it triggers memory allocation (stack allocating, but std:string is also dynamically allocating memory in the heap) - * We want to avoid heap allocation to ensure more performant code. - * See https://github.com/mapbox/cpp/blob/master/glossary.md#stack-allocation - * and https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap/80113#80113 - * Also, providing the length allows the std::string constructor to avoid calculating the length internally - * and should be faster since it skips an operation. - */ - std::string name(*utf8_value, static_cast(len)); - - /** - * This line is where HelloObjectAsync takes ownership of "name" with the use of move semantics. - * Then all later usage of "name" are passed by reference (const&), but the actual home or address in memory - * will always be owned by this instance of HelloObjectAsync. Generally important to know what has ownership of an object. - * When a object/value is a member of a class (like "name"), we know the class (HelloObjectAsync) has full control of the scope of the object/value. - * This avoids the scenario of "name" being destroyed or becoming out of scope. - * - * Also, we're using "new" here to create a custom C++ class, based on node::ObjectWrap since this is a node addon. - * In this case, "new" allocates a C++ object (dynamically on the heap) and then passes ownership (control of when it gets deleted) - * to V8, the javascript engine which decides when to clean up the object based on how its’ garbage collector works. - * In other words, the memory of HelloObjectAsync is expliclty deleted via node::ObjectWrap when it's gone out of scope - * (the object needs to stay alive until the V8 garbage collector has decided it's done): - * https://github.com/nodejs/node/blob/7ec28a0a506efe9d1c03240fd028bea4a3d350da/src/node_object_wrap.h#L124 - **/ - auto self = std::make_unique(std::move(name)); // Using unique pointer to adhere to cpp core guideline: https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines-owning-memory.html - self->Wrap(info.This()); // Connects C++ object to Javascript object (this) - self.release(); // NOLINT Release the ownership of self so it can be managed by wrapper - } else { - return Nan::ThrowTypeError( - "arg must be a string"); - } - } else { - return Nan::ThrowTypeError( - "must provide string arg"); - } - } catch (const std::exception& ex) { - return Nan::ThrowTypeError(ex.what()); - } - - info.GetReturnValue().Set(info.This()); - } else { - return Nan::ThrowTypeError( - "Cannot call constructor as function, you need to use 'new' keyword"); - } -} - -// This function performs expensive allocation of std::map, querying, and string -// comparison, therefore threads are nice & busy. -// Also, notice that name is passed by reference (std::string const& name) -std::unique_ptr do_expensive_work(bool louder, std::string const& name) { - - std::map container; - std::size_t work_to_do = 100000; - - for (std::size_t i = 0; i < work_to_do; ++i) { - container.emplace(i, std::to_string(i)); - } - for (std::size_t i = 0; i < work_to_do; ++i) { - std::string const& item = container[i]; - - if (item != std::to_string(i)) { - - // AsyncHelloWorker's Execute function will take care of this error - // and return it to js-world via callback - // Marked NOLINT to avoid clang-tidy cert-err60-cpp error which we cannot - // avoid on some linux distros where std::runtime_error is not properly - // marked noexcept. Details at https://www.securecoding.cert.org/confluence/display/cplusplus/ERR60-CPP.+Exception+objects+must+be+nothrow+copy+constructible - throw std::runtime_error("Uh oh, this should never happen"); // NOLINT - } - } - - std::unique_ptr result = std::make_unique("...threads are busy async bees...hello " + name); - - if (louder) { - *result += "!!!!"; - } - - return result; -} +namespace object_async { -// This is the worker running asynchronously and calling a user-provided -// callback when done. -// Consider storing all C++ objects you need by value or by shared_ptr to keep -// them alive until done. -// Nan AsyncWorker docs: -// https://github.com/nodejs/nan/blob/master/doc/asyncworker.md -struct AsyncHelloWorker : Nan::AsyncWorker // NOLINT to disable cppcoreguidelines-special-member-functions +/* +struct AsyncHelloWorker : Napi::AsyncWorker { - - using Base = Nan::AsyncWorker; - // We explicitly delete the copy constructor and assignment operator below (even though Nan::Asyncworker) - // already does this in the base class. This allows us to have the `const std::string* name` - // pointer member without the silly g++ warning of "error: ‘struct object_async::AsyncHelloWorker’ has pointer data members [-Werror=effc++]" - AsyncHelloWorker(AsyncHelloWorker const&) = delete; - AsyncHelloWorker& operator=(AsyncHelloWorker const&) = delete; + using Base = Napi::AsyncWorker; + // ctor AsyncHelloWorker(bool louder, bool buffer, - const std::string* name, - Nan::Callback* cb) - : Base(cb, "skel:object-async-worker"), + std::string name, + Napi::Function const& cb) + : Base(cb), louder_(louder), buffer_(buffer), - name_(name) {} + name_(std::move(name)) {} // The Execute() function is getting called when the worker starts to run. // - You only have access to member variables stored in this worker. // - You do not have access to Javascript v8 objects here. - void Execute() override { - try { - result_ = do_expensive_work(louder_, *name_); - } catch (const std::exception& e) { - SetErrorMessage(e.what()); + void Execute() override + { + try + { + result_ = detail::do_expensive_work(name_, louder_); + } + catch (std::exception const& e) + { + SetError(e.what()); } } - // The HandleOKCallback() is getting called when Execute() successfully + // The OnOK() is getting called when Execute() successfully // completed. // - In case Execute() invoked SetErrorMessage("") this function is not // getting called. // - You have access to Javascript v8 objects again // - You have to translate from C++ member variables to Javascript v8 objects // - Finally, you call the user's callback with your results - void HandleOKCallback() override { - Nan::HandleScope scope; + void OnOK() override + { + Napi::HandleScope scope(Env()); + if (!Callback().IsEmpty()) + { + if (buffer_) + { + char * data = result_->data(); + std::size_t size = result_->size(); + auto buffer = Napi::Buffer::New(Env(), + data, + size, + [](Napi::Env, char*, gsl::owner*> v) { + delete v; + }, + result_.release()); + Callback().Call({Env().Null(), buffer}); + } + else + { + Callback().Call({Env().Null(), Napi::String::New(Env(), result_->data(), result_->size())}); + } + } + } - if (buffer_) { - const auto argc = 2u; - v8::Local argv[argc] = { - Nan::Null(), utils::NewBufferFrom(std::move(result_)).ToLocalChecked()}; + std::unique_ptr> result_ = nullptr; + bool const louder_; + bool const buffer_; + std::string const name_; +}; +*/ - // Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy - callback->Call(argc, static_cast*>(argv), async_resource); +// This V2 worker is overriding `GetResult` to return the arguments +// passed to the Callback invoked by the default OnOK() implementation. +// Above is alternative implementation with OnOK() method calling +// Callback with appropriate args. Both implementations use default OnError(). +struct AsyncHelloWorker_v2 : Napi::AsyncWorker +{ + using Base = Napi::AsyncWorker; + // ctor + AsyncHelloWorker_v2(bool louder, + bool buffer, + std::string name, + Napi::Function const& cb) + : Base(cb), + louder_(louder), + buffer_(buffer), + name_(std::move(name)) {} - } else { - const auto argc = 2u; - v8::Local argv[argc] = { - Nan::Null(), Nan::New(*result_).ToLocalChecked()}; + // The Execute() function is getting called when the worker starts to run. + // - You only have access to member variables stored in this worker. + // - You do not have access to Javascript v8 objects here. + void Execute() override + { + try + { + result_ = detail::do_expensive_work(name_, louder_); + } + catch (std::exception const& e) + { + SetError(e.what()); + } + } - // Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy - callback->Call(argc, static_cast*>(argv), async_resource); + std::vector GetResult(Napi::Env env) override + { + if (result_) + { + if (buffer_) + { + char* data = result_->data(); + std::size_t size = result_->size(); + auto buffer = Napi::Buffer::New(env, + data, + size, + [](Napi::Env, char*, gsl::owner*> v) { + delete v; + }, + result_.release()); + return {env.Null(), buffer}; + } + return {env.Null(), Napi::String::New(env, result_->data(), result_->size())}; } + return Base::GetResult(env); // returns an empty vector (default) } - std::unique_ptr result_ = std::make_unique(); - const bool louder_; - const bool buffer_; - // We use a pointer here to avoid copying the string data. - // This works because we know that the original string we are - // pointing to will be kept in scope/alive for the time while AsyncHelloWorker - // is using it. If we could not guarantee this then we would need to either - // copy the string or pass a shared_ptr. - const std::string* name_; + std::unique_ptr> result_ = nullptr; + bool const louder_; + bool const buffer_; + std::string const name_; }; -NAN_METHOD(HelloObjectAsync::helloAsync) { - // "info" comes from the NAN_METHOD macro, which returns differently according - // to the Node version - // "What is node::ObjectWrap???" The short version is that node::ObjectWrap - // and wrapping/unwrapping objects - // is the somewhat clumsy way it is possible to bind Node and C++. The main - // points to remember: - // - To access a class instance inside a C++ static method, you must unwrap - // the object. - // - The C++ methods must be static to make them available at startup across - // the language boundary (JS <-> C++). - auto* h = - Nan::ObjectWrap::Unwrap(info.Holder()); +Napi::FunctionReference HelloObjectAsync::constructor; // NOLINT + +HelloObjectAsync::HelloObjectAsync(Napi::CallbackInfo const& info) + : Napi::ObjectWrap(info) +{ + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + + std::size_t length = info.Length(); + if (length != 1 || !info[0].IsString()) + { + Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException(); + return; + } + name_ = info[0].As().Utf8Value(); + if (name_.empty()) + { + Napi::TypeError::New(env, "arg must be a non-empty string").ThrowAsJavaScriptException(); + } +} +Napi::Value HelloObjectAsync::helloAsync(Napi::CallbackInfo const& info) +{ bool louder = false; bool buffer = false; - // Check second argument, should be a 'callback' function. - // This allows us to set the callback so we can use it to return errors - // instead of throwing. - // Also, "info" comes from the NAN_METHOD macro, which returns differently - // according to the version of node - if (!info[1]->IsFunction()) { - return Nan::ThrowTypeError("second arg 'callback' must be a function"); + Napi::Env env = info.Env(); + if (!(info.Length() == 2 && info[1].IsFunction())) + { + Napi::TypeError::New(env, "second arg 'callback' must be a function").ThrowAsJavaScriptException(); + return env.Null(); } - v8::Local callback = info[1].As(); + + Napi::Function callback = info[1].As(); // Check first argument, should be an 'options' object - if (!info[0]->IsObject()) { - return utils::CallbackError("first arg 'options' must be an object", - callback); + if (!info[0].IsObject()) + { + return utils::CallbackError("first arg 'options' must be an object", info); } - v8::Local options = info[0].As(); + Napi::Object options = info[0].As(); // Check options object for the "louder" property, which should be a boolean // value - if (Nan::Has(options, Nan::New("louder").ToLocalChecked()).FromMaybe(false)) { - v8::Local louder_val = - Nan::Get(options, Nan::New("louder").ToLocalChecked()).ToLocalChecked(); - if (!louder_val->IsBoolean()) { - return utils::CallbackError("option 'louder' must be a boolean", - callback); - } - Nan::Maybe maybe_louder = Nan::To(louder_val); - if (maybe_louder.IsNothing()) { - return utils::CallbackError("option 'louder' must be a boolean", callback); + if (options.Has(Napi::String::New(env, "louder"))) + { + Napi::Value louder_val = options.Get(Napi::String::New(env, "louder")); + if (!louder_val.IsBoolean()) + { + return utils::CallbackError("option 'louder' must be a boolean", info); } - louder = maybe_louder.FromJust(); + louder = louder_val.As().Value(); } // Check options object for the "buffer" property, which should be a boolean // value - if (Nan::Has(options, Nan::New("buffer").ToLocalChecked()).FromMaybe(false)) { - v8::Local buffer_val = - Nan::Get(options, Nan::New("buffer").ToLocalChecked()).ToLocalChecked(); - if (!buffer_val->IsBoolean()) { - return utils::CallbackError("option 'buffer' must be a boolean", - callback); + if (options.Has(Napi::String::New(env, "buffer"))) + { + Napi::Value buffer_val = options.Get(Napi::String::New(env, "buffer")); + if (!buffer_val.IsBoolean()) + { + return utils::CallbackError("option 'buffer' must be a boolean", info); } - Nan::Maybe maybe_buffer = Nan::To(buffer_val); - if (maybe_buffer.IsNothing()) { - return utils::CallbackError("option 'buffer' must be a boolean", callback); - } - buffer = maybe_buffer.FromJust(); + buffer = buffer_val.As().Value(); } - // Create a worker instance and queues it to run asynchronously invoking the - // callback when done. - // - Nan::AsyncWorker takes a pointer to a Nan::Callback and deletes the - // pointer automatically. - // - Nan::AsyncQueueWorker takes a pointer to a Nan::AsyncWorker and deletes - // the pointer automatically. - auto cb = std::make_unique(callback); - auto worker = std::make_unique(louder, buffer, &h->name_, cb.release()); - Nan::AsyncQueueWorker(worker.release()); + auto* worker = new AsyncHelloWorker_v2{louder, buffer, name_, callback}; // NOLINT + worker->Queue(); + return info.Env().Undefined(); // NOLINT } -// Singleton -Nan::Persistent& HelloObjectAsync::create_once() { - static Nan::Persistent init; - return init; +Napi::Object HelloObjectAsync::Init(Napi::Env env, Napi::Object exports) +{ + Napi::Function func = DefineClass(env, "HelloObjectAsync", {InstanceMethod("helloAsync", &HelloObjectAsync::helloAsync)}); + // Create a peristent reference to the class constructor. This will allow + // a function called on a class prototype and a function + // called on instance of a class to be distinguished from each other. + constructor = Napi::Persistent(func); + // Call the SuppressDestruct() method on the static data prevent the calling + // to this destructor to reset the reference when the environment is no longer + // available. + constructor.SuppressDestruct(); + exports.Set("HelloObjectAsync", func); + return exports; } -void HelloObjectAsync::Init(v8::Local target) { - // A handlescope is needed so that v8 objects created in the local memory - // space (this function in this case) - // are cleaned up when the function is done running (and the handlescope is - // destroyed) - // Fun trivia: forgetting a handlescope is one of the most common causes of - // memory leaks in node.js core - // https://www.joyent.com/blog/walmart-node-js-memory-leak - Nan::HandleScope scope; - - // This is saying: - // "Node, please allocate a new Javascript string object - // inside the V8 local memory space, with the value 'HelloObjectAsync' " - v8::Local whoami = Nan::New("HelloObjectAsync").ToLocalChecked(); - - // Create the HelloObject - auto fnTp = Nan::New( - HelloObjectAsync::New, v8::Local()); // Passing the HelloObject::New method above - fnTp->InstanceTemplate()->SetInternalFieldCount(1); // It's 1 when holding the ObjectWrap itself and nothing else - fnTp->SetClassName(whoami); // Passing the Javascript string object above - - // Add custom methods here. - // This is how helloAsync() is exposed as part of HelloObjectAsync. - // This line is attaching the "helloAsync" method to a JavaScript function - // prototype. - // "helloAsync" is therefore like a property of the fnTp object - // ex: console.log(HelloObjectAsync.helloAsync) --> [Function: helloAsync] - SetPrototypeMethod(fnTp, "helloAsync", helloAsync); - - // Create an unique instance of the HelloObject function template, - // then set this unique instance to the target - const auto fn = Nan::GetFunction(fnTp).ToLocalChecked(); - create_once().Reset(fn); // calls the static &HelloObjectAsync::create_once - // method above. This ensures the instructions in - // this Init function are retained in memory even - // after this code block ends. - Nan::Set(target, whoami, fn); -} } // namespace object_async diff --git a/src/object_async/hello_async.hpp b/src/object_async/hello_async.hpp index 3daffa1..7ef2900 100644 --- a/src/object_async/hello_async.hpp +++ b/src/object_async/hello_async.hpp @@ -1,5 +1,5 @@ #pragma once -#include +#include namespace object_async { @@ -10,27 +10,18 @@ namespace object_async { * Also, this class adheres to the rule of Zero because we define no custom * destructor or copy constructor */ -class HelloObjectAsync : public Nan::ObjectWrap { - +class HelloObjectAsync : public Napi::ObjectWrap +{ public: // initializer - static void Init(v8::Local target); - - // methods required for the V8 constructor - static NAN_METHOD(New); - static Nan::Persistent& create_once(); - - // helloAsync, custom async method tied to Init of this class - // method's logic lives in ./hello.cpp - static NAN_METHOD(helloAsync); - - // C++ Constructor - // Passing the arg by rvalue reference (&&) - HelloObjectAsync(std::string&& name); + static Napi::Object Init(Napi::Env env, Napi::Object exports); + explicit HelloObjectAsync(Napi::CallbackInfo const& info); + Napi::Value helloAsync(Napi::CallbackInfo const& info); private: // member variable // specific to each instance of the class - std::string name_; + static Napi::FunctionReference constructor; + std::string name_ = ""; }; } // namespace object_async diff --git a/src/object_sync/hello.cpp b/src/object_sync/hello.cpp index b2b7142..e2a8344 100644 --- a/src/object_sync/hello.cpp +++ b/src/object_sync/hello.cpp @@ -1,5 +1,4 @@ #include "hello.hpp" - #include /** @@ -28,143 +27,47 @@ // clearly organize your application. namespace object_sync { -// Custom constructor, assigns custom name passed in from Javascript world. -// This constructor uses member init list via the semicolon, aka "direct initialization" -// which is more efficient than using assignment operators. -HelloObject::HelloObject(std::string&& name) : name_(name) {} +Napi::FunctionReference HelloObject::constructor; // NOLINT // Triggered from Javascript world when calling "new HelloObject(name)" -NAN_METHOD(HelloObject::New) { - if (info.IsConstructCall()) { - try { - if (info.Length() >= 1) { - if (info[0]->IsString()) { - // Don't want to risk passing a null string around, which might create unpredictable behavior. - Nan::Utf8String utf8_value(info[0]); - int len = utf8_value.length(); - if (len <= 0) { - return Nan::ThrowTypeError("arg must be a non-empty string"); - } - - /** - * This line converts a V8 string to a C++ std::string. - * In the background, it triggers memory allocation (stack allocating, but std:string is also dynamically allocating memory in the heap) - * We want to avoid heap allocation to ensure more performant code. - * See https://github.com/mapbox/cpp/blob/master/glossary.md#stack-allocation - * and https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap/80113#80113 - * Also, providing the length allows the std::string constructor to avoid calculating the length internally - * and should be faster since it skips an operation. - */ - std::string name(*utf8_value, static_cast(len)); - - /** - * This line is where HelloObject takes ownership of "name" with the use of move semantics. - * Then all later usage of "name" are passed by reference (const&), but the actual home or address in memory - * will always be owned by this instance of HelloObjectAsync. Generally important to know what has ownership of an object. - * When a object/value is a member of a class (like "name"), we know the class (HelloObjectAsync) has full control of the scope of the object/value. - * This avoids the scenario of "name" being destroyed or becoming out of scope. - * - * Also, we're using "new" here to create a custom C++ class, based on node::ObjectWrap since this is a node addon. - * In this case, "new" allocates a C++ object (dynamically on the heap) and then passes ownership (control of when it gets deleted) - * to V8, the javascript engine which decides when to clean up the object based on how its’ garbage collector works. - * In other words, the memory of HelloObjectAsync is expliclty deleted via node::ObjectWrap when it's gone out of scope - * (the object needs to stay alive until the V8 garbage collector has decided it's done): - * https://github.com/nodejs/node/blob/7ec28a0a506efe9d1c03240fd028bea4a3d350da/src/node_object_wrap.h#L124 - */ - auto self = std::make_unique(std::move(name)); // Using unique pointer to adhere to cpp core guideline: https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines-owning-memory.html - self->Wrap(info.This()); // Connects C++ object to Javascript object (this) - self.release(); // NOLINT Release the ownership of self so it can be managed by wrapper - } else { - return Nan::ThrowTypeError( - "arg must be a string"); - } - } else { - return Nan::ThrowTypeError( - "must provide string arg"); - } - } catch (const std::exception& ex) { - return Nan::ThrowTypeError(ex.what()); - } - - info.GetReturnValue().Set(info.This()); - } else { - return Nan::ThrowTypeError( - "Cannot call constructor as function, you need to use 'new' keyword"); +HelloObject::HelloObject(Napi::CallbackInfo const& info) + : Napi::ObjectWrap(info) +{ + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + std::size_t length = info.Length(); + if (length != 1 || !info[0].IsString()) + { + Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException(); + return; + } + name_ = info[0].As().Utf8Value(); + if (name_.empty()) + { + Napi::TypeError::New(env, "arg must be a non-empty string").ThrowAsJavaScriptException(); } } -// NAN_METHOD is applicable to methods you want to expose to JS world -NAN_METHOD(HelloObject::hello) { - /** - * Note: a HandleScope is automatically included inside NAN_METHOD. See the - * docs at NAN that say: - * 'Note that an implicit HandleScope is created for you on - * JavaScript-accessible methods so you do not need to insert one yourself.' - * at - * https://github.com/nodejs/nan/blob/2dfc5c2d19c8066903a19ced6a72c06d2c825dec/doc/scopes.md#nanhandlescope - - * "What is node::ObjectWrap???" The short version is that node::ObjectWrap - * and wrapping/unwrapping objects - * is the somewhat clumsy way it is possible to bind Node and C++. The main - * points to remember: - * - To access a class instance inside a C++ static method, you must unwrap - * the object. - * - The C++ methods must be static to make them available at startup across - * the language boundary (JS <-> C++). - */ - auto* h = Nan::ObjectWrap::Unwrap(info.Holder()); - - // "info" comes from the NAN_METHOD macro, which returns differently - // according to the version of node - info.GetReturnValue().Set( - Nan::New("...initialized an object...hello " + h->name_) - .ToLocalChecked()); -} - -// This is a Singleton, which is a general programming design concept for -// creating an instance once within a process. -Nan::Persistent& HelloObject::create_once() { - static Nan::Persistent init; - return init; +Napi::Value HelloObject::hello(Napi::CallbackInfo const& info) +{ + Napi::Env env = info.Env(); + return Napi::String::New(env, name_); } -void HelloObject::Init(v8::Local target) { - // A handlescope is needed so that v8 objects created in the local memory - // space (this function in this case) - // are cleaned up when the function is done running (and the handlescope is - // destroyed) - // Fun trivia: forgetting a handlescope is one of the most common causes of - // memory leaks in node.js core - // https://www.joyent.com/blog/walmart-node-js-memory-leak - Nan::HandleScope scope; - - // This is saying: - // "Node, please allocate a new Javascript string object - // inside the V8 local memory space, with the value 'HelloObject' " - v8::Local whoami = Nan::New("HelloObject").ToLocalChecked(); - - // Officially create the HelloObject - auto fnTp = Nan::New( - HelloObject::New, v8::Local()); // Passing the HelloObject::New method above - fnTp->InstanceTemplate()->SetInternalFieldCount(1); // It's 1 when holding the ObjectWrap itself and nothing else - fnTp->SetClassName(whoami); // Passing the Javascript string object above - - // Add custom methods here. - // This is how hello() is exposed as part of HelloObject. - // This line is attaching the "hello" method to a JavaScript function - // prototype. - // "hello" is therefore like a property of the fnTp object - // ex: console.log(HelloObject.hello) --> [Function: hello] - SetPrototypeMethod(fnTp, "helloMethod", hello); - - // Create an unique instance of the HelloObject function template, - // then set this unique instance to the target - const auto fn = Nan::GetFunction(fnTp).ToLocalChecked(); - create_once().Reset(fn); // calls the static &HelloObject::create_once method - // above. This ensures the instructions in this Init - // function are retained in memory even after this - // code block ends. - Nan::Set(target, whoami, fn); +Napi::Object HelloObject::Init(Napi::Env env, Napi::Object exports) +{ + + Napi::Function func = DefineClass(env, "HelloObject", {InstanceMethod("helloMethod", &HelloObject::hello)}); + // Create a peristent reference to the class constructor. This will allow + // a function called on a class prototype and a function + // called on instance of a class to be distinguished from each other. + constructor = Napi::Persistent(func); + // Call the SuppressDestruct() method on the static data prevent the calling + // to this destructor to reset the reference when the environment is no longer + // available. + constructor.SuppressDestruct(); + exports.Set("HelloObject", func); + return exports; } } // namespace object_sync diff --git a/src/object_sync/hello.hpp b/src/object_sync/hello.hpp index a556cfc..255992b 100644 --- a/src/object_sync/hello.hpp +++ b/src/object_sync/hello.hpp @@ -1,35 +1,24 @@ #pragma once -#include +#include namespace object_sync { /** - * HelloObject class - * This is in a header file so we can access it across other .cpp files if necessary - * Also, this class adheres to the rule of Zero because we define no custom destructor or copy constructor - */ -class HelloObject : public Nan::ObjectWrap { - + * HelloObject class + * This is in a header file so we can access it across other .cpp files if necessary + * Also, this class adheres to the rule of Zero because we define no custom destructor or copy constructor + */ +class HelloObject : public Napi::ObjectWrap +{ public: - // initializer - static void Init(v8::Local target); - - // methods required for the V8 constructor (?) - static NAN_METHOD(New); - static Nan::Persistent& create_once(); - - // hello, custom sync method tied to Init of this class - // method's logic lives in ./hello.cpp - static NAN_METHOD(hello); - - // C++ Constructor - // Passing the arg by rvalue reference (&&) - HelloObject(std::string&& name); + // initializers + static Napi::Object Init(Napi::Env env, Napi::Object exports); + explicit HelloObject(Napi::CallbackInfo const& info); + Napi::Value hello(Napi::CallbackInfo const& info); private: - // member variable - // specific to each instance of the class - std::string name_; + static Napi::FunctionReference constructor; + std::string name_ = ""; }; } // namespace object_sync diff --git a/src/standalone/hello.cpp b/src/standalone/hello.cpp index 2a21c0f..ce4be7a 100644 --- a/src/standalone/hello.cpp +++ b/src/standalone/hello.cpp @@ -10,20 +10,11 @@ * console.log(check); // => "hello world" */ namespace standalone { -// If this was not defined within a namespace, it would be in the global scope. -// Namespaces are used because C++ has no notion of scoped modules, so all of -// the code you write in any file could conflict with other code. -// Namespaces are generally a great idea in C++ because it helps scale and -// clearly organize your application. -// hello is a "standalone function" because it's not a class. -// If this function was not defined within a namespace, it would be in the -// global scope. -NAN_METHOD(hello) { - - // "info" comes from the NAN_METHOD macro, which returns differently - // according to the version of node - info.GetReturnValue().Set( - Nan::New("hello world").ToLocalChecked()); +Napi::Value hello(Napi::CallbackInfo const& info) +{ + Napi::Env env = info.Env(); + return Napi::String::New(env, "hello world"); } -} // namespace standalone \ No newline at end of file + +} // namespace standalone diff --git a/src/standalone/hello.hpp b/src/standalone/hello.hpp index d445cae..08d7498 100644 --- a/src/standalone/hello.hpp +++ b/src/standalone/hello.hpp @@ -1,36 +1,9 @@ #pragma once -#include -// specify #include with carrots, ex: --> look for header in global -// specify #include with quotes, ex: "hello.hpp" --> look for header in location -// relative to this file +#include -/* - - Namespace is an organizational method that helps to clearly show where a - method is coming from. - Namespaces are generally a great idea in C++ because they help us scale - things. C++ has no notion of scoped modules, - so all of the code you write in any file could potentially conflict with other - classes/functions/etc. - Namespaces help to differentiate pieces of your code. - - The convention In this skeleton is to name the namespace to match the name of - the subdirectory where it lives. - So in this case, the namespace is called "standalone" because this method - lives within the "standalone" subdirectory. - If there is another "hello" function used for another example, the compiler - will know the difference between the two: - - standalone::hello - - VS - - potato::hello - -*/ namespace standalone { -// hello, custom sync method tied to module.cpp -// method's logic lives in hello.cpp -NAN_METHOD(hello); -} // namespace standalone \ No newline at end of file +// hello, custom sync method +Napi::Value hello(Napi::CallbackInfo const& info); + +} // namespace standalone diff --git a/src/standalone_async/hello_async.cpp b/src/standalone_async/hello_async.cpp index 83b3e6a..2aa77bb 100644 --- a/src/standalone_async/hello_async.cpp +++ b/src/standalone_async/hello_async.cpp @@ -1,4 +1,5 @@ #include "hello_async.hpp" +#include "../cpu_intensive_task.hpp" #include "../module_utils.hpp" #include @@ -24,97 +25,73 @@ * }); */ -// If this was not defined within a namespace, it would be in the global scope. -// Namespaces are used because C++ has no notion of scoped modules, so all of -// the code you write in any file could conflict with other code. -// Namespaces are generally a great idea in C++ because it helps scale and -// clearly organize your application. namespace standalone_async { -// Expensive allocation of std::map, querying, and string comparison, -// therefore threads are busy -std::unique_ptr do_expensive_work(bool louder) { - - std::map container; - std::size_t work_to_do = 100000; - - for (std::size_t i = 0; i < work_to_do; ++i) { - container.emplace(i, std::to_string(i)); - } - - for (std::size_t i = 0; i < work_to_do; ++i) { - std::string const& item = container[i]; - if (item != std::to_string(i)) { - - // AsyncHelloWorker's Execute function will take care of this error - // and return it to js-world via callback - // Marked NOLINT to avoid clang-tidy cert-err60-cpp error which we cannot - // avoid on some linux distros where std::runtime_error is not properly - // marked noexcept. Details at https://www.securecoding.cert.org/confluence/display/cplusplus/ERR60-CPP.+Exception+objects+must+be+nothrow+copy+constructible - throw std::runtime_error("Uh oh, this should never happen"); // NOLINT - } - } - - std::unique_ptr result = std::make_unique("...threads are busy async bees...hello world"); - - if (louder) { - *result += "!!!!"; - } - - return result; -} - // This is the worker running asynchronously and calling a user-provided // callback when done. // Consider storing all C++ objects you need by value or by shared_ptr to keep // them alive until done. -// Nan AsyncWorker docs: -// https://github.com/nodejs/nan/blob/master/doc/asyncworker.md -struct AsyncHelloWorker : Nan::AsyncWorker { - using Base = Nan::AsyncWorker; - - AsyncHelloWorker(bool louder, bool buffer, Nan::Callback* cb) - : Base(cb, "skel:standalone-async-worker"), louder_(louder), buffer_(buffer) {} +// Napi::AsyncWorker docs: +// https://github.com/nodejs/node-addon-api/blob/master/doc/async_worker.md +struct AsyncHelloWorker : Napi::AsyncWorker +{ + using Base = Napi::AsyncWorker; + // ctor + AsyncHelloWorker(bool louder, bool buffer, Napi::Function const& cb) + : Base(cb), + louder_(louder), + buffer_(buffer) {} // The Execute() function is getting called when the worker starts to run. // - You only have access to member variables stored in this worker. // - You do not have access to Javascript v8 objects here. - void Execute() override { + void Execute() override + { // The try/catch is critical here: if code was added that could throw an // unhandled error INSIDE the threadpool, it would be disasterous - try { - result_ = do_expensive_work(louder_); - } catch (const std::exception& e) { - SetErrorMessage(e.what()); + try + { + result_ = detail::do_expensive_work("world", louder_); + } + catch (std::exception const& e) + { + SetError(e.what()); } } - // The HandleOKCallback() is getting called when Execute() successfully + // The OnOK() is getting called when Execute() successfully // completed. // - In case Execute() invoked SetErrorMessage("") this function is not // getting called. // - You have access to Javascript v8 objects again // - You have to translate from C++ member variables to Javascript v8 objects // - Finally, you call the user's callback with your results - void HandleOKCallback() override { - Nan::HandleScope scope; - - if (buffer_) { - const auto argc = 2u; - v8::Local argv[argc] = { - Nan::Null(), utils::NewBufferFrom(std::move(result_)).ToLocalChecked()}; - // Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy - callback->Call(argc, static_cast*>(argv), async_resource); - } else { - const auto argc = 2u; - v8::Local argv[argc] = { - Nan::Null(), Nan::New(*result_).ToLocalChecked()}; - // Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy - callback->Call(argc, static_cast*>(argv), async_resource); + void OnOK() final + { + Napi::HandleScope scope(Env()); + if (!Callback().IsEmpty() && result_) + { + if (buffer_) + { + char* data = result_->data(); + std::size_t size = result_->size(); + auto buffer = Napi::Buffer::New(Env(), + data, + size, + [](Napi::Env, char*, gsl::owner*> v) { + delete v; + }, + result_.release()); + Callback().Call({Env().Null(), buffer}); + } + else + { + Callback().Call({Env().Null(), Napi::String::New(Env(), result_->data(), result_->size())}); + } } } - std::unique_ptr result_ = std::make_unique(); + std::unique_ptr> result_ = nullptr; const bool louder_; const bool buffer_; }; @@ -122,68 +99,59 @@ struct AsyncHelloWorker : Nan::AsyncWorker { // helloAsync is a "standalone function" because it's not a class. // If this function was not defined within a namespace ("standalone_async" // specified above), it would be in the global scope. -NAN_METHOD(helloAsync) { - +Napi::Value helloAsync(Napi::CallbackInfo const& info) +{ bool louder = false; bool buffer = false; // Check second argument, should be a 'callback' function. - // This allows us to set the callback so we can use it to return errors - // instead of throwing. - // Also, "info" comes from the NAN_METHOD macro, which returns differently - // according to the version of node - if (!info[1]->IsFunction()) { - return Nan::ThrowTypeError("second arg 'callback' must be a function"); + if (!info[1].IsFunction()) + { + Napi::TypeError::New(info.Env(), "second arg 'callback' must be a function").ThrowAsJavaScriptException(); + return info.Env().Null(); } - v8::Local callback = info[1].As(); + + Napi::Function callback = info[1].As(); // Check first argument, should be an 'options' object - if (!info[0]->IsObject()) { - return utils::CallbackError("first arg 'options' must be an object", - callback); + if (!info[0].IsObject()) + { + return utils::CallbackError("first arg 'options' must be an object", info); } - v8::Local options = info[0].As(); + Napi::Object options = info[0].As(); // Check options object for the "louder" property, which should be a boolean // value - if (Nan::Has(options, Nan::New("louder").ToLocalChecked()).FromMaybe(false)) { - v8::Local louder_val = - Nan::Get(options, Nan::New("louder").ToLocalChecked()).ToLocalChecked(); - if (!louder_val->IsBoolean()) { - return utils::CallbackError("option 'louder' must be a boolean", - callback); - } - Nan::Maybe maybe_louder = Nan::To(louder_val); - if (maybe_louder.IsNothing()) { - return utils::CallbackError("option 'louder' must be a boolean", callback); + + if (options.Has(Napi::String::New(info.Env(), "louder"))) + { + Napi::Value louder_val = options.Get(Napi::String::New(info.Env(), "louder")); + if (!louder_val.IsBoolean()) + { + return utils::CallbackError("option 'louder' must be a boolean", info); } - louder = maybe_louder.FromJust(); + louder = louder_val.As().Value(); } - // Check options object for the "buffer" property, which should be a boolean - // value - if (Nan::Has(options, Nan::New("buffer").ToLocalChecked()).FromMaybe(false)) { - v8::Local buffer_val = - Nan::Get(options, Nan::New("buffer").ToLocalChecked()).ToLocalChecked(); - if (!buffer_val->IsBoolean()) { - return utils::CallbackError("option 'buffer' must be a boolean", - callback); - } - Nan::Maybe maybe_buffer = Nan::To(buffer_val); - if (maybe_buffer.IsNothing()) { - return utils::CallbackError("option 'buffer' must be a boolean", callback); + // Check options object for the "buffer" property, which should be a boolean value + if (options.Has(Napi::String::New(info.Env(), "buffer"))) + { + Napi::Value buffer_val = options.Get(Napi::String::New(info.Env(), "buffer")); + if (!buffer_val.IsBoolean()) + { + return utils::CallbackError("option 'buffer' must be a boolean", info); } - buffer = maybe_buffer.FromJust(); + buffer = buffer_val.As().Value(); } // Creates a worker instance and queues it to run asynchronously, invoking the // callback when done. - // - Nan::AsyncWorker takes a pointer to a Nan::Callback and deletes the + // - Napi::AsyncWorker takes a pointer to a Napi::FunctionReference and deletes the // pointer automatically. - // - Nan::AsyncQueueWorker takes a pointer to a Nan::AsyncWorker and deletes + // - Napi::AsyncQueueWorker takes a pointer to a Napi::AsyncWorker and deletes // the pointer automatically. - auto cb = std::make_unique(callback); - auto worker = std::make_unique(louder, buffer, cb.release()); - Nan::AsyncQueueWorker(worker.release()); + auto* worker = new AsyncHelloWorker{louder, buffer, callback}; // NOLINT + worker->Queue(); + return info.Env().Undefined(); // NOLINT } } // namespace standalone_async diff --git a/src/standalone_async/hello_async.hpp b/src/standalone_async/hello_async.hpp index e846c42..ab21844 100644 --- a/src/standalone_async/hello_async.hpp +++ b/src/standalone_async/hello_async.hpp @@ -1,35 +1,10 @@ #pragma once -#include -// carrots, ex: --> look for header in global -// quotes, ex: "hello.hpp" --> look for header in location relative to this file +#include -/* - - Namespace is an organizational method that helps to clearly show where a - method is coming from. - Namespaces are generally a great idea in C++ because they help us scale - things. C++ has no notion of scoped modules, - so all of the code you write in any file could potentially conflict with other - classes/functions/etc. - Namespaces help to differentiate pieces of your code. - - The convention In this skeleton is to name the namespace to match the name of - the subdirectory where it lives. - So in this case, the namespace is called "standalone" because this method - lives within the "standalone" subdirectory. - If there is another "hello" function used for another example, the compiler - will know the difference between the two: - - standalone::hello - - VS - - potato::hello - -*/ namespace standalone_async { -// hello, custom sync method tied to module.cpp +// hello, custom sync method // method's logic lives in hello.cpp -NAN_METHOD(helloAsync); -} // namespace standalone_async \ No newline at end of file +Napi::Value helloAsync(Napi::CallbackInfo const& info); + +} // namespace standalone_async diff --git a/test/hello_object.test.js b/test/hello_object.test.js index 07a943c..3589064 100644 --- a/test/hello_object.test.js +++ b/test/hello_object.test.js @@ -4,7 +4,7 @@ var module = require('../lib/index.js'); test('success: prints expected string', function(t) { var H = new module.HelloObject('carol'); var check = H.helloMethod(); - t.equal(check, '...initialized an object...hello carol', 'returned expected string'); + t.equal(check, 'carol', 'returned expected string'); t.end(); }); @@ -13,7 +13,7 @@ test('error: throws when passing empty string', function(t) { var H = new module.HelloObject(''); } catch(err) { t.ok(err, 'expected error'); - t.equal(err.message, 'arg must be a non-empty string', 'expected error message') + t.equal(err.message, 'arg must be a non-empty string', 'expected error message'); t.end(); } }); @@ -23,7 +23,7 @@ test('error: throws when missing "new"', function(t) { var H = module.HelloObject(); } catch(err) { t.ok(err); - t.equal(err.message, 'Cannot call constructor as function, you need to use \'new\' keyword', 'expected error message') + t.equal(err.message, 'Class constructors cannot be invoked without \'new\'', 'expected error message'); t.end(); } }); @@ -33,7 +33,7 @@ test('error: handles non-string arg within constructor', function(t) { var H = new module.HelloObject(24); } catch(err) { t.ok(err, 'expected error'); - t.ok(err.message.indexOf('arg must be a string') > -1, 'expected error message'); + t.ok(err.message, 'A string was expected', 'expected error message'); t.end(); } }); @@ -43,7 +43,7 @@ test('error: handles missing arg', function(t) { var H = new module.HelloObject(); } catch (err) { t.ok(err, 'expected error'); - t.ok(err.message.indexOf('must provide string arg') > -1, 'expected error message'); + t.ok(err.message, 'must provide string arg', 'expected error message'); t.end(); } -}); \ No newline at end of file +}); diff --git a/test/hello_object_async.test.js b/test/hello_object_async.test.js index 74452c7..e2c4e64 100644 --- a/test/hello_object_async.test.js +++ b/test/hello_object_async.test.js @@ -45,7 +45,7 @@ test('error: throws when missing "new"', function(t) { var H = module.HelloObjectAsync('world'); } catch(err) { t.ok(err, 'expected error'); - t.equal(err.message, 'Cannot call constructor as function, you need to use \'new\' keyword', 'expected error message') + t.equal(err.message, 'Class constructors cannot be invoked without \'new\'', 'expected error message') t.end(); } }); @@ -54,8 +54,8 @@ test('error: handles non-string arg within constructor', function(t) { try { var H = new module.HelloObjectAsync(24); } catch(err) { - t.ok(err, 'expected error'); - t.ok(err.message.indexOf('arg must be a string') > -1, 'expected error message'); + console.log(err.message); + t.equal(err.message, 'String expected', 'expected error message'); t.end(); } }); @@ -103,7 +103,7 @@ test('error: handles missing arg', function(t) { var H = new module.HelloObjectAsync(); } catch (err) { t.ok(err, 'expected error'); - t.ok(err.message.indexOf('must provide string arg') > -1, 'expected error message'); + t.equal(err.message, 'String expected', 'expected error message'); t.end(); } });