Table of Contents

    1. preface
    1. A simple introduction
    1. example
    • 3.1. build
      • 3.1.1. Download the node – gyp
      • 3.1.2. Add binding. Gyp file
      • 3.1.3. Run nod-gyp configure
      • 3.1.4. Run the nod-gyp build command
    • 3.2. Use
    1. implementation
    • 4.1. Dynamic link
      • Dynamic in 4.1.1.js
      • 4.1.2. Static and dynamic linking of C language
    • 4.2. DLOpen
    • 4.3. Env – > TryLoadAddon
    • 4.4. Dlib – > Open
    • 4.5. Plug-in registration
      • 4.5.1. NODE_C_CTOR
    • 4.6. If you don’t register
      • 4.6.1. GetInitializerCallback
      • 4.6.2. GetNapiInitializerCallback
    1. Excellent plug-in recommendations
    1. summary

1. preface

Node consists of c++ modules, js modules in the lib directory, users’ js modules, and users’ c++ plug-in modules. The mechanisms for registering, running, and loading are different. Let’s go through each of them

Knowledge points involved

2. A simple introduction

Plug-ins are dynamically linked shared objects written in C++. The require() function loads the plug-in as a normal Node.js module. The plug-in provides an interface between JavaScript and the C/C++ library.

There are three options for implementing plug-ins: Nod-API, nan, or directly using the internal V8, Libuv, and Node.js libraries. Use Node-api unless you need direct access to functionality not exposed by Node-API. For more information about Node-API, see C/C++ Plug-ins that use Node-API.

In fact, both node-api and node-adon-api are the most basic c++ plug-in writing encapsulation, let’s take a look at the basic c++ plug-in implementation

3. example

  • The example code is in the Addons directory of the Git repository
  • The runtime node version is 16.5.0
  • Running system: MacOS

}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} So that users can find the module when they require it

// hello.cc

#include <node.h>

namespace demo
{

  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Object;
  using v8::String;
  using v8::Value;

  void Method(const FunctionCallbackInfo<Value> &args)
  {
    Isolate *isolate = args.GetIsolate();
    args.GetReturnValue().Set(String::NewFromUtf8(
                                  isolate, "world")
                                  .ToLocalChecked());
  }

  void Initialize(Local<Object> exports)
  {
    NODE_SET_METHOD(exports, "hello", Method);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}
Copy the code

3.1. build

Building to run hello.cc requires the following steps

3.1.1. Download the node – gyp

npm install -g node-gyp
Copy the code

Nod-gyp is a cross-platform command line tool written in Node.js to compile native plug-in modules for Node.js. It contains a vendor copy of the Gyp-Next project that the Chromium team previously used, extended to support the development of native node.js plug-ins.

3.1.2. Add the binding. Gyp file

The file content is as follows

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "hello.cc" ]
    }
  ]
}
Copy the code

After you write the source code, you must compile it into a binary addon.node file. To do this, create a file called binding.gyp at the top level of the project that describes the build configuration of the module in a JSON-like format. This file is used by Node-gyp, a tool written specifically for compiling node.js plug-ins.

3.1.3. Run node-gyp configure

node-gyp configure
Copy the code

After creating the bind.gyp file, use Node-gyp configure to generate the appropriate project build file for the current platform. This will generate either a Makefile (on Unix platforms) or a vCXproj file (on Windows) in the build/ directory.

3.1.4. Run node-gyp build

node-gyp build
Copy the code

Next, call the nod-gyp build command to generate the compiled adon.node file. This will go into the build/Release/ directory.

3.2. use

Once the build is complete, you can use the binary plug-in in Node.js by referring require() to the addon.node module you are building

// main.js const addon = require('./build/Release/addon'); console.log(addon.hello()); // Print: 'world'Copy the code

4. implementation

This is what happens when code requires a compiled c++ plug-in. Node file

  • The main implementation is the process.dlopen function
  • The policy at the beginning of the code for node.js includes experimental support for creating policies that load the code, such as running Node –experimental-policy= policy-.json app.js, The specific use can refer to the policy | Node. Js API documentation
// lib/internal/modules/cjs/loader.js Module._extensions['.node'] = function(module, filename) { if (policy? .manifest) { const content = fs.readFileSync(filename); const moduleURL = pathToFileURL(filename); policy.manifest.assertIntegrity(moduleURL, content); } // Be aware this doesn't use `content` return process.dlopen(module, path.toNamespacedPath(filename)); };Copy the code

4.1. Dynamic link

Dlopen is used to dynamically open a c++ plug-in file and run its code. Before we talk about dlopen, let’s talk about dynamic linking.

4.1.1. Js

