Node中完整的?node?addon?實現(xiàn)流程
背景介紹
為什么要寫 node addon
試想這樣一種場景:我們想在 js 層實現(xiàn)某個業(yè)務場景,但是這套業(yè)務邏輯已經(jīng)有存在的 C++ 版本了,這個時候我們有兩個選擇
- 重新實現(xiàn)一套在 JS 版本的業(yè)務場景
- 使用
node addon橋接C++版本代碼
對比以上兩種方案,顯然,使用 addon 不用去寫過重的業(yè)務邏輯,是一種成本更低的方案
node addon 是什么
node addon,即為node插件 / 擴展,插件是用C++編寫的動態(tài)鏈接共享對象。- 動態(tài)鏈接共享對象,即動態(tài)鏈接庫
- 鏈接庫:庫文件的二進制版本,即將庫文件進行編譯、打包操作后得到二進制文件,無法獨立運行,必須等待其他程序調用才會被載入內存中。
- 靜態(tài)鏈接:無論缺失的地址位于其他目標文件還是鏈接庫,鏈接庫都會逐個找到各目標文件中缺失的地址。采用此鏈接方式生成的可執(zhí)行文件,可以獨立載入內存運行。
- 動態(tài)鏈接:鏈接器先從所有目標文件中找到部分缺失的地址,然后將所有目標文件組織成一個可執(zhí)行文件。這樣生成的可執(zhí)行文件,仍缺失部分函數(shù)和變量地址,待文件執(zhí)行時,需連同所有的鏈接庫文件一起載入內存,再由鏈接器完成剩余的地址修復工作,才能正常執(zhí)行
- 靜態(tài)鏈接庫:在生成可執(zhí)行文件之前完成所有鏈接操作,使用的庫文件是靜態(tài)鏈接庫,后綴名 .a .lib
- 動態(tài)鏈接庫:將部分鏈接操作推遲到程序執(zhí)行時才進行,使用的庫文件是動態(tài)鏈接庫,后綴名:.so .dll .dylib
- 插件提供了
JavaScript和C/C++庫之間的接口。 require函數(shù)可以將插件加載為普通的Node.js模塊。- 通俗點來講,是一個能夠橋接
c++和js的中間轉換層
可通過 NODE-API、NAN、或者使用底層 v8 庫來實現(xiàn)【官方建議使用 NODE-API】
node-api:構建原生插件的 api,獨立于 JS 運行時,此 API 是跨 Node.js 版本穩(wěn)定的應用程序二進制接口,它旨在將插件與底層 JavaScript 引擎中的更改隔離開來,并允許為一個主要版本編譯的模塊nan(Native Abstractions for Node.js):是一個Node.js原生模塊抽象接口集。它提供了一套API- 底層
V8:就是我們熟悉的Chrome V8
addon 實現(xiàn)方式的變遷
Chrome V8 API
1、是啥:即使用 Node 自身各種 API 以及 Chrome V8 的 API
2、存在的問題
這些寫好的代碼只能在特定的 Node 版本下編譯,因為其中各種 API、函數(shù)聲明等的變化會很大,舉個例子
Handle<Value> Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo<value>& args); // 6.x
NAN 時代
1、是啥:
Native Abstractions for Node.js,即Node.js原生模塊抽象接口集- 代碼只需要隨著
NAN的升級做改變,它會幫我們兼容各個版本

2、存在的問題
- 一次寫好的代碼在不用版本的
Node.js下也需要重新編譯,如果版本不符合,Node.js就無法正常載入一個C++擴展- NAN 的封裝方式是使用了一堆宏,在不同的 Node 版本下使用不同的宏變量、函數(shù)等,所以針對用戶使用不同的 Node 版本,是需要進行重新編譯的
符合 ABI 的 N-API
1、是啥
- 自從 2017 年 Node.js v8.0.0 發(fā)布之后,Node.js 推出了全新的用于開發(fā) C++ 原生模塊的接口-> N-API
- 與 NAN 相比,它把 Node.js 的所有底層數(shù)據(jù)結構全部黑盒化,抽象成 N-API 中的接口;不同版本的 Node.js 使用同樣的接口,這些接口穩(wěn)定且 ABI 化。只要 ABI 版本號一致,編譯好的 C++ 擴展就可以直接使用,而不需要重新編譯
- ABI 化:(Application Binary Interface)應用程序二進制接口;可以理解為一種約定,是 API 的編譯版本;ABI 允許編譯好的目標代碼在使用兼容 ABI 的系統(tǒng)中無需改動就能運行;一套完整的 ABI 可以讓程序在所有支持該 ABI 的系統(tǒng)上運行,無需對程序進行修改
- API:(Application programming interface),應用編程接口
- 主要收益:消除了 Nodejs 版本之間的差異
2、N-API 的使用姿勢
- 提供頭文件 node_api.h
- 使用文檔:nodejs.org/api/n-api.h…
3、node-addon-api 是啥?
- 可以理解為是對 N-API 更進一步的封裝,更加便于我們開發(fā)
- 舉個例子
// N-API
#include <assert.h>
#include <node_api.h>
static napi_value Method(napi_env env, napi_callback_info info) {
napi_status status;
napi_value world;
status = napi_create_string_utf8(env, "world", 5, &world);
assert(status == napi_ok);
return world;
}
#define DECLARE_NAPI_METHOD(name, func) \
{ name, 0, func, 0, 0, 0, napi_default, 0 }
static napi_value Init(napi_env env, napi_value exports) {
napi_status status;
napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
status = napi_define_properties(env, exports, 1, &desc);
assert(status == napi_ok);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
// node-addon-api
#include <napi.h>
Napi::String Method(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "world");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "hello"),
Napi::Function::New(env, Method));
return exports;
}
NODE_API_MODULE(hello, Init) 編碼階段
如何寫出正確的 addon 邏輯
- demo.h

