Introduction to the

Anyone familiar with node.js knows that node.js is a JavaScript runtime developed based on C++. Since node.js is developed in C++, can I introduce C++ code into node.js? Of course, this technique is called C++ modules. The node. js C++ module is explained as follows

Node.js plug-ins are dynamically linked libraries written in C++ that can be loaded by node.js as require and used like node.js native modules. It is mainly used to bridge the relationship between Node.js JavaScript and C or C++ libraries.

Dynamic link library, namely window platform. DLL file, Linux under the. So file. The node.js module exports. Node files.

A dynamically linked library provides a way for a process to call functions that are not part of its executable code, which is in a. DLL (Window) or. So (Linux) file that contains one or more functions that have been compiled, linked, and stored separately from the process that uses them. Speaking of dynamic linked libraries, we have to mention static linked libraries. Static linked libraries refer to related function libraries (static libraries) linked into an executable file at compile time.

So why C++ modules?

JavaScript is an asynchronous, single-threaded language, which is very advantageous for some asynchronous tasks, but also has a significant disadvantage (perhaps a scripting language disadvantage) for some computationally intensive tasks. In other words, using a JavaScript interpreter to execute JavaScript code is usually less efficient than simply executing a C++ compiled binary. In addition, in fact, many open source libraries are written based on C++, such as image processing library (ImageMagick). The image processing library used by our team is also written based on C++ (written in JavaScript, the performance is not up to the requirements). Therefore, using C++ to achieve some problems can significantly improve the efficiency and performance. Why not?

So this article explains how to load C++ code into JavaScript from the basics of C++ plug-ins and several ways to write them.

The principle of the author

As mentioned earlier, node.js’s C++ modules are stored as dynamic links (.node), so how does node.js load C++ modules? A node.js module is either a JavaScript source file (.js) written in accordance with the CommonJS specification, or a C++ module binary (.node), which is imported and used by the require() function in node.js.

The essence of introducing C++ modules into node.js is the process of introducing a dynamically linked library into the node.js runtime. Load modules in node.js, whether node.js or C++ modules, via the require() function. So if you know how this function is implemented, you know how to load modules.

In order to reveal the true nature of require, we open the source of Node.js (Node.js Github).

In the lib/internal/modules/CJS/loader, js, we can find the Module to realize the JavaScript code

function Module(id = ' ', parent) { // Class Module
  this.id = id; / / module id
  this.path = path.dirname(id);
  this.exports = {}; //
  this.parent = parent;
  updateChildren(parent, this.false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

Module._cache = ObjectCreate(null); // Object.create()
Module._pathCache = ObjectCreate(null); // Module cache
Module._extensions = ObjectCreate(null); // File name processing

let wrap = function(script) {
  // Wrap the corresponding JS script with the following wrapper
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { '.'\n}); '
];

Copy the code

Scroll down to find the implementation of require()

Module.prototype.require = function(id) {
 // ...
 return Module._load(id, this./* isMain */ false);
 // ... 
};

Module._load = function(request, parent, isMain) {
  // omit most of the code...
  const filename = Module._resolveFilename(request, parent, isMain);
  // If the module is in the cache, it is loaded from the cache
  const cachedModule = Module._cache[filename]; 
  if(cachedModule ! = =undefined) {
    return cachedModule.exports;
  }
  // Built-in modules
  const mod = loadNativeModule(filename, request);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // Other module processing
  const module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '. ';
  }
  
  Module._cache[filename] = module;
  // ...
  module.load(filename); // Delegate to load
  return module.exports;
};
Copy the code

You can see the loading rules for modules in the code above

  1. If the module is in cache, read directly from the cache
  2. If the built-in module ofloadNativeModuleLoad module
  3. For other purposesModule.proptype.loadFunction to load the module
Module.prototype.load = function(filename) {
  // omit...
  const extension = findLongestRegisteredExtension(filename);
  // To get to the point, use a different function for each extension
  Module._extensions[extension](this, filename);
  this.loaded = true;
  // omit...
};
Copy the code

See the Module. _extensions [extension] (this filename); This line treats the.js/.node/.json files separately, and lets focus on the implementation of the Module._Extensions

Module.prototype._compile = function(content, filename) {
  // omit...
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  let result;
  /* Wrap the content with the wrapper above and pass in the parameters. Const wrapper = ['(function (exports, require, module, etc.), __filename, __dirname) __filename, __dirname) { ', '\n});' ]; * /
  const compiledWrapper = wrapSafe(filename, content, this);
  const exports = this.exports;
  const thisValue = exports;
  const module = this;

  result = compiledWrapper.call(thisValue, exports, require.module,
                                  filename, dirname);
  return result;
};
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  // omit...
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename); // Throw the wrapper contents into the VM module for execution
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');
  // omit...
  module.exports = JSONParse(stripBOM(content));
};


// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  // omit...
  return process.dlopen(module, path.toNamespacedPath(filename));
};
Copy the code

As you can see, the.node file is processed using the process.dlopen function, but this function is implemented in C++ (similar to the way a C++ plug-in is written). You can find the definition of this function under SRC /node_process_methods.cc.

env->SetMethodNoSideEffect(target, "cwd", Cwd); //process.cwd()
env->SetMethod(target, "dlopen", binding::DLOpen); // process.dlopen()
env->SetMethod(target, "reallyExit", ReallyExit);
env->SetMethodNoSideEffect(target, "uptime", Uptime);
env->SetMethod(target, "patchProcessObject", PatchProcessObject);
Copy the code

The binding::DLOpen function is implemented under SRC /node_binding.cc

Void DLOpen(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); auto context = env->context(); CHECK_NULL(thread_local_modpending); // Compare process.dlopen(module, filename) if (args.Length() < 2) { env->ThrowError("process.dlopen needs at least 2 arguments."); return; } int32_t flags = DLib::kDefaultFlags; if (args.Length() > 2 && ! args[2]->Int32Value(context).To(&flags)) { return env->ThrowTypeError("flag argument must be an integer."); } Local<Object> module; Local<Object> exports; Local<Value> exports_v; if (! args[0]->ToObject(context).ToLocal(&module) || ! module->Get(context, env->exports_string()).ToLocal(&exports_v) || ! exports_v->ToObject(context).ToLocal(&exports)) { return; Node ::Utf8Value filename(env-> ISOLATE (), args[1]); Env ->TryLoadAddon(*filename, flags, [&](DLib* DLib) {// C++ lambda expression, Static Mutex dlib_load_mutex; static Mutex dlib_load_mutex; // Mutex::ScopedLock lock(dlib_load_mutex); const bool is_opened = dlib->Open(); // open node_module* mp = thread_local_modpending; thread_local_modpending = nullptr; if (! is_opened) { dlib->Close(); / /... return false; } if (mp ! If (env->options()->force_context_aware) {if (mp->nm_context_register_func == nullptr) { dlib->Close(); return false; } } mp->nm_dso_handle = dlib->handle_; Dlib ->SaveInGlobalHandleMap(mp); } else {if (callback = GetInitializerCallback(dlib)) {// Callback(exports, module, context); return true; } else if (auto napi_callback = GetNapiInitializerCallback (dlib)) {/ / write for a plug-in napi_module_register_by_symbol(exports, module, context, napi_callback); return true; } else { mp = dlib->GetSavedModuleFromGlobalHandleMap(); if (mp == nullptr || mp->nm_context_register_func == nullptr) { dlib->Close(); / /... return false; } } } // -1 is used for N-API modules if ((mp->nm_version ! = -1) && (mp->nm_version ! = NODE_MODULE_VERSION)) { if (auto callback = GetInitializerCallback(dlib)) { callback(exports, module, context); return true; } //... return false; } CHECK_EQ(mp->nm_flags & NM_F_BUILTIN, 0); // Do not keep the lock while running userland addon loading code. Mutex::ScopedUnlock unlock(lock); If (mp->nm_context_register_func! = nullptr) { mp->nm_context_register_func(exports, module, context, mp->nm_priv); } else if (mp->nm_register_func ! = nullptr) { mp->nm_register_func(exports, module, mp->nm_priv); } else { dlib->Close(); return false; } return true; }); }Copy the code

Node.js encapsulates the operations of a dynamic link library into a DLib class. DLib ->Open() actually calls uv_dlopen() to load the link library.

 int ret = uv_dlopen(filename_.c_str(), &lib_); // [out] _lib
Copy the code

Uv_dlopen () is a function provided in libuv that loads dynamically linked libraries and returns a uv_lib_t handle type

typeof strcut uv_lib_s uv_lib_t;
struct uv_lib_s {
  char* errmsg;
  void* handle;
};
Copy the code

Handle saves the link library handle. callback(exports, module, context); To call a C++ plug-in written by node.js v8. (there is another treatment for n-api, Void Init(Localexports, Localmodule, Localcontext) {} Here is a flow chart to describe the loading process

The preparatory work

Finally to the practice of the link, but don’t worry, to do good work, must first sharpen its tools, ready to develop the environment.

The editor

After comparing Vim/Vs Code/Qt Creator/CLoin editors, we get a conclusion: Vim does not have Code prompts (too bad, can not be equipped), Vs Code writes C++ Code abnormal card, always Code prompts, highlighting all disappear. Qt Creator is good for C++, but it’s a headache when it comes to JavaScript. In the end, it’s CLoin. It supports both C++ and JavaScript very well (jetbrian is good). It’s just a little bit of cmakelist.txt.

node-gyp

Node-gyp is the extension building tool for node.js, which generates C++ project files (UNIX makefiles, Windows Visual Studio projects) using a binding.gyp description file. The appropriate build tool (GCC) is then called to build.

The installation

Make sure you have Xcode installed on your MAC (download it directly from the App Store) and then run the command line

npm install node-gyp -g
Copy the code

Node-gyp common command

  • help
  • Configure generates a build directory based on the platform and Node version.
  • Build build the Node plug-in (according to the contents of the build folder, generate a. Node file)
  • Clean Clears the build directory
  • Rebuild executes clean, configure, and build in order to easily rebuild the plug-in
  • Install Install the node header file of the corresponding version
  • List lists the version of the node header file currently installed
  • Remove Deletes the installed Node header file

A peek at the binding.gyp file

The binding.gyp file mentioned above is actually a file similar to Python dict, based on Python’s dict syntax and annotated in the same style as Python. For example, a simple binding.gyp would look like this

{
  "targets": [{"target_name": "addon"."sources": [
        "addon.cpp" # c++ source file for compilation]],}}Copy the code

The targets field is an array in which each element is the C++ module to be built by node-gyp. Target_name is required, representing the module name and will be named by that name when compiled. The node file, sources field is also required, which files will be compiled as source.

  1. Basic types of

Similar to Python data types, the basic types in GYP are String, Integer, Lists, and Dictionaries

  1. Key fields

Here are some of the more common fields (keys)

Targets, target_name,sources are explained above, but not here.

Include_dirs: specifies the header search path, identified by -i, such as GCC -i some.c -o some.o

C -o some. I GCC -d N=1000 some. C -o some

Libraries: Add link libraries for compilation, -l for compilation identifier

Cflags: custom compilation flags

Dependencies: if your code uses a third party’s C++ code, compile the library into a static link library in binding.gyp and then use dependencies in the main target.

Conditions: Branch condition processing field

Type: compile type. There are three values: shared_library,static_library,loadable_module, node.js, default type for binding.gyp.

Variables: Variables fields that you can write variables to use in gyp files

Here is a simple example of a simple, more examples please refer to: github.com/Node.js/nod…

{
  "targets": [{"target_name": "some_library"."sources": [
      "some.cc"] {},"target_name": "main_addon"."variables": { # define variables
        "main": "main.cc"."include": ["./lib".".. /src"]},"cflags": ["-Werror"] # g++ compiler identifier
      "sources": [
        "<(main)" Use < to refer to a variable]."defines": [ # define macros
        "MY_NODE_ADDON=1"]."include_dirs": [
        "/usr/local/node/include"."<@(include)" # use <@ to reference array variables]."dependencies": [ # define dependencies
        "some_library" # depend on some_library above]."libraries": [
        "some.a".# mac
        "xxx.lib" # win]."conditions": [ # condition, in the following format[["OS=='mac'", {"sources": ["mac_main.cc"]}], 
          ["OS=='win'", {"sources": ["win_main.cc"[}], []}]}Copy the code
  1. variable

There are three main types of variables in GYP: predefined variables, user-defined variables, and automatic variables.

Predefined variables: such as OS variables, which represent the current operating system (Linux, MAC, Win)

User-defined variables: Variables defined under the Variables field.

Automatic variables: All string keys are treated as automatic variables. Variable names are key names prefixed with _.

Variable references: start with < or > and use @ to distinguish between different types of variables. <(VAR) or >(VAR), if VAR is a string, it is treated as a normal string, if VAR is an array, the string of each item of the array is concatenated by Spaces. <@(VAR) or >@(VAR), this directive can only be used in arrays. If VAR is an array, the contents of the array will be inserted into the current array. If VAR is a string, the specified delimiter will be converted into an array and then inserted into the current array.

  1. instruction

Instructions are similar to, but a little more advanced than, variables. When GYP reads the instruction, it starts a process to execute the expanded instruction in the syntactic format:

{
  #...
  "include_dirs": [
    "
       Node-e "require('nan')" and place the result in include_dirs
  ]
  #...
}
Copy the code
  1. Conditional branch

Conditions field, whose value is an array, so the first element is a string representing a condition in the same format as python’s conditional branches, such as “OS==’ MAC ‘or OS==’win'” or “VAR>=1 and VAR <= 2”. The second element is an object for the content merged into the most recent context based on the criteria.

  1. List filter

For keys whose values are arrays, the key name begins with! Or /, where the key name begins with! The end is an exclusion filter, indicating that the key value here will be removed! The key of the same name is excluded. The key name ending with/is a match filter, which matches the result by the re and then processes it in the specified manner (include or exclude).

{
  "targets": [{"target_name": "addon"."sources": [
        "a.cc"."b.cc"."c.cc"."d.cc"]."conditions": [["OS=='mac'", {"sources!": ["a.cc"]}], Filter, if the condition is true, remove a.c from sources
        ["OS=='win'", {"sources/": [ # match filter
          ["include"."b|c\\.cc"].# contains both b.c and c.c
          ["exclude"."a\\.cc"] A.c c # ruled out[}]]}Copy the code
  1. merge

As you can see above, many of GYP’s operations are implemented by merging dictionaries and list items together (conditional branching), and it is important to recognize the difference between source and target values when merging operations.

When merging a dictionary, follow these rules

  • If the key does not exist in the target dictionary, it is inserted
  • If the key already exists
    • If the value is a dictionary, the source and target value dictionaries perform the dictionary merge process
    • If the value is a list, the source and target value lists perform the list merge process
    • If the value is a string or integer, the source value is replaced directly

When you merge lists, you merge them based on the suffix appended to the key name

  • The key to=At the end, the source list completely replaces the target list
  • The key to?At the end, the source list is set to the target list only if the key is not there
  • The key to+End, the source list is appended to the target list
  • If the key has no modifier, the source list contents are appended to the target list

For example,

# source
{
  "include_dirs+": [
    "/public"]}# target
{
  "include_dirs": [
    "/header"]."sources": [
    "aa.cc"]}# combined
{
  "include_dirs": [
    "/public"."/header"]."sources": [
    "aa.cc"]}Copy the code

First C++ plugin: Hello World

Install the node.js header file using node-gyp install. After installing the header file, locate it in the ~/. Node-gyp /node-version/include/node directory, or locate the include directory in your node.js installation directory. Inside is the Node.js header file.

NODE_MODULE is used to register a C++ module. The corresponding Init function takes the Localexports argument, which is similar to module.exports in node.js. Exports mount function.

Write cmakelists.txt and use include_directories to link the node header file to it, useful when prompted by the editor code

Cmake_minimum_required (VERSION 3.15) project(cpp_addon_test) set(CMAKE_CXX_STANDARD 14) # link node header file, Include_directories (/Users/dengpengfei/.node-gyp/12.6.0/include/node) add_executable(cpp_addon_test main.cpp)! [](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/1/21/16fc641a9cc6f4fa~tplv-t2oaga2asx-image.image )Copy the code

Write binding.gyp and specify sources as main.cpp

{
  "targets": [{"target_name": "cpp_addon"."sources": [
        "main.cpp"]]}}Copy the code

Node-gyp is used to compile C++ files, node-gyp configure is used to generate configuration files, and node-gyp build is used to build C++ plug-ins. Or build the C++ plug-in directly using node-gyp rebuild.

Index. js imports the cpp_adon. node file

const cpp = require("./build/Release/cpp_addon");
console.log(cpp.hello());
Copy the code

The result is as follows

Hello world! Not enough? Let’s take a look at some simple C++ functions and wrap the BigNumber class. Let’s look at some tool methods, lib/utils.h

int findSubStr(const char* str, const char* subStr); // Find the substring position, KMP algorithm
int subStrCount(const char* str, const char* subStr); // Number of occurrences of a string in the source string, KMP algorithm
Copy the code

And the BigNumber wrapper class, lib/ bignumber.h

class BigNumber: node::ObjectWrap { public: static void Init(Local<Object>); Private: explicit BigNumber(const char* value): value(value) {} constructor ~BigNumber() override = default; static void New(const FunctionCallbackInfo<Value>&); // New static void Val(const FunctionCallbackInfo<Value>&); // Return Value static void Add(const FunctionCallbackInfo<Value>&); Static void Multiply(const FunctionCallbackInfo<Value>&); // multiply STD ::string value; // use a STD ::string to store};Copy the code

This uses the node::ObjectWrap wrapper Class, a utility Class that connects the C++ Class to the JavaScript Class (located in the node_object_wrap.h header, more on this utility Class below). Due to limited space, only functions and class definitions are presented here. For implementation and examples, please refer to GitHub: github.com/sundial-dre…

The main function main. CPP

#include <iostream> #include <stdio.h> #include <stdlib.h> #include <node.h> #include <node_object_wrap.h> #include "lib/utils.h" #include "lib/bigNumber.h" const int N = 10000; using namespace v8; / / the findSubStr (const char *, Void FindSubStr(const FunctionCallbackInfo<Value>& args) {Isolate* Isolate = args.getisolate (); if (! args[0]->IsString() || ! args[1]->IsString()) { isolate->ThrowException(Exception::TypeError(ToLocalString("type error"))); String::Utf8Value STR (ISOLATE, args[0].As<String>()); String::Utf8Value subStr(isolate, args[1].As<String>()); int i = findSubStr(*str, *subStr); args.GetReturnValue().Set(Number::New(isolate, i)); } // subStrCount(const char*, Void SubStrCount(const FunctionCallbackInfo<Value>& args) {Isolate* Isolate = args.getisolate (); if (! args[0]->IsString() || ! args[1]->IsString()) { isolate->ThrowException(Exception::TypeError(ToLocalString("type error"))); String::Utf8Value STR (ISOLATE, args[0].As<String>()); String::Utf8Value subStr(isolate, args[1].As<String>()); int i = subStrCount(*str, *subStr); // Call args.getreturnValue ().set (Number::New(ISOLATE, I)); } void Init(Local<Object> exports) {// NODE_SET_METHOD(exports, "findSubStr", findSubStr); NODE_SET_METHOD(exports, "subStrCount", SubStrCount); // Exposes the BigNumber class BigNumber::Init(exports); } NODE_MODULE(addon, Init)Copy the code

The binding.gyp file is shown below

{
  "targets": [{"target_name": "addon"."sources": [
              "lib/utils.cpp"."lib/bigNumber.cpp"."main.cpp"]]}}Copy the code

Add both lib/utils. CPP and lib/ bignumber. CPP to sources and rebuild to node-gyp.

Then the JavaScript side

const { findSubStr, subStrCount, BigNumber } = require("./build/Release/addon");
console.log("subStr index is: ", findSubStr("abcabdacac"."cab"));
console.log("subStr count is: ", subStrCount("abababcda"."ab"));

let n = new BigNumber("9999");
n.add(n);
console.log("add: ", n.val());
n.multiply("12222");
console.log("multiply: ", n.val());
Copy the code

Run the

Several ways to write C++ plug-ins

As the way to write node.js C++ plug-ins changes, this article summarizes the following ways to write C++ plug-ins

  1. Native development
  2. The use of NAN
  3. Using N – API
  4. Using the addon – API

Native extensions

The native way is to create plug-ins directly using the internal V8, Libuv, and Node.js libraries. Writing a plug-in this way can be complicated and involves the following components and apis.

  • V8: JavaScript runtime, used to explain executing JavaScript. V8 provides mechanisms for creating objects, calling functions, and more.
  • Libuv: Implements node.js event loops.
  • Internal node.js library: node.js itself exports the C++API that plug-ins can use, but more importantlynode::ObjectWrapClass.
  • Node.js other statically linked libraries: including OpenSSL, zlib, etc. You can use zlib.h, OpenSSL, etc to reference your own plug-ins.

V8

The V8 (V8 documentation) engine is a standalone JavaScript runtime. The difference between the browser side and the Node.js side is that the V8 engine is wrapped in a different layer, which means that you can take the V8 engine and wrap your own Node.js.

Node.js is a host for the V8 engine, and much of it is exposed by using Chrome V8 directly.

Some basic V8 concepts

  1. Isolate
  • An Isolate is a V8 engine instance, also known as Isolated Instance, that has completely independent states within the instance, including heap management, garbage collection, and so on.

  • The Isolate is usually passed to other V8 API functions and provides apis to manage the behavior of the JavaScript engine or to query information such as memory usage.

  • Any objects generated by one Isolate cannot be used in another Isolate.

  • You can obtain the Isolate in the Node.js plug-in in the following ways

// Get it directly
Isolate* isolate = Isolate::GetCurrent();
// If there is a Context
Isolate* isolate = context->GetIsolate(); 
// There is const FunctionCallback
      
       & args in the binding function
      
Isolate* isolate = args.GetIsolate();
// If there is an Environment
Isolate* isolate = env->isolate();
Copy the code

Context

Node also has its own context, global, and we can even wrap the context, for example

Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
Local<String> key = String::NewFromUtf8(isolate, "CONST".NewStringType::kNormal).ToLocalChecked();
Local<String> value = String::NewFromUtf8(isolate, "I am global value".NewStringType::kNormal).ToLocalChecked();
global->Set(key, value);
Local<Context> myContext = Context::New(isolate, nullptr, global); // Create the Context this way, and now the Context is {CONST: "I am global value"}
Copy the code
  1. Script

An object that contains a compiled JavaScript Script of type Script and is bound to a Context at compile time. We can implement an eval function that compiles a piece of JavaScript code and encapsulates a Context of our own to look at C++ code

#include <node.h>
#include <iostream>
using namespace v8;
// This piece of code is not complicated
void Eval(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate(); / / to isolate
  HandleScope handleScope(isolate); // Define the handle scope
  Local<Context> context = isolate->GetCurrentContext(); / / get the Context

  // Define a global object and set its corresponding key and value
  Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
  Local<String> key = String::NewFromUtf8(isolate, "CONST", NewStringType::kNormal).ToLocalChecked();
  Local<String> value = String::NewFromUtf8(isolate, "I am global value", NewStringType::kNormal).ToLocalChecked();
  global->Set(key, value);
  Local<String> printStr = String::NewFromUtf8(isolate, "print", NewStringType::kNormal).ToLocalChecked(); // let printStr = "print";

  global->Set(printStr, FunctionTemplate::New(isolate, [](const FunctionCallbackInfo<Value>& args) -> void {
    Isolate* isolate = args.GetIsolate();
    for (size_t i = 0; i < args.Length(); i++) {
      Local<String> str = args[i].As<String>();
      String::Utf8Value s(isolate, str); // Convert Local to char* for cout
      std: :cout<<*s<<"";
    }
    std: :cout<<std: :endl;
  }));
  /* global = { CONST: "I am global value" print: function(... args) { for(let i = 0; i < args.length; i++) console.log(args[i]) } } */
  // Bind global to its own context
  Local<Context> myContext = Context::New(isolate, nullptr, global);
  Local<String> code = args[0].As<String>();
  // Compile the JavaScript code
  Local<Script> script = Script::Compile(myContext, code).ToLocalChecked(); // Bind to myContext
  // Run and return the result
  args.GetReturnValue().Set(script->Run(context).ToLocalChecked());
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "eval", Eval);
}