If you take js as an example, you can run a JS file directly by referring to the script to the browser without any additional intervention. For example, if you want to dynamically load and run a js file, you need to wrap a dynamicImport method around it and wait until it is actually called. This dynamicImport program might be implemented by document.createElement(‘script’)

4.1.2. C language static link and dynamic link

Content from the C language static links and dynamic links

What is a link?

For beginners C language friends, may be a little strange to the concept of links, here is a brief introduction. Compiling our C code to generate an executable goes through the following process:

1. What is static linking?

Static linking is the practice of adding the contents of a library to an executable program at link time by the linker. A linker is a stand-alone program that links one or more library or object files (previously generated by a compiler or assembler) into a block to generate an executable program. Libraries are static linked libraries, with.lib for Windows and.a for Linux.

2. What is dynamic linking?

Dynamic Linking, in which the Linking process is deferred to run time, is loaded by the operating system loader either at executable load time or at run time. The library here refers to the dynamic link library, Windows with.dll suffix, Linux with.so suffix. It is worth mentioning that dynamic linking in Windows also uses.lib files, but here the.lib files are called import libraries and are generated from.dll files.

4.2. DLOpen

The implementation process of DLOpen consists of the following steps, and we will analyze the important steps in detail later

  1. Did some validation on the passed parameters, saved the Module object passed by JS, and checked that the exports attribute was required
  2. Call env->TryLoadAddon to try to load the plug-in. TryLoadAddon mainly calls the passed callback function
  3. Mutex::ScopedLock lock(dlib_load_mutex);
  4. Then call the dlib->Open() method to get the handle to the dynamic link library file
  5. As soon as the dlib->Open() of the previous step is run, the c++ plug-in code is actually finished. Recalling the c++ plug-in code from our example above, its NODE_MODULE macro registers the module into memory
  6. if (mp ! = nullptr) to determine whether NODE_MODULE has been actively registered, so that it can be automatically registered
  7. If registration is successful, call dlib->SaveInGlobalHandleMap to save the handle returned by dlib->Open() into memory
  8. Otherwise, use the auto Callback = GetInitializerCallback(dlib) return value to see if you can automatically help the user register
  9. Dlib ->Open() The opening process is finished. Unlock it by Mutex::ScopedUnlock (lock)
  10. The mp->nm_register_func call is the Initialize function passed in by the user in the c++ plug-in example above
// src/node_binding.cc void DLOpen(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); auto context = env->context(); CHECK_NULL(thread_local_modpending); if (args.Length() < 2) { return THROW_ERR_MISSING_ARGS( env, "process.dlopen needs at least 2 arguments"); } int32_t flags = DLib::kDefaultFlags; if (args.Length() > 2 && ! args[2]->Int32Value(context).To(&flags)) { return THROW_ERR_INVALID_ARG_TYPE(env, "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; // Exception pending. } node::Utf8Value filename(env->isolate(), args[1]); // Cast env->TryLoadAddon(*filename, flags, [&](DLib* dlib) { static Mutex dlib_load_mutex; Mutex::ScopedLock lock(dlib_load_mutex); const bool is_opened = dlib->Open(); // Objects containing v14 or later modules will have registered themselves // on the pending list. Activate all of them now. At present, only one // module per object is supported. node_module* mp = thread_local_modpending; thread_local_modpending = nullptr; if (! is_opened) { std::string errmsg = dlib->errmsg_.c_str(); dlib->Close(); #ifdef _WIN32 // Windows needs to add the filename into the error message errmsg += *filename; #endif // _WIN32 THROW_ERR_DLOPEN_FAILED(env, errmsg.c_str()); return false; } if (mp ! = nullptr) { if (mp->nm_context_register_func == nullptr) { if (env->force_context_aware()) { dlib->Close(); THROW_ERR_NON_CONTEXT_AWARE_DISABLED(env); return false; } } mp->nm_dso_handle = dlib->handle_; dlib->SaveInGlobalHandleMap(mp); } else { if (auto callback = GetInitializerCallback(dlib)) { callback(exports, module, context); return true; } else if (auto napi_callback = GetNapiInitializerCallback(dlib)) { 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(); char errmsg[1024]; snprintf(errmsg, sizeof(errmsg), "Module did not self-register: '%s'.", *filename); THROW_ERR_DLOPEN_FAILED(env, errmsg); return false; } } } // -1 is used for N-API modules if ((mp->nm_version ! = -1) && (mp->nm_version ! = NODE_MODULE_VERSION)) { // Even if the module did self-register, it may have done so with the // wrong version. We must only give up after having checked to see if it // has an appropriate initializer callback. if (auto callback = GetInitializerCallback(dlib)) { callback(exports, module, context); return true; } char errmsg[1024]; snprintf(errmsg, sizeof(errmsg), "The module '%s'" "\nwas compiled against a different Node.js version using" "\nNODE_MODULE_VERSION %d. This version of Node.js requires" "\nNODE_MODULE_VERSION %d. Please try re-compiling or " "re-installing\nthe module (for instance, using `npm rebuild` " "or `npm install`).", *filename, mp->nm_version, NODE_MODULE_VERSION); // NOTE: `mp` is allocated inside of the shared library's memory, calling // `dlclose` will deallocate it dlib->Close(); THROW_ERR_DLOPEN_FAILED(env, errmsg); 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(); THROW_ERR_DLOPEN_FAILED(env, "Module has no declared entry point."); return false; } return true; }); // Tell coverity that 'handle' should not be freed when we return. // coverity[leaked_storage] }Copy the code