demo.cc

1、熟悉 C++ 基礎語法
宏的定義:#define 是定義一個宏的指令(預編譯指令),它用來將一個標識符定義為一個字符串,該標識符被稱為宏,被定義的字符串被稱為替換文本,
- 簡單的宏定義和帶參數(shù)的宏定義
- 當宏出現(xiàn)在一個文件中時,在該文件后續(xù)出現(xiàn)的所有宏都將被替換為 《替換文本》
- 常用于條件編譯情況下,比如版本號的不同而編譯不同的邏輯
// 簡單的宏定義 #define PI 1415926 // 宏名 字符串 // 帶參數(shù)的宏定義 #define A(x) x // 宏名(參數(shù)表) 宏體
- 類
- 公有繼承、私有繼承、保護繼承
- 公有繼承:繼承父類 public 和 protected 的方法和變量,不能訪問 private
- 私有繼承:繼承父類的 public 和 protected 的方法和變量作為私有成員,不能被該類的子類再次訪問
- 保護繼承:繼承父類 public 和 protected 成員作為保護成員
- 公有、私有、受保護的成員
- public:可被子類繼承或在類外內訪問
- private:僅限類內部使用,不可被繼承和訪問
- protected: 可被子類繼承,但不能在類外訪問
- 公有繼承、私有繼承、保護繼承
// test.h
class Test : public B { // private || protected
public:
private:
protected:
int pro = 1;
}
// 類外
#include "test.h"
Test test; // 實例化 Test 類
std::cout << test.pro << std::endl; // error -> 不可在類外被訪問- 構造函數(shù)和析構函數(shù)
- 構造函數(shù):類的構造函數(shù)是類的一種特殊的成員函數(shù),它會在每次創(chuàng)建類的新對象時執(zhí)行。類似于 JS 中的 constructor; 可自己實現(xiàn),也可使用編譯器生成的默認構造函數(shù),即與類名相同的函數(shù);
- 析構函數(shù):類的析構函數(shù)是類的一種特殊的成員函數(shù),它會在每次刪除所創(chuàng)建的對象時執(zhí)行。析構函數(shù)的名稱與類的名稱是完全相同的,只是在前面加了個波浪號(~)作為前綴,它不會返回任何值,也不能帶有任何參數(shù)。析構函數(shù)有助于在跳出程序(比如關閉文件、釋放內存等)前釋放資源。
- 虛函數(shù)、純虛函數(shù)是啥
- virtual:虛函數(shù)關鍵字,子類可選擇自己實現(xiàn)或使用父類原有方法
- = 0:純虛函數(shù)關鍵字,子類必須自己實現(xiàn),如果不實現(xiàn),編譯階段將會報錯
- override:是一個覆蓋虛函數(shù)的標識符
2、熟悉 addon 語法
1、如何讓 js require?無后綴情況下的 .js -> .json -> .node
- 在 js 中,使用 commonJs 語法即可讓該模塊被其他模塊 require,addon 中則也是類似的想法
- 在 addon 中,提供了 NODE_API_MODULE 宏方法,用這個方法即可實現(xiàn)外部 require 效果,方法接收兩個參數(shù),即模塊名字和導出的方法
- 具體實現(xiàn)?
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
return Link::Init(env, exports);
}
NODE_API_MODULE(link, InitAll);2、定義一個類以及注冊方法
- 在 js 中的效果即為 class A { //.... }
Napi::Object Link::Init(Napi::Env env, Napi::Object exports) {
Napi::Function func =
DefineClass(
env, "Demo",
{
InstanceMethod("add", &Demo::Add),
}
);
auto constructor = Napi::Persistent(func);
constructor.SuppressDestruct();
exports.Set("Demo", func);
return exports;
}3、函數(shù)的接收參數(shù)
- 在 addon 中
- 接收多個參數(shù):
- 定義好每個參數(shù)的類型接收
- 統(tǒng)一在 CallbackInfo 中接收:github.com/nodejs/node…
- 接收一個對象
- 也是從 info[0] 中去拿到這個對象,然后用 object.Get(key) 方法拿到對應的參數(shù)
// 1、定義好參數(shù)接收
Napi::Object Link::Init(Napi::Env env, Napi::Object exports) {}
// 2、在 CallbackInfo 中接收
Napi::Value Link::TagSync(const Napi::CallbackInfo &info) {
string bizId = info[0].As<Napi::String>();
auto tags = info[1].As<Napi::Array>();
}ApplicationInfo applicationInfo = ParseValueAsApplicationInfo(info[0]);
kwai::link::ApplicationInfo ParseValueAsApplicationInfo(Napi::Value value) {
kwai::link::ApplicationInfo applicationInfo;
auto object = value.As<Napi::Object>();
applicationInfo.app_id = GetObjectValueAsInt32(object, "appId");
return applicationInfo;
}
int32_t GetObjectValueAsInt32(Napi::Object object, std::string keyName) {
if (object.Get(keyName).IsNumber()) {
return object.Get(keyName).ToNumber().Int32Value();
}
return 0;
}4、函數(shù)的返回值
- 類型約束:在函數(shù)前約束,類型可寫 addon 類型或者原生 C++ 類型
- 和 js 一樣,寫個 return 就可以了
5、env
- 是什么
- 可以理解為是 node addon 運行時的請求環(huán)境
- Env 對象通常由 Node.js 運行時或 node-addon-api 基礎結構創(chuàng)建和傳遞
- 怎么用:github.com/nodejs/node…
- 類型聲明:Napi::Env
- 取值:info.Env()
- 為什么需要構建這個環(huán)境
3、熟悉業(yè)務邏輯
有了上面兩個知識儲備后,下一步我們就要根據(jù)實際的業(yè)務場景,去寫 addon 邏輯了
如何向外暴露方法
這個例子可結合上面的 demo.cc 和 demo.h 來一起看
Value runSimpleAsyncWorker(const CallbackInfo& info) {
int runTime = info[0].As<Number>();
Function callback = info[1].As<Function>();
SimpleAsyncWorker* asyncWorker = new SimpleAsyncWorker(callback, runTime);
asyncWorker->Queue();
std::string msg =
"SimpleAsyncWorker for " + std::to_string(runTime) + " seconds queued.";
return String::New(info.Env(), msg.c_str());
};
Object Init(Env env, Object exports) {
exports["runSimpleAsyncWorker"] = Function::New(
env, runSimpleAsyncWorker, std::string("runSimpleAsyncWorker"));
return exports;
}
NODE_API_MODULE(addon, Init)編譯階段
編譯流程
使用 node-gyp 來構建,最終產(chǎn)出 .node 文件
1、第一步安裝所需依賴
npm i node-gyp -g
2、第二步配置 binding.gyp
{
"targets": [
{
"target_name": "demo",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [
"-Wc++11-extensions"
],
"sources": [
"./src/simple_async_worker.cc",
"./src/addon.cc",
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"./",
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
"conditions": [
[
'OS=="mac"',
{
"link_settings": {
"libraries": [
# 可引入一個靜態(tài)庫
]
},
"xcode_settings": {
"OTHER_CFLAGS": [ "-std=c++17", "-fexceptions", ],
},
'defines': [
'MACOS',
],
"cflags_cc": [
"-std=c++17"
]
}
],
]
},
],
}3、執(zhí)行 node-gyp rebuild 命令即可生成 require 方法可引入的 .node 文件
結語
根據(jù)以上步驟,可實現(xiàn)一個極為簡單的 node addon 擴展,但是在實際開發(fā)過程中,會面臨更多的問題解決,歡迎討論~
到此這篇關于Node中完整的 node addon 實現(xiàn)流程的文章就介紹到這了,更多相關node addon 流程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Node.js v8.0.0正式發(fā)布!看看帶來了哪些主要新特性
Node.js v8.0.0 已正式發(fā)布。v8.0.0 是下一個主要的版本,帶來了一系列重大的變化和新功能,內容十分多!下面這篇文章主要帶著大家一起看看Node.js v8.0.0帶來了哪些主要新特性,需要的朋友可以參考借鑒,下面來一起看看吧。2017-06-06
nodejs版本過高導致vue2版本的項目無法正常啟動的解決方案
這篇文章主要給大家介紹了關于nodejs版本過高導致vue2版本的項目無法正常啟動的解決方案,本文小編給大家詳細介紹了如何解決這個問題,如有遇到同樣問題的朋友可以參考下2023-11-11

