您的位置 首页 > 腾讯云社区

理解nodejs插件的加载原理并使用n-api编写你的第一个nodejs插件---theanarkh

nodejs拓展本质是一个动态链接库,写完编译后,生成一个.node文件。我们在nodejs里直接require使用,nodejs会为我们处理这一切。下面我们按照文档写一个拓展并通过nodejs14源码了解他的原理(ubuntu18.4)。 首先建立一个test.cc文件

// hello.cc using N-API #include <node_api.h> namespace demo { napi_value Method(napi_env env, napi_callback_info args) { napi_value greeting; napi_status status; status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting); if (status != napi_ok) return nullptr; return greeting; } napi_value init(napi_env env, napi_value exports) { napi_status status; napi_value fn; status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn); if (status != napi_ok) return nullptr; status = napi_set_named_property(env, exports, "hello", fn); if (status != napi_ok) return nullptr; return exports; } NAPI_MODULE(NODE_GYP_MODULE_NAME, init) } // namespace demo

我们不需要具体了解代码的意思,但是从代码中我们大致知道他做了什么事情。剩下的就是阅读n-api的api文档就可以。接着我们新建一个binding.gyp文件。gyp文件是node-gyp的配置文件。node-gyp可以帮助我们针对不同平台生产不同的编译配置文件。比如linux下的makefile。

{ "targets": [ { "target_name": "test", "sources": [ "./test.cc" ] } ] }

语法和makefile有点像,就是定义我们编译后的目前文件名,依赖哪些源文件。然后我们安装node-gyp。

npm install node-gyp -g

nodejs源码中也有一个node-gyp,他是帮助npm安装拓展模块时,就地编译用的。我们安装的node-gyp是帮助我们生成配置文件并编译用的,具体可以参考nodejs文档。一切准备就绪。我们开始编译。直接执行

node-gyp rebuild

在路径./build/Release/下生成了test.node文件。这就是我们的拓展模块。我们编写测试程序。

var addon = require("./build/Release/test"); console.log(addon.hello());

执行

nodejs app.js

我们看到输出world。我们已经学会了如何编写一个nodejs的拓展模块。剩下的就是阅读n-api文档,根据自己的需求编写不同的模块。 写完了一个拓展模块,当然要去分析他的机制。一切的源头在于require函数。但是我们不必从这开始分析,我们只需要从加载.node模块的源码开始。