4.3. env->TryLoadAddon

  1. Loaded_addons_. Emplace_back adds an element to the end of the loaded_addons_ queue
  2. Call the passed callback function was_loaded
  3. If WAS_loaded fails loaded_addons_. Pop_back removes an element from the tail
// src/env-inl.h

inline void Environment::TryLoadAddon(
    const char* filename,
    int flags,
    const std::function<bool(binding::DLib*)>& was_loaded) {
  loaded_addons_.emplace_back(filename, flags);
  if (!was_loaded(&loaded_addons_.back())) {
    loaded_addons_.pop_back();
  }
}
Copy the code

4.4. dlib->Open

  1. Dlopen opens the specified dynamic link library file in the specified mode and returns a handle to the calling process
  2. Dlerror Obtain errors that may occur
bool DLib::Open() { handle_ = dlopen(filename_.c_str(), flags_); if (handle_ ! = nullptr) return true; errmsg_ = dlerror(); return false; }Copy the code

4.5. Plug-in registration

After running the dlib->Open function above, the user’s code is loaded and running, just as in the c++ plug-in example above, calling the NODE_MODULE macro is mainly used to register the module

The node_module_register function inserts data into the linked list modlist_Internal, but the NODE_C_CTOR macro allows us to learn a lot

// src/node.h

#define NODE_MODULE(modname, regfunc)                                 \
  NODE_MODULE_X(modname, regfunc, NULL, 0)  // NOLINT (readability/null_usage)
  
#define NODE_MODULE_X(modname, regfunc, priv, flags)                  \
  extern "C" {                                                        \
    static node::node_module _module =                                \
    {                                                                 \
      NODE_MODULE_VERSION,                                            \
      flags,                                                          \
      NULL,  /* NOLINT (readability/null_usage) */                    \
      __FILE__,                                                       \
      (node::addon_register_func) (regfunc),                          \
      NULL,  /* NOLINT (readability/null_usage) */                    \
      NODE_STRINGIFY(modname),                                        \
      priv,                                                           \
      NULL   /* NOLINT (readability/null_usage) */                    \
    };                                                                \
    NODE_C_CTOR(_register_ ## modname) {                              \
      node_module_register(&_module);                                 \
    }                                                                 \
  }
  
#define NODE_C_CTOR(fn)                                               \
  NODE_CTOR_PREFIX void fn(void) __attribute__((constructor));        \
  NODE_CTOR_PREFIX void fn(void)
  
# define NODE_CTOR_PREFIX static
Copy the code

4.5.1. NODE_C_CTOR

The implementation of the NODE_C_CTOR macro is a bit nice. The main thing is to declare a dynamic function register ## modname and immediately call register ## modname. This is similar to the example below, which is located in the Git repository demo_node_c_ctor.cpp

The register function is declared via the NODE_C_CTOR macro and is made to run before the main function via attribute((constructor))

#include <iostream> # define NODE_CTOR_PREFIX static #define NODE_C_CTOR(fn) \ NODE_CTOR_PREFIX void fn(void) __attribute__((constructor)); \ NODE_CTOR_PREFIX void fn(void) using namespace std; NODE_C_CTOR(_register_) { \ std::cout << "before main function" << std::endl; \ } int main() { char site[] = "Hello, world!" ; cout << site << endl; return 0; }Copy the code

4.6. If you don’t register

4.6.1. GetInitializerCallback

If the user does not call the NODE_MODULE macro register, the code logic of auto Callback = GetInitializerCallback(dlib) is found

  • The following code “node_register_module_v” STRINGIFY(NODE_MODULE_VERSION) in C is equivalent to concatenation of two strings, if NODE_MODULE_VERSION is 95, The variable name equals node_register_module_v95
  • The dlsym function is equivalent to getting the address reference of the specified name in the c++ plug-in, that is, getting the function name or variable name through the handle and the connection name.
// src/node_binding.cc

inline InitializerCallback GetInitializerCallback(DLib* dlib) {
  const char* name = "node_register_module_v" STRINGIFY(NODE_MODULE_VERSION);
  return reinterpret_cast<InitializerCallback>(dlib->GetSymbolAddress(name));
}