NODE_MODULE(addon, Init)
Copy the code

Local
global = ObjectTemplate::New(ISOLATE); Local

myContext = Context::New(ISOLATE, nullptr, global); Bind global to a Context. Script::Compile(myContext, Code) compile the JavaScript code and return the result to args.getreturnValue ().set (script->Run(context).tolocalchecked ()); . Local, FunctionCallbackInfo, HandleScope, and some of the data types involved are explained below

Build the. Node file using node-gyp rebuild or node-gyp clean && node-gyp configure && node-gyp build

On the JavaScript side, load the.node file

const cpp = require("./build/Release/addon");
const code = ` let arr = ["hello", "world"]; print(CONST); For (let v of arr) {print(v); } `;
console.log(cpp.eval(code));
Copy the code

Run JavaScript

  1. Handle

Handles, an important concept in V8, provide a reference to a JavaScript data object in heap memory. When an object is no longer referenced by a handle, it is considered garbage, and V8’s garbage collection mechanism collects it from time to time.

In Windows, a handle is a unique integer (number) used to identify the object created or used by the application. It can be understood as an identifier that identifies an object or item.

In V8, there are several types of handles (handles exist in the form of a C++ template class that is declared differently depending on the V8 data type)

  • Local: local handles, which are most commonly used when writing C++ extensions, live in stack memory and are removed when the corresponding destructor is called. Their life is determined by the HandleScope scope in which they reside. Most of the time you can get one through some static method of a JavaScript data classLocalhandle
HandleScope handleScope(isolate);
Local<Number> n = Number::New(isolate, 22);
Local<String> str = String::NewFromUtf8(isolate, "fff");
Local<Function> func = Function::New(context, Eval).ToLocalChecked();
Local<Array> arr = Array::New(isolate, 20);
Copy the code

Use handle.clear () to Clear a handle (similar to a pointer to “empty”), use handle.isempty () to determine whether the current handle IsEmpty, and use As()/Cast() to Cast the handle type

Local<String> str = Local<String>::Cast(handle); / / use the Cast
Local<String> str1 = handle.As<String>(); / / use the As
Copy the code
  • MaybeLocal: Sometimes you need to use it where the handle is usedhandle.IsEmpty()It’s kind of like a null pointer, but it’s a little bit more code and a little bit more complexity to do it everywhere a handle is used, soMaybeLocalThat was born, and those that could return to emptyLocalThe interface of the handle is usedMaybeLocalTo replace, and if you want to get realLocalHandle, you have to use itToLocalChecked()
MaybeLocal<String> s = String::NewFromUtf8(isolate, "sss", NewStringType::kNormal);
Local<String> str = s.ToLocalChecked();
double a = args[0]->ToNumber(context).ToLocalChecked();

// Or use the ToLocal method, which returns true if the handle is not empty and gives the value to out
Local<String> out;
if (s.ToLocal(&out)) {
  // 
} else {
  // 
}
Copy the code
  • Persistent/Global: persistent handle, which provides references to JavaScript objects declared in heap memory (such as the browser’S DOM), so persistent handle andLocalLocal handles manage the lifecycle in two different ways. Persistent handles can be usedSetWeakV8’s garbage collector triggers a callback when there is only one weak persistent handle left for references to JavaScript objects in the heap.
Local<String> str = String::NewFromUtf8(isolate, "fff", NewStringType::kNormal).ToLocalChecked();
Global<String> gStr(isolate, str); Construct a persistent handle based on the Local handle
Copy the code

As with other handles, persistent handles can still be cleared by using Clear() and IsEmpty() to determine whether they are empty.

Reset sets the handle reference, and Get gets the current handle reference

Local<String> str1 = String::NewFromUtf8(isolate, "yyy", NewStringType::kNormal).ToLocalChecked();
gStr.Reset(isolate, str1); // Reset the handle
  
Local<String> r = gStr.Get(isolate);
Copy the code

SetWeak is set to a weakly persistent handle, whose function prototype is as follows

void PersistentBase<T>::SetWeak(
  P* parameter, typename WeakCallbackInfo<P>::Callback callback,
  WeakCallbackType type) 
Copy the code

Parameter is any data type, callback is a callback triggered when a reference to a JavaScript object only has a weak persistent handle, and Type is an enumerated value, including kParameter and kInternalFields and kFinalizer

int* p = new int;
// When gStr is about to be recycled, the p object is passed to the callback function and p is retrieved from data.getParameter () in the callback
gStr.SetWeak(p, [](const WeakCallbackInfo<int> &data) -> void {
    int* p = data.GetParameter();
    delete p;
}, WeakCallbackType::kParameter);
Copy the code

ClearWeak is used to cancel the weak persistent handle and change it into a persistent handle. IsWeak is used to determine whether the weak persistent handle is a weak persistent handle.

  • Eternal: immortal handle, which is not deleted for the entire life of the program and, due to this feature, is less expensive than persistent handles.
  1. HandleScope

Handle scope, a container that maintains handles. When the destructor of a handle scope object is called (when the object is destroyed), handles created in this scope are erased from the stack, losing all references, and then disposed of by the garbage collector. The HandleScope has two types of HandleScope and the EscapableHandleScope

  • HandleScope: General handle scope, usedHandleScope handleScope(isolate)To declare the handle scope under the current scope, according to C++ syntax, whenhandleScopeIts destructor is called when its scope ends (for example, when the function is finished executing) to do some closing operations.
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate); // Define a handle scope
  // A bunch of Local handles
  Local<Number> a = args[0].As<Number>();
  Local<Number> b = args[1].As<Number>();
  double r = a->Value() + b->Value();
  Local<Number> result = Number::New(isolate, r);
  args.GetReturnValue().Set(result);

  return; // When the current scope terminates, handleScope's destructor is called to remove handles a, b, and result from the stack
}
Copy the code
  • EscapableHandleScopeEscapable handle scope, as the name implies, lets a handle escape the current scope, for example
Local<Number> getValue() {
  Isolate* isolate = Isolate::GetCurrent();
  HandleScope handleScope(isolate);
  Local<Number> result = Number::New(isolate, 12);
  return result;
}
Copy the code

The above function feel no problem, but in fact, this is a huge pit, according to the above said, handleScope at the end of the current scope to invoke the destructor will handle in the current scope of the result to delete all, and the reference value of entity reference is lost is marked as spam, and then used in the outside, will be a problem, So use the EscapableHandleScope to transform the above function

Local<Number> getValue() {
  Isolate* isolate = Isolate::GetCurrent();
// HandleScope handleScope(isolate);
  EscapableHandleScope scope(isolate);
  Local<Number> result = Number::New(isolate, 12);
  return scope.Escape(result); // Escape result from the current scope
}
Copy the code
  1. V8 JavaScript Value

V8 has a C++ wrapper around every data type in JavaScript, such as Number, String, Object, Function, Boolean, Date, Promise, and so on, derived from Value

Here are some examples of these types

  • Value

The parent class of all data types, or an abstraction of all data types. It has two important apis, Is… , To… . Is… IsNumber(), args[0].isFunction (), To… To some type args.ToNumber(context), which returns a MayBeLocal handle.

  • Number

Number::New(ISOLATE, 222) creates a handle to a Number object. Number ->Value() returns a double.

  • String

String is a common data type. String::NewFromUtf8(ISOLATE, “FFF “, NewStringType::kNormal) is used to construct a String handle, which returns a MaybeLocal handle.

Each time you create a String handle, you need to write a long String of code, so you wrap a ToLocalString function

Local<String> ToLocalString(const char* str) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);
  Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(result);
}
Copy the code

In many cases, we need to convert strings to the C++ char* type. In this case, we can use String::Utf8Value

Local<String> str = String::NewFromUtf8(isolate, "hello world", NewStringType::kNormal).ToLocalChecked();
String::Utf8Value value(isolate, str);
std: :cout<<value.length()<<std: :endl;
const char* cppStr = *value; // *value can be converted to char* or const char*
std: :cout<<cppStr<<std: :endl;
Copy the code

As above, the converted native string is wrapped into a function ToCString

char* ToCString(Local<String> from) {
  Isolate* isolate = Isolate::GetCurrent();
  String::Utf8Value v(isolate, from);
  return *v;
}
Copy the code
  • Function

Functions are also objects, so they inherit from the Object class. For a function type, Call() is used to Call the function, NewInstance() is used to Call the function, and setName()/getName() is used to set the function name.

Call: This function is prototyped as follows

MaybeLocal<Value> Call(Local<Context> context,Local<Value> recv, int argc, Local<Value> argv[]);
Copy the code

Context is a context handle object, recv is bound to this, and you can pass a Null(ISOLATE) into it, similar to the first argument of call in JavaScript. Argc is the number of function arguments, and argv is an array representing the parameters passed into the function

Example, use C++ to implement the filter function

#include <iostream>
#include <node.h>

using namespace v8;

// to Local<String>
Local<String> ToLocalString(const char* str) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);
  Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(result);
}

void Filter(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope scope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  if(! args[0]->IsArray() && ! args[1]->IsArray()) {
    isolate->ThrowException(Exception::TypeError(ToLocalString("Type error")));
    return;
  }
  Local<Array> array = args[0].As<Array>();
  Local<Function> fn = args[1].As<Function>();
  Local<Array> result = Array::New(isolate);
  Local<Value> fnArgs[3] = { Local<Value>(), Number::New(isolate, 0), array };
  for (uint32_t i = 0, j = 0; i < array->Length(); i++) {
    fnArgs[0] = array->Get(context, i).ToLocalChecked(); // v
    fnArgs[1] = Number::New(isolate, i); // i
    Local<Value> v = fn->Call(context, Null(isolate), 3, fnArgs).ToLocalChecked();
    if (v->IsTrue()) { // get return
      result->Set(context, j++, fnArgs[0]).FromJust();
    }
  }
  args.GetReturnValue().Set(result);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "filter", Filter);
}

NODE_MODULE(addon, Init)

Copy the code
  • Array

Array objects are relatively simple. Commonly used are Array::New(ISOLATE) to create arrays, Set()/Get() to manipulate arrays, and Length() to Get the Array Length. See the filter function above

  • Object

Object types. Many types, such as Function and Array, inherit from Object. Using Object::New(ISOLATE), you can create an Object handle and manipulate keys with Set(), Get(), and Delete()