Module._extensions['.node'] = function(module, filename) { // ... return process.dlopen(module, path.toNamespacedPath(filename)); };

直接调了process.dlopen,该函数在node.js里定义。

const rawMethods = internalBinding('process_methods'); process.dlopen = rawMethods.dlopen;

找到process_methods模块对应的是node_process_methods.cc。

env->SetMethod(target, "dlopen", binding::DLOpen);

之前说过,node的拓展模块其实是动态链接库,那么我们先看看一个动态链接库我们是如何使用的。以下是示例代码。

#include <stdio.h> #include <stdlib.h> #include <dlfcn.h> int main(){ // 打开一个动态链接库,拿到一个handler handler = dlopen('xxx.so',RTLD_LAZY); // 取出动态链接库里的函数add add = dlsym(handler,"add"); // 执行 printf("%d",add (1,1)); dlclose(handler); return 0; }

了解动态链接库的使用,我们继续分析刚才看到的DLOpen函数。

void DLOpen(const FunctionCallbackInfo<Value>& args) { int32_t flags = DLib::kDefaultFlags; node::Utf8Value filename(env->isolate(), args[1]); // Cast env->TryLoadAddon(*filename, flags, [&](DLib* dlib) { const bool is_opened = dlib->Open(); node_module* mp = thread_local_modpending; thread_local_modpending = nullptr; // 省略部分代码 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); } return true; }); }

我们看到重点是TryLoadAddon函数,该函数的逻辑就是执行他的第三个参数。我们发现第三个参数是一个函数,入参是DLib对象。所以我们先看看这个类。

class DLib { public: static const int kDefaultFlags = RTLD_LAZY; DLib(const char* filename, int flags); bool Open(); void Close(); const std::string filename_; const int flags_; std::string errmsg_; void* handle_; uv_lib_t lib_; };

再看一下实现。

bool DLib::Open() { handle_ = dlopen(filename_.c_str(), flags_); if (handle_ != nullptr) return true; errmsg_ = dlerror(); return false; }

DLib就是对动态链接库的一个封装,他封装了动态链接库的文件名和操作。TryLoadAddon函数首先根据require传入的文件名,构造一个DLib,然后执行

const bool is_opened = dlib->Open();

Open函数打开了一个动态链接库,这时候我们要先了解一下打开一个动态链接库究竟发生了什么。首先我们看一个napi动态链接库的定义。我们回来文章开头的测试代码test.cc。最后一句是

NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

这是个宏定义。

#define NAPI_MODULE(modname, regfunc) NAPI_MODULE_X(modname, regfunc, NULL, 0)

继续展开

#define NAPI_MODULE_X(modname, regfunc, priv, flags) static napi_module _module = { NAPI_MODULE_VERSION, flags, __FILE__, regfunc, #modname, priv, {0}, }; static void _register_modname(void) __attribute__((constructor)); static void _register_modname(void) { napi_module_register(&_module); }

所以一个node扩展就是定义了一个napi_module 模块和一个register_modname(modname是我们定义的)函数。我们貌似定义了两个函数,其实一个带attribute_((constructor))。__attribute((constructor))是代表该函数会先执行的意思,具体可以查阅文档。看到这里我们知道,当我们打开一个动态链接库的时候,会执行_register_modname函数,该函数执行的是

napi_module_register(&_module);

我们继续展开。

// Registers a NAPI module. void napi_module_register(napi_module* mod) { node::node_module* nm = new node::node_module { -1, mod->nm_flags | NM_F_DELETEME, nullptr, mod->nm_filename, nullptr, napi_module_register_cb, mod->nm_modname, mod, // priv nullptr, }; node::node_module_register(nm); }

nodejs把napi模块转成node_module。最后调用node_module_register。

extern "C" void node_module_register(void* m) { struct node_module* mp = reinterpret_cast<struct node_module*>(m); if (mp->nm_flags & NM_F_INTERNAL) { mp->nm_link = modlist_internal; modlist_internal = mp; } else if (!node_is_initialized) { mp->nm_flags = NM_F_LINKED; mp->nm_link = modlist_linked; modlist_linked = mp; } else { thread_local_modpending = mp; } }

napi模块不是NM_F_INTERNAL模块,node_is_initialized是在nodejs初始化时设置的变量,这时候已经是true。所以注册napi模块时,会执行thread_local_modpending = mp。thread_local_modpending 类似一个全局变量,保存当前加载的模块。分析到这,我们回到DLOpen函数。

node_module* mp = thread_local_modpending; thread_local_modpending = nullptr;

这时候我们就知道刚才那个变量thread_local_modpending的作用了。node_module* mp = thread_local_modpending后我们拿到了我们刚才定义的napi模块的信息。接着执行node_module的函数nm_register_func。

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); }

从刚才的node_module定义中我们看到函数是napi_module_register_cb。

static void napi_module_register_cb(v8::Local<v8::Object> exports, v8::Local<v8::Value> module, v8::Local<v8::Context> context, void* priv) { napi_module_register_by_symbol(exports, module, context, static_cast<napi_module*>(priv)->nm_register_func); }

该函数调用napi_module_register_by_symbol函数,并传入napi_module的nm_register_func函数,即我们test.cc代码里定义的函数。

void napi_module_register_by_symbol(v8::Local<v8::Object> exports, v8::Local<v8::Value> module, v8::Local<v8::Context> context, napi_addon_register_func init) { // Create a new napi_env for this specific module. napi_env env = v8impl::NewEnv(context); napi_value _exports; env->CallIntoModuleThrow([&](napi_env env) { _exports = init(env, v8impl::JsValueFromV8LocalValue(exports)); }); if (_exports != nullptr && _exports != v8impl::JsValueFromV8LocalValue(exports)) { napi_value _module = v8impl::JsValueFromV8LocalValue(module); napi_set_named_property(env, _module, "exports", _exports); } }

init就是我们在test.cc里定义的函数。入参是env和exports,可以对比我们定义的函数的入参。最后我们修改exports变量。即设置导出的内容。最后在js里,我们就拿到了c++层定义的内容。

---来自腾讯云社区的---theanarkh

关于作者: 瞎采新闻

这里可以显示个人介绍!这里可以显示个人介绍!

热门文章

留言与评论(共有 0 条评论)
   
验证码: