理解nodejs插件的加載原理並使用n-api編寫你的第一個nodejs插件

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章