void CreateObject(const FunctionCallbackInfo<Value>& args) { // return { name, age }
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();

  Local<Object> object = Object::New(isolate);
  Local<String> nameKey = String::NewFromUtf8(isolate, "name", NewStringType::kNormal).ToLocalChecked();
  Local<String> ageKey = String::NewFromUtf8(isolate, "age", NewStringType::kNormal).ToLocalChecked();
  Local<String> nameValue = args[0].As<String>();
  Local<Number> ageValue = args[1].As<Number>();

  object->Set(context, nameKey, nameValue).Check(); / / set the key
  object->Set(context, ageKey, ageValue).Check(); / / set the key
  args.GetReturnValue().Set(object);
}
Copy the code
  1. FunctionCallbackInfo

Function callback information, which contains various information (parameters, this, etc.) required for a JavaScript function call, for example

void Add(const FunctionCallbackInfo<Value>& args) {}Copy the code

When using ARGS

  • throughLength()To get the number of arguments passed in
  • throughargs[i]To get the ith parameter
  • throughargs.This()To get the delta functionthis
  • throughargs.Holder()Gets the value of the function callthis, the use ofcall.bind.applyYou can changethisPoint to the
  • throughargs.IsConstructCall()To determine whether it is a constructor call, i.enewcall
  • throughargs.GetIsolate()Get the currentIsolate
  • throughargs.GetReturnValue()To get the object that stores the return value, and set the return value, that is, useSet()Method, can be usedSetNull()To return anull.SetUndefined()Returns aundefined.SetEmptyString()To return an empty string.
  1. Template

A template is a template for JavaScript objects and functions that wraps C++ functions or data structures into JavaScript objects. There are two types of templates: FunctionTemplate and ObjectTemplate.

  • FunctionTemplate

As the name implies, the template used to wrap C++ functions. After a function template is generated, GetFunction can be called to get its entity handle, and the JavaScript side can call this function directly. Review the NODE_SET_METHOD macro, NODE_SET_METHOD(exports, “eval”, eval); Wrap the Eval function around it, and you can call it on the JavaScript side, and you can flip through its implementation

inline void NODE_SET_METHOD(v8::Local<v8::Object> recv,
                            const char* name,   
                            v8::FunctionCallback callback) { // inline functions
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  // Generate a function template
  v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate,
                                                                callback);
  // GetFunction gets the handle instance and returns a MayBeLocal
  v8::Local<v8::Function> fn = t->GetFunction(context).ToLocalChecked();
  v8::Local<v8::String> fn_name = v8::String::NewFromUtf8(isolate, name,
      v8::NewStringType::kInternalized).ToLocalChecked();
  // Set the function name
  fn->SetName(fn_name);
  recv->Set(context, fn_name, fn).Check();
}
#define NODE_SET_METHOD node::NODE_SET_METHOD
Copy the code

Callback is a typedef void (*FunctionCallback)(const FunctionCallbackInfo

& info); Void Method(const FunctionCallbackInfo

& args) {},

  • ObjectTemplateObject templates are used to create objects at run time. Like this one
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
Copy the code

Object templates have two common uses. When a function template is used as a constructor, the object template is used to configure the created object (specifically, the constructor implementation class in JavaScript), such as the JavaScript class below

function Person(name, age) {
    this._name = name;
    this._age = age;
}
Person.prototype.getName = function () {
    return this._name;
};
Person.prototype.getAge = function () {
    return this._age;
};
Copy the code

The implementation in C++ is

#include <iostream>
#include <node.h>
using namespace v8;
// return Local<String>
Local<String> ToLocalString(const char* str) { 
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate); // The EscapableHandleScope will come in handy
  Local<String> key = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(key);
}

// Person constructor
void Person(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> self = args.This(); // this is similar to the js function this

  Local<String> nameKey = ToLocalString("name");
  Local<String> nameValue = args[0].As<String>();

  Local<String> ageKey = ToLocalString("age");
  Local<Number> ageValue = args[1].As<Number>();
  self->Set(context, nameKey, nameValue).Check();
  self->Set(context, ageKey, ageValue).Check();
  args.GetReturnValue().Set(self);
}
void GetName(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();

  args.GetReturnValue().Set(args.This()->Get(context, ToLocalString("name")).ToLocalChecked());
}
void GetAge(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  args.GetReturnValue().Set(args.This()->Get(context ,ToLocalString("age")).ToLocalChecked());
}

void Init(Local<Object> exports) {
  Isolate* isolate = Isolate::GetCurrent();
  Local<Context> context = isolate->GetCurrentContext();
  HandleScope handleScope(isolate);
  // Define a function template
  Local<FunctionTemplate> person = FunctionTemplate::New(isolate, Person);
  // Set the class name
  person->SetClassName(ToLocalString("Person"));
  // Get the prototype object of the function template
  Local<ObjectTemplate> prototype = person->PrototypeTemplate();
  // Set the value for this object
  prototype->Set(ToLocalString("getName"), FunctionTemplate::New(isolate, GetName));
  prototype->Set(ToLocalString("getAge"), FunctionTemplate::New(isolate, GetAge));

  exports->Set(context, ToLocalString("Person"), person->GetFunction(context).ToLocalChecked()).Check();
}

NODE_MODULE(addon, Init)
Copy the code

Node-gyp rebuild builds the plug-in and then calls it on the JavaScript side

const cpp = require("./build/Release/addon");
const person = new cpp.Person("sundial-dreams".21);
console.log(person.getName());
console.log(person.getAge());
Copy the code

Running effect

The second use is to create an object using an object template, such as implementing the JavaScript function that creates the object below

function createObject(name, age) {
    return { name, age }
}
Copy the code

The implementation on the C++ side is (using object templates)

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();
  Local<ObjectTemplate> obj = ObjectTemplate::New(isolate); // Create an empty object using the object template
  obj->Set(ToLocalString("name"), args[0].As<String>());
  obj->Set(ToLocalString("age"), args[1].As<Number>());
  args.GetReturnValue().Set(obj->NewInstance(context).ToLocalChecked()); // instantiate the object
}
Copy the code
  1. Internal fields

Built-in fields, which relate C++ level data structures to V8 data types, are not visible to JavaScript code and can only be obtained through Object’s specific methods (i.e. private properties). In the ObjectTemplate

  • throughobjectTemplate->SetInternalFieldCount(1)To set the number of built-in fields
  • throughobjectTemplate->InternalFieldCount()To get the number of built-in fields
  • throughObjectTemplateAn instance of the
    • object->SetInternalField(0, vaule);To set the built-in field values, where value is oneExternalType to wrap a pointer to any type
    • The correspondingobject->GetInternalField(0);To get the value of the corresponding built-in field
    • object->SetAlignedPointerInInternalField(0, p);Sets the built-in field value, only the second parameterpA direct is a pointer of any type
    • The correspondingobject->GetAlignedPointerFromInternalField(0);To get the value of the set fieldvoid*Pointer to the

We can modify the Person above to hide the name and age in InternalFileds

#include <iostream>
#include <node.h>
using namespace v8;
// to Local<String>
Local<String> ToLocalString(const char* str) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);
  Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked();
  return scope.Escape(result);
}

// Built-in field
struct person {
  person(const char* name, int age): name(name), age(age) {}
  std: :string name;
  int age;
};

void GetAll(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Object> self = args.Holder(); // Get this at runtime
  Local<External> wrapper = Local<External>::Cast(self->GetInternalField(0)); / / or self - > GetInternalField (0) As "External > ();
  auto p = static_cast<person*>(wrapper->Value()); // Value() returns void*

  char result[1024];
  sprintf(result, "{ name: %s, age: %d }", p->name.c_str(), p->age);
  args.GetReturnValue().Set(ToLocalString(result));
}
// { getAll() { return `{ name: ${name}, age: ${age} }` } }
void CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  HandleScope handleScope(isolate);
  Local<Context> context = isolate->GetCurrentContext();

  Local<ObjectTemplate> objectTemplate = ObjectTemplate::New(isolate);
  objectTemplate->SetInternalFieldCount(1); // Set the number of built-in fields
  Local<Object> object = objectTemplate->NewInstance(context).ToLocalChecked();

  Local<String> name = args[0].As<String>();
  Local<Number> age = args[1].As<Number>();
  String::Utf8Value nameValue(isolate, name); // Can be converted to char* using *nameValue
  auto p = new person(*nameValue, int(age->Value())); // The person pointer to the built-in field
  
  object->SetInternalField(0, External::New(isolate, p)); // Wrap the person pointer with the External data type, where the second argument to New is a void* pointer
  Local<Function> getAll = FunctionTemplate::New(isolate, GetAll)->GetFunction(context).ToLocalChecked(); // GetAll to get the value of the built-in field
  object->Set(context, ToLocalString("getAll"), getAll).Check();
  args.GetReturnValue().Set(object);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "createObject", CreateObject);
}

NODE_MODULE(addon, Init)

Copy the code

Node-gyp rebuild builds plug-in

const cpp = require("./build/Release/addon");
const person = cpp.createObject("sundial-dreams".21);
console.log(person);
console.log(person.getAll());
Copy the code

perform

node::ObjectWrap

That is, wrapping a JavaScript class in C++. Although the wrapping of JavaScript classes is described above, it is actually based on JavaScript language features (constructors, prototype chains, etc.). C++ is an object-oriented language, so what good is it if you can’t use C++ class? Node.js therefore provides the ObjectWrap class (under the node_object_wrap.h header) to help us create JavaScript classes using C++ classes. The BigNumber in the example is also written in this way.

  • vodi Wrap(Local<Object> handle): will be incomingObjectThe local handle is made into one with the currentObjectWrapObject is the object associated with setting the built-in field
  • static T* Unwrap(Local<Object> handle)From:ObjectGets the object associated with the local handleObjectWrapobject

Based on node::ObjectWrap, we re-implement the Person class

#include <iostream> #include <string> #include <node.h> #include <node_object_wrap.h> using namespace v8; // to Local<String> Local<String> ToLocalString(const char* str) { Isolate* isolate = Isolate::GetCurrent(); EscapableHandleScope scope(isolate); Local<String> result = String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked(); return scope.Escape(result); } // Person Class class Person : public node::ObjectWrap { public: static void Init(Local<Object>); private: explicit Person(const char* name, int age) : name(name), age(age) { }; ~Person() override = default; Static void New(const FunctionCallbackInfo<Value> &); static void GetName(const FunctionCallbackInfo<Value> &); static void GetAge(const FunctionCallbackInfo<Value> &); std::string name; int age; }; // Class void Person::Init(Local< Class V8 ::Object> exports) {Isolate* Isolate = exports->GetIsolate(); Local<Context> context = isolate->GetCurrentContext(); Local<ObjectTemplate> dataTemplate = ObjectTemplate::New(isolate); dataTemplate->SetInternalFieldCount(1); Local<Object> data = dataTemplate->NewInstance(context).tolocalchecked (); // The third argument is the Data() information passed to the New function args, Args.Data() Local<FunctionTemplate> fnTemplate = FunctionTemplate::New(ISOLATE, New, Data); Person::New method fnTemplate->SetClassName(ToLocalString("Person")); fnTemplate->InstanceTemplate()->SetInternalFieldCount(1); // Using this macro, set the prototype function NODE_SET_PROTOTYPE_METHOD(fnTemplate, "getName", getName); NODE_SET_PROTOTYPE_METHOD(fnTemplate, "getAge", GetAge); Local<Function> constructor = fnTemplate->GetFunction(context).ToLocalChecked(); Data ->SetInternalField(0, constructor); exports->Set(context, ToLocalString("Person"), constructor).FromJust(); } // True constructor void Person::New(const FunctionCallbackInfo<Value> &args) {Isolate* Isolate = args.getisolate (); Local<Context> context = isolate->GetCurrentContext(); // Call if (args.IsConstructCall()) {Local<String> t = args[0].As<String>(); String::Utf8Value name(isolate, t); int age = int(args[1].As<Number>()->Value()); auto person = new Person(*name, age); Person ->Wrap(args.this ()); person->Wrap(args.this ()); args.GetReturnValue().Set(args.This()); } else {// Call const int argc = 2 from Person("aa", 21); Local<Value> argv[argc] = {args[0], args[1]}; // args.data () is the Data of the Init function, Get the Person constructor from the built-in field (as set in the Init Function) Local<Function> constructor = args.Data().As<Object>()->GetInternalField(0).As<Function>(); Local<Object> result = constructor->NewInstance(context, argc, argv).ToLocalChecked(); args.GetReturnValue().Set(result); } } void Person::GetName(const FunctionCallbackInfo<Value> &args) { Isolate* isolate = args.GetIsolate(); HandleScope handleScope(isolate); // Here is the point, Auto Person = node::ObjectWrap::Unwrap< person >(args.holder ()); args.GetReturnValue().Set(ToLocalString(person->name.c_str())); } void Person::GetAge(const FunctionCallbackInfo<Value> &args) { Isolate* isolate = args.GetIsolate(); HandleScope handleScope(isolate); auto person = node::ObjectWrap::Unwrap<Person>(args.Holder()); args.GetReturnValue().Set(Number::New(isolate, person->age)); } void Init(Local<Object> exports) { Person::Init(exports); } NODE_MODULE(addon, Init)Copy the code