void* DLib::GetSymbolAddress(const char* name) {
  return dlsym(handle_, name);
}
Copy the code

So if the user does not display the call to the NODE_MODULE macro register, the user will check to see if the node_register_module_v95 function is available in the c++ plug-in. If so, the user will actively call the function, and then find the node implementation commit 3828fc62

If you don’t need to register with the NODE_MODULE macro, you can do so by using the following example in the Git repository demo_node_module_initializer.cc

#include <node.h> #include <v8.h> static void Method(const v8::FunctionCallbackInfo<v8::Value>& args) { v8::Isolate* isolate = args.GetIsolate(); args.GetReturnValue().Set(v8::String::NewFromUtf8( isolate, "world").ToLocalChecked()); } // NODE_MODULE_EXPORT is equivalent to __attribute__((visibility("default"))) // NODE_MODULE_INITIALIZER is equivalent to the NODE_MODULE_INITIALIZER macro "node_register_module_v" STRINGIFY(NODE_MODULE_VERSION) extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(v8::Local<v8::Object> exports, v8::Local<v8::Value> module, v8::Local<v8::Context> context) { NODE_SET_METHOD(exports, "hello", Method); }Copy the code

The reason for this is that the NODE_MODULE macro is not registered the second time because dlopen will no longer run the c++ plugin code after running it several times, similar to node’s require mechanism. By NODE_MODULE_INITIALIZER declare a function, behind every time by dlsym get to take the initiative to call this function reference address once the function is good, here is a website | plug-in explanation context awareness

In some environments, it may be necessary to load the Node.js plug-in multiple times in multiple contexts. For example, the Electron runtime runs multiple Instances of Node.js in a single process. Each instance has its own require() cache, so each instance needs the native plug-in to run properly when loaded via require(). This means that the plug-in must support multiple initializations.

Context-aware plug-ins can be built using the macro NODE_MODULE_INITIALIZER, which is extended to the name of the function that Node.js expects to find when loading the plug-in.

4.6.2. GetNapiInitializerCallback

After GetInitializerCallback in DLOpen function has another branching logic GetNapiInitializerCallback function call, actually the role of the two functions is similar, the function name here should be left for a hole

// src/node_binding.cc

inline napi_addon_register_func GetNapiInitializerCallback(DLib* dlib) {
  const char* name =
      STRINGIFY(NAPI_MODULE_INITIALIZER_BASE) STRINGIFY(NAPI_MODULE_VERSION);
  return reinterpret_cast<napi_addon_register_func>(
      dlib->GetSymbolAddress(name));
}
Copy the code

Example of nAPI plug-in code in the Git repository demo_napi_module_init.cc

#include <assert.h> #include <node_api.h> #include <node_api.h> static napi_value Hello(napi_env env, napi_callback_info info) { napi_value result; napi_status status = napi_create_int32(env, increment++, &result); assert(status == napi_ok); return result; } NAPI_MODULE_INIT() { napi_value hello; napi_status status = napi_create_function(env, "hello", NAPI_AUTO_LENGTH, Hello, NULL, &hello); assert(status == napi_ok); status = napi_set_named_property(env, exports, "hello", hello); assert(status == napi_ok); return exports; }Copy the code

5. Excellent plug-in recommendations

The cause is a FaaS interface written by myself, the current QPS is about 30, the system memory is found on the monitoring is slowly rising with time 📈, the QPS of this interface is expected to reach about 1000, now there is a memory leak can be quickly checked out!

My initial idea was to create an interface with get_v8_heapSnapshot in the URL to return a snapshot of the current process v8 heap, and then pull it several times to analyze it

Later, it is found that the automatic access to easy-Monitor can meet this requirement very quickly, you can directly click On DevTools online analysis or download the local analysis snapshot

After comparing several snapshots at different times, there is no significant change, and there is no significant fluctuation in the heap space trend chart. It is confirmed that other processes on the physical machine are caused by this is expected, and our node process has no memory leak

The implementation of this c++ plugin is in the x-profiler /xprofiler repository. For more information, see easy-monitor 3.0, an open source, Addon based performance monitoring solution for node. js

6. summary

The implementation of running a c++ plug-in in node is primarily in the DLOpen function, which essentially calls the following functions

Function introduction from the article using DLOPEN, DLSYM, DLCLOSE load dynamic link library

Void *dlopen(const char *filename, int flag); void *dlopen(const char *filename, int flag) // dlError returns the error occurred char * dlError (void); Void *dlsym(void *handle, const char *symbol); Int dlclose(void *handle);Copy the code

Read more: github.com/xiaoxiaojx/… If you are interested, you can click “Star” for support. Thank you for reading ~