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++層定義的內容。