To summarize, the node::ObjectWrap approach combines the function prototype chain template with the object’s built-in fields, encapsulates the C++ side of the data structure as private, and exposes some common methods to the JavaScript side. The NODE_SET_PROTOTYPE_METHOD macro is used directly to set the prototype object for the function template. This macro is actually a wrapper for the set function prototype mentioned earlier. The whole process of wrapping an object is to make a connection between args.this () and the current class pointer (Person). With This connection, we can get the current class pointer (Person) in args.this () and do whatever we want (return Person ->name, etc.). Node ::ObjetWrap also puts a lot of finishing touches on node::ObjetWrap. If you’re interested, try reading the source code for Node ::ObjectWrap.

The JavaScript side

const { Person } = require("./build/Release/addon");
const person = new Person("dengpengfei".21); / / the new call
console.log(person.getName());
console.log(person.getAge());

const person1 = Person("sundial-dreams".21); / / use directly
console.log(person1.getName());
console.log(person1.getAge());
Copy the code

The execution result

libuv

Libuv is another node.js library that provides an abstraction for asynchronous operations on multiple operating systems.

Due to limited space, this article will not cover libuv in detail. Interested readers can read the official Libuv documentation

Based on the NAN

Before we get to NAN, let’s consider what goes wrong with developing node.js C++ plug-ins natively

  • The first is version incompatibility. Since V8 is iterating fast, node.js is following V8’s lead, so the V8 API changes will directly cause the current node.js C++ plug-in to run in error. For example, the code above runs stably on node.js v12. But Node.js V10 doesn’t necessarily work, and It certainly doesn’t work in Node.js V8 (most of the V8 APIS have changed). Think about how your C++ plugin won’t run on other versions of node.js.
  • The second point is that the V8 API is quite complex, so writing a node.js C++ plug-in requires some basic V8 concepts.

From this premise, NAN (official NAN documentation) was born. NAN is a Native Abstraction for Node.js, a set of abstract interfaces for node. js Native modules. NAN provides a stable API for cross-versioning Node.js and a set of useful apis to simplify some of the tedious development processes.

Official explanation: A header file filled with macro and utility goodness for making add-on development for Node.js easier across versions 0.8, 0.10, 0.12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 and 13.

In other words, Nan is a header file that encapsulates a bunch of macros and utility functions. In this case, the macros are determined by the Node.js version to determine how to expand.

At the beginning of NAN experience

  • Installation of nan
npm install --save nan
Copy the code
  • Configure the binding. Gyp
{
  "targets": [{"target_name": "addon"."sources": [
        "nan.cpp"]."include_dirs": [
        "
      ]]}}Copy the code

In fact, more than ”

Nan’s header file path, which includes nan’s header file. Modify the project’s cmakelists. TXT and add the following instructions on the original basis

include_directories(./node_modules/nan)
Copy the code
  • Write a plug-in using nan

Let’s take the filter function as an example and use Nan to implement it

#include <node.h>
#include <nan.h>
using namespace v8;

// Declare functions using the NAN_METHOD macro, which expands like void Filter(const FunctionCallbackInfo
      
       & info) {}
      
NAN_METHOD(Filter) {
  Local<Array> array = info[0].As<Array>();
  Local<Function> fn = info[1].As<Function>();
  Local<Context> context = Nan::GetCurrentContext(); // Each V8 data type has a corresponding encapsulation in Nan
  Local<Array> result = Nan::New<Array>();

  Local<Value> argv[3] = { Nan::New<Object>(), Nan::Null(), array };
  for (uint32_t i = 0, j = 0; i < array->Length(); i++) {
    argv[0] = array->Get(context, i).ToLocalChecked();
    argv[1] = Nan::New<Number>(i);
    Local<Value> v = Nan::Call(fn, Nan::New<Object>(), 3, argv).ToLocalChecked();
    if (v->IsTrue()) {
      result->Set(context, j++, argv[0]).FromJust();
    }
  }

  info.GetReturnValue().Set(result);
}
// Declare the Init function with NAN_MODULE_INIT
NAN_MODULE_INIT(Init) {
  Nan::Export(target, "filter", Filter);
}

NODE_MODULE(addon, Init)
Copy the code

We first use the NAN_METHOD macro to declare a plug-in function that expands like the void Filter(const FunctionCallbackInfo

& info) {}, where info is the argument object passed in. Nan also encapsulates the construction of data types by getting the corresponding handle through Nan::New

(). The NAN_MODULE_INIT macro is used to create the Init function. Target is an argument passed by Init, like the exports of void Init(Local
exports), and Nan::Export is used to set the module Export.

Node-gyp rebuild builds plug-in

The JavaScript side

const { filter } = require("./build/Release/addon");
console.log(filter([1.2.2.3.4], (i, v) => v >= 2));
Copy the code

Execute JavaScript

Nan basic types

In fact, some of the Nan apis will be easier to understand if you have a V8 base, and many of the things in Nan are very similar to V8 apis

  • Function parameter types

In Nan to a V8 FunctionCallbackInfo and ReturnValue encapsulation, through Nan: : FunctionCallbackInfo and Nan: : ReturnValue to access. For example,

void Hello(const Nan::FunctionCallbackInfo<Value>& args) {
  args.GetReturnValue().Set(Nan::New("Hello World").ToLocalChecked());
}
Copy the code
  • The data type

Nan::New

() is used to create handlers of the corresponding Type, such as Nan::New

(12), Nan::Undefined(), Nan::Null(), Nan::True(), Nan::False(), Nan::Undefined(), Nan::Null(), Nan::True(), Nan::False(), Nan: : EmptyString (). And Nan::To

() is used To do the data Type conversion.


  • Handle scope

Nan: namely: HandleScope and Nan: : EscapableHandleScope, Nan HandleScope of V8 and EscapableHandleScope did an encapsulation

  • Persistent handle

    Nan::Persistent and Nan::Global, and since the V8API is always changing, Nan also encapsulates V8’s Persistent/Global handles

  • The script

For V8’s scripts, including Nan::CompileScript() and Nan::RunScript(), you can try Eval as Nan

  • Helper function

Remember that templates, FunctionTemplate and ObjectTemplate, are cumbersome to operate on each template, so Nan encapsulates these operations to simplify the template process.

Nan::SetMethod(): Sets the function for the object handle

Nan: : Set ()/Nan: : Get ()/Nan: : from the ()/Nan: : Delete () : the object handle to Set/Get/key exists/Delete key

Take the createObject function as an example

void CreateObject(const Nan::FunctionCallbackInfo<Value>& info) {
  Local<Object> object = Nan::New<Object>();
  // Set properties for the object
  Nan::Set(object, Nan::New("name").ToLocalChecked(), info[0].As<String>());
  Nan::Set(object, Nan::New("age").ToLocalChecked(), info[1].As<Number>());
  // Set the function for the object
  Nan::SetMethod(object, "getAll"[] (const Nan::FunctionCallbackInfo<Value>& args) -> void {
    Local<Object> self = args.Holder();
    Local<String> name = Nan::Get(self, Nan::New("name").ToLocalChecked()).ToLocalChecked().As<String>();
    Local<Number> age = Nan::Get(self, Nan::New("age").ToLocalChecked()).ToLocalChecked().As<Number>();
    Nan::Utf8String n(name); / / use the Nan: : Utf8String
    int a = int(age->Value());
    
    char result[1024];
    sprintf(result, "{ name: %s, age: %d }", *n, a);
    args.GetReturnValue().Set(Nan::New(result).ToLocalChecked());
  });

  info.GetReturnValue().Set(object);
}
Copy the code

Nan::SetPrototypeMethod(): Sets prototype methods for function templates

Nan::SetPrototype(): Sets the prototype properties for the function template

For example,

Local<FunctionTemplate> fnTemplate = Nan::New<FunctionTemplate>();
Nan::SetPrototype(fnTemplatem, "name", Nan::New("fff").ToLocalChecked());
Nan::SetPrototype(fnTemplatem, "age", Nan::New(2));
Nan::SetPrototypeMethod(fnTemplate, "getAll", GetAll);
Copy the code

Nan::Call: a utility method that makes synchronous function calls, modeled as follows

inline MaybeLocal<v8::Value> Call(v8::Local<v8::Function> fun, v8::Local<v8::Object> recv, int argc, v8::Local<v8::Value> argv[])
Copy the code

Nan::ObjectWrap

ObjectWrap encapsulates node::ObjectWrap, and adds some apis to adapt to earlier versions of Node.js. Back to the Person class wrapped in Node ::ObjectWrap, implemented using Nan

#include <iostream>
#include <string>
#include <node.h>
#include <nan.h>

using namespace v8;
// Person Class, extends Nan::ObjectWrap
class Person : public Nan::ObjectWrap {
  public:
    static NAN_MODULE_INIT(Init) { // void Init(Local<Object> exports)
      Local<FunctionTemplate> fnTemplate = Nan::New<FunctionTemplate>(New);
      fnTemplate->SetClassName(Nan::New("Person").ToLocalChecked());
      fnTemplate->InstanceTemplate()->SetInternalFieldCount(1);
      Nan::SetPrototypeMethod(fnTemplate, "getName", GetName);
      Nan::SetPrototypeMethod(fnTemplate, "getAge", GetAge);
      constructor().Reset(Nan::GetFunction(fnTemplate).ToLocalChecked()); // save a constructor function in global
      Nan::Set(target, Nan::New("Person").ToLocalChecked(), Nan::GetFunction(fnTemplate).ToLocalChecked());
    }

  private:
    explicit Person(const char* name, int age) : name(name), age(age) {}

    static NAN_METHOD(New) { // void New(const FunctionCallbackInfo<Value>& args)
      if (info.IsConstructCall()) {
        Local<String> name = info[0].As<String>();
        Local<Number> age = info[1].As<Number>();
        Nan::Utf8String n(name);
        int a = int(age->Value());
        auto person = new Person(*n, a);
        person->Wrap(info.This()); // like node::ObjectWrap
        info.GetReturnValue().Set(info.This());
        return;
      }
      const int argc = 2;
      Local<Value> argv[argc] = {info[0], info[1]};
      Local<Function> c = Nan::New(constructor());
      info.GetReturnValue().Set(Nan::NewInstance(c, argc, argv).ToLocalChecked());
    }

    static NAN_METHOD(GetName) {
      auto person = static_cast<Person*>(Nan::ObjectWrap::Unwrap<Person>(info.Holder()));
      info.GetReturnValue().Set(Nan::New(person->name.c_str()).ToLocalChecked());
    }

    static NAN_METHOD(GetAge) {
      auto person = static_cast<Person*>(Nan::ObjectWrap::Unwrap<Person>(info.Holder()));
      info.GetReturnValue().Set(Nan::New(person->age));
    }
    // define a global handle of constructor
    static inline Nan::Global<Function>& constructor() {
      static Nan::Global<Function> _constructor; // global value
      return _constructor;
    }

    std::string name;
    int age;
};

NODE_MODULE(nan_addon02, Person::Init)

Copy the code

This is mostly written the same way as Node ::ObjectWrap, only with macros instead. In the Node ::ObjectWrap implementation’s Person class, use the object’s built-in fields to hold constructor’s, Is the data – > SetInternalField (0, constructor); . In Nan, however, a Global handle holds Constructor directly. Except for this, the node::ObjectWrap is basically the same as the node::ObjectWrap.

Based on the N – API

N-api (n-API documentation) is an API for building native plug-ins, based in C, independent of the JavaScript runtime (V8), and maintained as part of Node.js. The APi is a stable application binary interface (ABI) on all versions of Node.js. The goal is to keep the plug-in isolated from changes to the underlying JavaScript engine (V8) and run under higher versions of Node.js without recompiling.

N-api provides an API that is typically used to create and manipulate JavaScript values. The API has the following characteristics

  • N-api is a C-style API, and many functions are also c-style 💢

  • All N-API calls return a status code of type NAPi_STATUS. Indicates whether the API was successfully invoked

  • The return value of the API is passed through the out argument (the return value of the function is already napi_STATUS, so the normal return value can only be followed by a pointer).

  • All JavaScript values are wrapped with a type called NAPi_value, which can be understood as var/let in JavaScript

  • If the API call gets the wrong status code. You can use napi_get_last_error_info to obtain the last error information

N – API early adopters

Again, this article uses the filter function as an example to give n-API a try

The first step is to modify binding.gyp (n-API can also be built directly using Node-gyp).

{
  "targets": [{"target_name": "addon"."sources": [
        "napi.cpp"]]}}Copy the code

Implement the filter function using n-API

#include <node.h>
#include <node_api.h>
#include <cstdio>
#include <cstdlib>
// Write a macro for the status judgment of each NAPI call, and call is the result of the call
#define NAPI_CALL(env, call)                                   \
     do {                                                      \
       napi_status status = (call);                            \
       if(status ! = napi_ok) { \ const napi_extended_error_info* error_info = nullptr; \ napi_get_last_error_info((env), &error_info); \ bool is_pending; \ napi_is_exception_pending((env), &is_pending); \if(! is_pending) { \ const char* message = error_info->error_message; \ napi_throw_error((env),"ERROR", message);          \
           return nullptr;                                     \
         }                                                     \
       }                                                       \
    } while(false)


napi_value filter(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value argv[2];
  // Get the args parameter
  NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, nullptr.nullptr));
  napi_value arr = argv[0], fn = argv[1];
  // Get the length of the array passed in
  uint32_t length = 0;
  NAPI_CALL(env, napi_get_array_length(env, arr, &length));
  // Create an array to store the results
  napi_value result;
  NAPI_CALL(env, napi_create_array(env, &result));
  napi_value fn_argv[3] = { nullptr.nullptr, arr };

  for (uint32_t i = 0, j = 0; i < length; i++) {
    napi_value fn_ret;
    uint32_t fn_argc = 3;
    napi_value index, arr_val;
    // Get item I of the array
    NAPI_CALL(env, napi_get_element(env, argv[0], i, &arr_val));
    // Encapsulate I as napi_value
    NAPI_CALL(env, napi_create_int32(env, (int32_t) i , &index));
    fn_argv[0] = arr_val;
    fn_argv[1] = index;
    // Call the callback function
    NAPI_CALL(env, napi_call_function(env, arr, fn, fn_argc, fn_argv, &fn_ret));
    // Get the result of the call
    bool ret;
    NAPI_CALL(env, napi_get_value_bool(env, fn_ret, &ret));
    if (ret) {
      // Sets the value for the result arrayNAPI_CALL(env, napi_set_element(env, result, j++, arr_val)); }}return result;
}

napi_value init(napi_env env, napi_value exports) {
  // Wrap the filter function as napi_value
  napi_value filter_fn;
  napi_create_function(env, "filter", NAPI_AUTO_LENGTH, filter, nullptr, &filter_fn);
  napi_set_named_property(env, exports, "filter", filter_fn);
  return exports;
}
// Use NAPI_MODULE to initialize macros (not NODE_MODULE)
NAPI_MODULE(addon, init)
Copy the code

Each n-API call is judged by the status returned, similar to the code below

napi_status status1 = napi_get_cb_info(env, info, &argc, argv, nullptr.nullptr);
if(status1 ! = napi_ok) {// error handle
  return nullptr;
}
Copy the code

It is obviously impractical to write this code for every call, so encapsulate the n-API call and error handling into a single macro, the NAPI_CALL macro above. The outer do {} while(0) is actually 😊 for NAPI_CALL followed by a semicolon. The filter function is implemented in the same way as before, but with a different API. For example, the parameter is not args[0]/args[1]. Instead, napi_get_cb_info(env, info, &argc, argv, NULlptr, NULlptr); , argv[0]/argv[1], as well as function calls and corresponding type creation have been replaced with n-API.

Node-gyp rebuild builds plug-in

The JavaScript side

const { filter } = require("./build/Release/addon");
console.log(filter(["abc".1.3."hello"."b".true], (v, i) => (typeof v === "string")));
Copy the code

The execution result

N-api basic introduction

  • Basic data types

    1. Napi_status: indicates the status code of an N-API call that succeeded or failed. It is an enumeration type with many values. The most commonly used type is napi_OK, which checks whether the call succeeded

    2. Napi_env: represents the specific state context of the underlying N-API.

    3. Napi_value: An abstract data type that represents a JavaScript value. And you can use napi_get_… Wait for the API to get the actual value, for example

    bool ret;
    NAPI_CALL(env, napi_get_value_bool(env, fn_ret, &ret)); // The NAPI_CALL macro above
    Copy the code
    1. napi_handle_scope: General handle scope type, similar tov8::HandleScopeBut you have to use itnapi_open_handle_scope()/napi_close_handle_scope()Open and close to use handle scope.
    {
        napi_handle_scope scope;
    	  NAPI_CALL(env, napi_open_handle_scope(env, &scope));
        // do something
        NAPI_CALL(env, napi_close_handle_scope(env, scope));
    }
    Copy the code
    1. napi_escapable_handle_scope: escapable handle scope type, also similar tov8::EscapableHandleScopeStill in usenapi_open_escapable_handle_scope()/napi_close_escapable_handle_scope()To open or close the handle scope, and usenapi_escape_handle()To allow some handles to escape the current scope, analogous toscope.Escape(result)
    napi_value make_string(napi_env env, const char* name) {
      napi_escapable_handle_scope handle_scope;
      NAPI_CALL(env, napi_open_escapable_handle_scope(env, &handle_scope));
      napi_value val = nullptr;
      // create a string
      NAPI_CALL(env, napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &val));
      napi_value ret = nullptr;  // Save the handle after escape
      NAPI_CALL(env, napi_escape_handle(env, handle_scope, val, &ret));
      NAPI_CALL(env, napi_close_escapable_handle_scope(env, handle_scope));
      return ret;
    }
    Copy the code
    1. napi_callback/napi_callback_info: analogy to thev8::FunctionCallback/v8::FunctionCallbackInfo.napi_callbackThe function is defined as follows
    typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info);
    Copy the code

    V8 ::FunctionCallback: napi_callback_info: v8: FunctionCallback: napi_callback_info

    The corresponding parameter information can be obtained using the napi_get_cb_info() function, which is modeled as follows

    napi_status napi_get_cb_info(
        napi_env env,               // [in] NAPI environment handle
        napi_callback_info cbinfo,  // [in] callback-info Handle
        size_t* argc,      // [in-out] Specifies the number of parameters
        napi_value* argv,  // [out] parameter array
        napi_value* this_arg,  // [out] this of the current function
        void** data);          // [out] data pointer similar to v8 args.data ();
    Copy the code

    For example

    size_t argc = 2;
    napi_value argv[2], self;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &self, nullptr));
    Copy the code
    1. napi_extended_error_info: Error type, used fromnapi_get_last_error_info()The second argument to the function is thisnapi_extended_error_infoType.
  • Create an N-API(JavaScript) type

    Analogous to V8 ::Number::New() for creating numeric types, n-API also provides functions to create some commonly used JavaScript types

    1. napi_create_array/napi_create_array_with_lengthCreate an array
    napi_value array = nullptr;
    NAPI_CALL(env, napi_create_array(env, &array)); // An indefinite array
    napi_value array_1024 = nullptr;
    NAPI_CALL(env, napi_create_array_with_length(env, 1024, &array_1024)); // Fixed length array
    Copy the code
    1. napi_create_objectCreate an object
    napi_value object = nullptr;
    NAPI_CALL(env, napi_create_object(env, &object));
    Copy the code
    1. napi_create_int32/napi_create_uint32/napi_create_int64/napi_create_double: Creates a numeric type
    int val = 32;
    napi_value number = nullptr;
    NAPI_CALL(env, napi_create_int32(env, (int_32)val, &number)); //
    Copy the code
    1. napi_create_string_utf8Create string
    napi_value str = nullptr;
    // The length of the third argument, NAPI_AUTO_LENGTH, which is iterated but terminated when null is encountered
    NAPI_CALL(env, napi_create_string_utf8(env, "hello world", NAPI_AUTO_LENGTH, &str));
    Copy the code
    1. napi_create_function:napi_callbackType creates a function. The function prototype is defined as follows
    napi_status napi_create_function(napi_env env,
                                     const char* utf8name, // String function name
                                     size_t length, // Utf8NAME length, NAPI_AUTO_LENGTH
                                     napi_callback cb, // The corresponding function
                                     void* data, // To the data field of napi_callback_info
                                     napi_value* result); / / [out]
    Copy the code

    For example

    napi_status filter_fn = nullptr;  
    napi_create_function(env, "filter", NAPI_AUTO_LENGTH, filter, nullptr, &filter_fn);
    Copy the code
    1. napi_get_boolean/napi_get_global/napi_get_null/napi_get_undefined: These functions are written so straightforwardly that no explanation is required
    napi_value bool_val = nullptr;
    NAPI_CALL(env, napi_get_boolean(env, false, &bool_val)); // get false;
    
    napi_value global = nullptr;
    NAPI_CALL(env, napi_get_global(env, &global)); // get global object
    
    napi_value undefined = nullptr;
    NAPI_CALL(env, napi_get_undefined(env, &undefined)); // get undefined
    
    napi_value null_val = nullptr;
    NAPI_CALL(env, napi_get_null(env, &null_val)); // get null
    Copy the code
  • Conversion of n-API type to C type

    Convert NAPI_value to type C for easy operation

    1. napi_get_value_bool: getboolType, whose function prototype is as follows
    napi_status napi_get_value_bool(napi_env env,
                                    napi_value value, // Handle to
                                    bool* result); // [out] Outputs the corresponding bool value
    Copy the code
    1. napi_get_value_double/napi_get_value_int32/napi_get_value_int64: getdouble/int32/int64type
    double val;
    napi_value double_val = nullptr;
    NAPI_CALL(env, napi_create_double(env, 233.2, &double_val));
    NAPI_CALL(env, napi_get_value_double(env, double_val, &val)); // get double
    printf("%f\n", val);
    Copy the code
    1. napi_get_value_string_utf8: gets a string, whose function prototype is as follows
    napi_status napi_get_value_string_utf8(napi_env env,
                                           napi_value value, // JavaScript side string
                                           char* buf, / / the buffer
                                           size_t bufsize, // Buffer length
                                           size_t* result); // [out] The length of the result string
    Copy the code

    For example,

    napi_value str_val = make_string(env, "hello world!");
    char str[1024];
    size_t str_size;
    NAPI_CALL(env, napi_get_value_string_utf8(env, str_val, str, 1024, &str_size));
    for (size_t i = 0; i < str_size; i++) {
      printf("%c", str[i]);
    }
    printf("\n");
    Copy the code
  • JavaScript type manipulation

    That is, perform some abstract operations on JavaScript values, including casting JavaScript values to JavaScript values of a specific type, checking the type of JavaScript values, and checking whether two JavaScript values are equal. Part of the function is relatively simple, directly write function prototype

    1. napi_coerce_to_bool/napi_coerce_to_number/napi_coerce_to_object/napi_coerce_to_string: Force conversion tobool/number/object/stringAnalogy inv8::Local<Boolean>::Cast()Such as the following JavaScript statement
    var a = Boolean(b);
    Copy the code

    This is implemented using n-API

    napi_value a;
    NAPI_CALL(env, napi_coerce_to_bool(env, b, &a));
    Copy the code
    1. napi_typeof: similar to JavaScript sidetypeofOperation, whose function prototype is as follows
    napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result); / / napi_valuetype JavaScript is the enumeration of data types, values for: napi_string | napi_null | napi_object, etc
    Copy the code
    1. napi_instanceof: similar to JavaScript sideinstanceofOperation, whose function prototype is as follows
    napi_status napi_instanceof(napi_env env,
                                napi_value object, / / object
                                napi_value constructor, // constructor
                                bool* result); / / [out]
    Copy the code
    1. napi_is_array: similar to JavaScriptArray.isArray(), the function prototype is as follows
    napi_status napi_is_array(napi_env env,
                              napi_value value,
                              bool* result); / / [out]
    Copy the code
    1. napi_strict_equals: Determines whether two JavaScript values are strictly equal= = =, its function prototype is as follows
    napi_status napi_strict_equals(napi_env env,
                                   napi_value lhs, / / the left value
                                   napi_value rhs, / / right value
                                   bool* result); / / [out]
    Copy the code
  • Handle scope

    Napi_handle_scope and napi_escapable_handLE_scope have been explained above and their corresponding usage will not be described here.

  • The object operation

    N-api also encapsulates methods to operate on objects, starting with two useful types, napi_property_Attributes and napi_property_Descriptor

    1. napi_property_attributes: flag used to control the behavior of properties set on an object, enumerable/configurable/writable, etc., so it is an enumeration type
    typedef enum {
      napi_default = 0.// The default value is read-only, non-enumerable, and non-configurable
      napi_writable = 1 << 0.// The property is writable
      napi_enumerable = 1 << 1.// Attributes are enumerable
      napi_configurable = 1 << 2.// Properties are configurable
      napi_static = 1 << 10.// Class static attribute, used for napi_define_class
    } napi_property_attributes;
    Copy the code
    1. napi_proptery_descriptor: Property structure, containing property name, specific value,getter/setterMethods etc.
    typedef struct {
      const char* utf8name; // Attribute names utf8name and name must be supplied
      napi_value name; // Attribute names utf8name and name must be supplied
      napi_callback method; // Property value function, if provided, value and getter and setter must be null
      napi_callback getter; // Getters, if there is one, value and method must be null
      napi_callback setter; // Setter functions, if there is one, value and method must be null
      napi_value value; Getters and setters and method and data must be null if this is present
      napi_property_attributes attributes; // Attribute behavior flags
      void* data; // This data is passed to method, getter, setter
    } napi_property_descriptor;
    Copy the code
    1. napi_define_properties: Defines attributes for objects, similar to those in JavaScriptObject.defineProperties(), its function prototype is as follows
    napi_status napi_define_properties(napi_env env,
                                       napi_value object, / / object
                                       size_t property_count, // Number of attributes
                                       const napi_property_descriptor* properties); // Attribute array
    Copy the code

    Now implement the createObject function again, but this time based on Object.defineProperties(), similar to the JavaScript code below

    function createObject(name, age) {
        let object = {};
        Object.defineProperties(object, {
            name: { enumerable: true.value: name },
            age: { enumerable: true.value: age },
        });
        return object;
    }
    Copy the code

    Now using n-API to implement this function is

    napi_value create_object(napi_env env, napi_callback_info info) {
      size_t argc;
      napi_value args[2], object;
      NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr.nullptr));
      NAPI_CALL(env, napi_create_object(env, &object));
      napi_property_descriptor descriptors[] = {
              {"name".0.0.0.0, args[0], napi_enumerable, 0},
              {"age".0.0.0.0, args[1], napi_enumerable, 0}}; NAPI_CALL(env, napi_define_properties(env, object,sizeof(descriptors) / sizeof(descriptors[0]), descriptors));
      return object;
    }
    Copy the code
    1. napi_get_property_names: Gets all the attribute names of an object whose function prototype is as follows
    napi_status napi_get_property_names(napi_env env,
                                        napi_value object, / / object
                                        napi_value* result); // The result is an array
    Copy the code

    example

    napi_value prop_list = nullptr; 
    NAPI_CALL(env, napi_get_property_names(env, object, &prop_list)); // Return prop_list will be an array
    Copy the code
    1. napi_set_property/napi_get_property/napi_has_property/napi_delete_property/napi_has_own_property: Set object properties/obtain object properties/determine whether an object has a property/delete object properties/determine whether an object property is its own property (non-prototype property). This is a little bit easier, so I’m just going to paste the prototypes of these functions
    napi_status napi_set_property(napi_env env,
                                  napi_value object, / / object
                                  napi_value key, / / key
                                  napi_value value); / / value
    
    napi_status napi_get_property(napi_env env, 
                                  napi_value object, / / object
                                  napi_value key, / / key
                                  napi_value* result); // [out] attribute value
    
    napi_status napi_has_property(napi_env env,
                                  napi_value object, / / object
                                  napi_value key, / / key
                                  bool* result); // [out
    
    napi_status napi_delete_property(napi_env env,
                                     napi_value object, / / object
                                     napi_value key, / / key
                                     bool* result); // [out] Check whether the deletion is successful
    
    napi_status napi_has_own_property(napi_env env,
                                      napi_value object, / / object
                                      napi_value key, / / key
                                      bool* result); // [out
    Copy the code
    1. napi_set_named_property/napi_get_named_property/napi_has_named_property: Also set object properties/get object properties/check whether object properties exist, but different from the above,keyIs aconst char*Type, which is also useful in some scenarios, and its function prototype is as follows
    napi_status napi_set_named_property(napi_env env,
                                        napi_value object, / / object
                                        const char* utf8Name, / / key
                                        napi_value value); / / property values
    napi_status napi_get_named_property(napi_env env, 
                                        napi_value object, / / object
                                        const char* utf8Name, / / key
                                        napi_value* result); // The attribute value returned by [out]
    napi_status napi_has_named_property(napi_env env,
                                        napi_value object, / / object
                                        const char* utf8Name, / / key
                                        bool* result); // [out
    Copy the code
    1. napi_set_element/napi_get_element/napi_has_element/napi_delete_element: is also set object property value/get object property value/determine whether object property value exists/delete object property value, here functionkeyIs auint32_tType, more suitable for arrays, and its function prototype is as follows
    napi_status napi_set_element(napi_env env,
                                 napi_value object, / / object
                                 uint32_t index, // key/index
                                 napi_value value); / / value
    
    napi_status napi_get_element(napi_env env,
                                 napi_value object, / / object
                                 uint32_t index, // key/index
                                 napi_value* result); // [out] attribute value
    
    napi_status napi_has_element(napi_env env,
                                 napi_value object, / / object
                                 uint32_t index, // key/index
                                 bool* result); //[out
    
    napi_status napi_delete_element(napi_env env,
                                    napi_value object, / / object
                                    uint32_t index, // key/index
                                    bool* result); // [out] Check whether the deletion is successful
    Copy the code
  • Function operation

    Mainly the creation and invocation of functions and their use as constructors

    1. napi_create_function: This method has been mentioned above and will not be repeated here.
    2. napi_function_call: calls a function whose prototype is as follows
    napi_status napi_call_function(napi_env env,
                                   napi_value recv, //JavaScript side this object
                                   napi_value func, / / function
                                   size_t argc, // The number of arguments passed to the function
                                   const napi_value* argv, // Pass the argument to the function
                                   napi_value* result); // [out] result of function call
    Copy the code

    For examples, see the implementation of the filter function above

    1. napi_new_target: equivalent to JavaScript sidenew.targetCheck whether the current call is a constructor, rememberPersonClass, now use n-API to implement it
    napi_value person(napi_env env, napi_callback_info info) {
      size_t argc;
      napi_value args[2], self, target;
      NAPI_CALL(env, napi_get_new_target(env, info, &target));
      if (target == nullptr) { // Non-new calls
        napi_throw_error(env, "ERROR"."need new");
        return nullptr;
      }
      NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, &self, nullptr));
      NAPI_CALL(env, napi_set_named_property(env, self, "name", args[0]));
      NAPI_CALL(env, napi_set_named_property(env, self, "age", args[1]));
      return self;
    }
    Copy the code

    Use napi_new_target to determine if the function is called with new, and if it is called with new, that is, new Person(” AAA “, 21), we get the correct result

Otherwise Person(” AAA “, 21) calls directly and throws an exception

  1. napi_new_instance: that is, to call a function as a constructor and generate an instance, its function prototype is as follows
napi_status napi_new_instance(napi_env env,
                              napi_value constructor, // constructor
                              size_t argc, // Number of parameters
                              const napi_value* argv, / / parameters
                              napi_value* result); // And the object out of the instance (new)
Copy the code

Based on the Person class implemented above, we can implement a createPerson function, similar to the JavaScript side

function createPerson(name, age) { return new Person(name, age) }
Copy the code

The C++ code is as follows

napi_value create_person(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value args[2], instance = nullptr, constructor = nullptr;
  NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, nullptr.nullptr));
  NAPI_CALL(env, napi_create_function(env, "Person", NAPI_AUTO_LENGTH, person, nullptr, &constructor));
  NAPI_CALL(env, napi_new_instance(env, constructor, argc, args, &instance));
  return instance;
}
Copy the code

Based on the node – addon – API

Since n-API is a new feature of Node.js V8, it will not work in versions later than Node.js V8, the same problem as native extensions, hence the Node-Addon-API package. Node-addon-api (node-addon-api official documentation) is a C++ wrapper for n-api. It wraps some low-overhead wrapper classes based on n-api, enabling the use of C++ classes and other features to simplify the use of n-api, and the packaged plug-in can run across multiple node.js versions.

The node – addon – API

  • Node-addon-api installation: as with Nan, a stack of header files and C++ source files is installed
npm install --save node-addon-api
Copy the code
  • Modify the binding. Gyp
{
  "targets": [{"target_name": "addon"."include_dirs": [
        "
      ].Add the following dependent libraries based on the current node.js version
      "dependencies": [
        "
      ]."cflags!": ["-fno-exceptions"]."cflags_cc!": ["-fno-exceptions"]."defines": [
        "NAPI_DISABLE_CPP_EXCEPTIONS" Remember to add this macro]."sources": [
        "node_addon_api.cpp"]]}}Copy the code

Add node-addon-api headers to include_dirs like Nan. Require (‘node-addon-api’) requires node-addon-api/index.js to require

var path = require('path');

var versionArray = process.version
  .substr(1)
  .replace(/ -. * $/.' ')
  .split('. ')
  .map(function(item) {
    return +item;
  });
// Node version check to see if there is n-API
var isNodeApiBuiltin = (
  versionArray[0] > 8 ||
  (versionArray[0] = =8 && versionArray[1] > =6) ||
  (versionArray[0] = =6 && versionArray[1] > =15) ||
  (versionArray[0] = =6 && versionArray[1] > =14 && versionArray[2] > =2));

varneedsFlag = (! isNodeApiBuiltin && versionArray[0] = =8);
// Current directory
var include = [__dirname];
// For earlier versions of Node, the.gyp file for Dependencies is required
var gyp = path.join(__dirname, 'src'.'node_api.gyp');
// Check if there is an N-api
if (isNodeApiBuiltin) {
  gyp += ':nothing';
} else {
  gyp += ':node-api';
  include.unshift(path.join(__dirname, 'external-napi'));
}

module.exports = {
  include: include.map(function(item) {
    return '"' + item + '"';
  }).join(' '),
  gyp: gyp,
  isNodeApiBuiltin: isNodeApiBuiltin,
  needsFlag: needsFlag
};

Copy the code

Node_api/SRC /node_api.cc node-addon-api/ SRC /node_internals.cc node-addon-api/ SRC /node_internals.cc To do nothing with older versions of Node.js, see node-addon-api/ SRC /node_api.gyp. Then modify cmakelists. TXT to import the node-addon-API header directory, adding the following command.

include_directories(./node_modules/node-addon-api)
Copy the code
  • Again, the filter function: (at least less than “Hello world! Level 😭)
#include <napi.h>
using namespace Napi;
// This is similar to the n-api
Array Filter(const CallbackInfo &info) {
  Env env = info.Env(); // Similar to env in n-api
  Array result = Array::New(env);
  Array arr = info[0].As<Array>(); // Similar to V8
  Function fn = info[1].As<Function>();
  for (size_t i = 0, j = 0; i < arr.Length(); i++) {
    // Function calls can use initializer_list, which is nice
    Boolean ret = fn.Call(arr, {arr.Get(i), Number::New(env, i), arr}).As<Boolean>();
    if(ret.Value()) { result.Set(j++, arr.Get(i)); }}return result;
}

// Init is a function similar to n-api
Object Init(Env env, Object exports) {
  exports.Set("filter", Function::New(env, Filter));
  return exports;
}
// use the NODE_API_MODULE macro
NODE_API_MODULE(addon, Init)
Copy the code

Assuming that the reader has read the v8 and N-API above carefully and has a basic understanding of the V8 and N-API types, I think it is easy to understand the code above, but n-API has been replaced with names similar to v8 types.

Node – addon – API is introduced

As you can see from the filter function above, the node-Addon-API data types and some usage are very similar to V8’s

  • Basic data types

    1. Value: Any type of abstraction (superclass) that encapsulates napi_value. Commonly used API are

      Value.as

      (): data Type conversion, similar to v8’s As method.

      value.Is… (): Data type judgment, such as value.isfunction (), value.isnull (), etc

      value.To… (): Data type conversion, such as value.toBoolean (), value.tonumber (), etc

      Napi_value (value): Converts value back to napi_value. The overloaded function is declared as follows

      operator napi_value(a) const;
      Copy the code

      There are similar overloads in the following types, such as operator STD ::string() const; Function to convert data types from Napi to C++ data types.

    2. Object: Object type, inherited from Value.

      Object::New(): Creates an Object, which is a static function, Object::New(env)

      Obj.set (): Sets attributes for objects, which are overloaded, for example

      Object obj = Object::New(env);
      obj.Set("name"."sundial-dreams");
      obj.Set(21."age");
      obj.Set(String::New(env, "age"), Number::New(env, 21));
      Copy the code

      Keys can be napi_value | for a: : Value | | const char * const STD: : string & | uint32_t type, Value can be napi_value | for a: : Value | const char * | STD: : string & | bool | type double

      Obj.delete (): Deletes the property of the object, obj.delete (“name”), with the same key type as above

      Obj.get (): Gets the property of the object, obj.get (21), with the same key type as above

      Obj.has ()/ obj.hasownProperty (): this is the same as above

      [] operator overloading: that is to say, we can through the obj/” name “=” DPF “way to set or get the value of the attribute, is one of the key type uint32_t | | const char * const STD: : string &

      Obj.getpropertynames (): Returns all enumerable properties of an object, and the return value is an Array type

      Object object = Object::New(env);
      object["name"] = info[0].As<String>();
      object["age"] = info[1].As<Number>();
      Array attrs = object.GetPropertyNames();
      // "name", "age"
      for (size_t i = 0; i < attrs.Length(); i++) {
        std: :cout << std: :string(attrs.Get(i).As<String>()) << std: :endl;
      }
      Copy the code

      obj.DefineProperty()/obj.DefineProperties(): To define the property, obj.defineProperty () takes an argument of type Napi::PropertyDescriptor&, While obj.defineProperties () takes arguments of the STD :: Initializer_list
      <:propertydescriptor>
      or STD :: Vector
      <:propertydescriptor>
      type, The Napi::PropertyDescriptor type is mentioned here, which can be understood as the encapsulation of napi_property_descriptor in N-API.

      Object CreateObject(const CallbackInfo &info) {
        Env env = info.Env();
        Object object = Object::New(env);
        // Define an attribute, key, value, enumerable
        PropertyDescriptor nameProp = PropertyDescriptor::Value("name", info[0], napi_enumerable);
        PropertyDescriptor ageProp = PropertyDescriptor::Value("age", info[1], napi_enumerable);
        // Define a function attribute, key, function, enumerable
        PropertyDescriptor getAllFn = PropertyDescriptor::Function("getAll"[] (const CallbackInfo &args) -> void {
          Object self = args.This().ToObject();
          std: :string name = self.Get("name").As<String>(); Implicit type conversion
          int age = self.Get("age").As<Number>();
          std: :cout<<name<<""<<age<<std: :endl;
        }, napi_enumerable);
        // Pass initializer_list
        object.DefineProperties({ nameProp, ageProp, getAllFn });
        return object;
      }
      Copy the code
    3. The String/Number/Boolean/Array: These types is simple, is to use the New function structure, type String, can use STR. Utf8Value ()/STR. Utf16Value () to obtain a String value (return to STD: : String/STD: : u16string type), Or simply use the display conversion string(STR) to convert to STD :: String.

      String str = String::New(env, "hello world!");
      std: :string str1 = str.Utf8Value();
      std::u16string u16Str = str.Utf16Value();
      std: :string cpp_str = str; // Implicit conversion, String overrides the String () conversion operator
      std: :string cpp_str1 = std: :string(str); // Explicit type conversion
      std: :cout<<str1<<""<<""<<cpp_str<<""<<cpp_str1<<std: :endl;
      Copy the code

      Number and Boolean are the same, so I won’t repeat them here. The last Array is also shown in the filter function.

    4. Env: encapsulates n-API napi_env. You can also convert Napi::Env to NAPi_env by using napi_env(Env)

      Env also provides methods such as env.global () to get Global objects, env.undefined () to get Undefined, and env.null () to get Null.

    5. CallbackInfo: is similar to v8::FunctionCallbackInfo

      , except that it uses the following methods to get information about a JavaScript function

      Info.env (): Gets the Env object

      Info.newtarget (): equivalent to the new.target operation in JavaScript

      Info.isconstructcall (): Whether to call as a constructor (new) function

      Info.length (): The Length of the passed argument

      Info [I]: CallbackInfo overloads the [] operator, so you can get the number of arguments by subscript

      Info.this (): This of the current function

  • Handle scope

    1. HandleScope: Similar to V8 ::HandleScope, declaring a HandleScope simply requires HandleScope Scope (env)

    2. EscapableHandleScope: is similar to V8 ::EscapableHandleScope

    Value ReturnValue(Env env) {
      EscapableHandleScope scope(env);
      Number number = Number::New(env, 222);
      return scope.Escape(number); // Similar to v8::EscapableHandleScope usage
    }
    Copy the code
  • function

The Napi::Function class is used to create JavaScript Function objects, which inherit from Napi::Object. Napi::Function can be created with two classes of C++ functions, Typedef void (*VoidCallback)(const Napi::CallbackInfo& info); And typedef Value (*Callback)(const Napi::CallbackInfo& info); Are functions that return void and Value, respectively

  1. NewFrom:CallbackorVoidCallbackType of C++ functions to create JavaScript functions
Function fn1 = Function::New(env, OneFunc, "oneFunc");
Function fn2 = Function::New(env, TwoFunc);
Copy the code
  1. Call: Calls a function that is overloaded
Value Call(const std: :initializer_list<napi_value>& args) const; // Pass parameters using the initializer list
Value Call(const std: :vector<napi_value>& args) const; // Use vector to pass parameters
Value Call(size_t argc, const napi_value* args) const; // Use array to pass parameters directly
Value Call(napi_value recv, const std: :initializer_list<napi_value>& args) const; // bind this to initialize the list
Value Call(napi_value recv, const std: :vector<napi_value>& args) const; // bind this, vector
Value Call(napi_value recv, size_t argc, const napi_value* args) const; // bind this to an array
Copy the code

For example,

// Use the initializer list
fn1.Call({Number::New(env, 1), String::New(env, "sss")});
fn1.Call(self, {Number::New(env, 2)});
/ / using the vector
std: :vector<napi_value> args = {Number::New(env, 222)};
fn1.Call(args);
fn1.Call(self, args);
// Use an array
napi_value args2[] = {String::New(env, "func"), Number::New(env, 231)};
fn1.Call(2, args2);
fn1.Call(self, 2, args2);
Copy the code
  1. **()** overload: Call a function whose prototype is as follows
Value operator (a)(const std: :initializer_list<napi_value>& args) const;
Copy the code

Call the following

fn1({Number::New(env, 22)});
Copy the code

conclusion

A lot of this article actually borrowed from dead moon’s node.js to a dozen C++ extensions, but since the book was written based on node.js v6, node.js is now updated to v12, Much of the original extension has changed (all the examples in this article are based on Node.js V12). This paper also introduces some uses of node.js C++ plug-in and the basic principle of node.js C++ plug-in, and then summarizes the development of node.js C++ plug-in in four ways: We wrote our node.js plug-in based on Nan, n-api, and node-addon-api, from which we can see some evolution of node.js C++ plug-in development. For each of the development methods in the article is only a brief introduction to some API and usage, I hope these content can let the reader have a basic introduction to C++ plug-in, more content please refer to the official documentation. I think the barrier to learning C++ plugin development is not that the apis are hard to use, but that they may not be C++😂😂 (I recommend reading C++ Primer Plus three times).

Making the address

Addresses for all examples of this article: github.com/sundial-dre…

reference

  • Node.js to a dozen C++ extensions

  • V8 documentation: v8.dev/docs

  • Node.js documentation: nodejs.org/dist/latest…

  • Nan official documentation: github.com/nodejs/nan

  • N-api Official document: nodejs.org/dist/latest…

  • Node-addon-api official documentation: github.com/nodejs/node…