Node中完整的?node?addon?實(shí)現(xiàn)流程
背景介紹
為什么要寫 node addon
試想這樣一種場(chǎng)景:我們想在 js 層實(shí)現(xiàn)某個(gè)業(yè)務(wù)場(chǎng)景,但是這套業(yè)務(wù)邏輯已經(jīng)有存在的 C++ 版本了,這個(gè)時(shí)候我們有兩個(gè)選擇
- 重新實(shí)現(xiàn)一套在 JS 版本的業(yè)務(wù)場(chǎng)景
- 使用
node addon
橋接C++
版本代碼
對(duì)比以上兩種方案,顯然,使用 addon
不用去寫過重的業(yè)務(wù)邏輯,是一種成本更低的方案
node addon 是什么
node addon
,即為node
插件 / 擴(kuò)展,插件是用C++
編寫的動(dòng)態(tài)鏈接共享對(duì)象。- 動(dòng)態(tài)鏈接共享對(duì)象,即動(dòng)態(tài)鏈接庫(kù)
- 鏈接庫(kù):庫(kù)文件的二進(jìn)制版本,即將庫(kù)文件進(jìn)行編譯、打包操作后得到二進(jìn)制文件,無法獨(dú)立運(yùn)行,必須等待其他程序調(diào)用才會(huì)被載入內(nèi)存中。
- 靜態(tài)鏈接:無論缺失的地址位于其他目標(biāo)文件還是鏈接庫(kù),鏈接庫(kù)都會(huì)逐個(gè)找到各目標(biāo)文件中缺失的地址。采用此鏈接方式生成的可執(zhí)行文件,可以獨(dú)立載入內(nèi)存運(yùn)行。
- 動(dòng)態(tài)鏈接:鏈接器先從所有目標(biāo)文件中找到部分缺失的地址,然后將所有目標(biāo)文件組織成一個(gè)可執(zhí)行文件。這樣生成的可執(zhí)行文件,仍缺失部分函數(shù)和變量地址,待文件執(zhí)行時(shí),需連同所有的鏈接庫(kù)文件一起載入內(nèi)存,再由鏈接器完成剩余的地址修復(fù)工作,才能正常執(zhí)行
- 靜態(tài)鏈接庫(kù):在生成可執(zhí)行文件之前完成所有鏈接操作,使用的庫(kù)文件是靜態(tài)鏈接庫(kù),后綴名 .a .lib
- 動(dòng)態(tài)鏈接庫(kù):將部分鏈接操作推遲到程序執(zhí)行時(shí)才進(jìn)行,使用的庫(kù)文件是動(dòng)態(tài)鏈接庫(kù),后綴名:.so .dll .dylib
- 插件提供了
JavaScript
和C/C++
庫(kù)之間的接口。 require
函數(shù)可以將插件加載為普通的Node.js
模塊。- 通俗點(diǎn)來講,是一個(gè)能夠橋接
c++
和js
的中間轉(zhuǎn)換層
可通過 NODE-API
、NAN
、或者使用底層 v8
庫(kù)來實(shí)現(xiàn)【官方建議使用 NODE-API
】
node-api
:構(gòu)建原生插件的 api,獨(dú)立于 JS 運(yùn)行時(shí),此 API 是跨 Node.js 版本穩(wěn)定的應(yīng)用程序二進(jìn)制接口,它旨在將插件與底層 JavaScript 引擎中的更改隔離開來,并允許為一個(gè)主要版本編譯的模塊nan(Native Abstractions for Node.js)
:是一個(gè)Node.js
原生模塊抽象接口集。它提供了一套API
- 底層
V8
:就是我們熟悉的Chrome V8
addon 實(shí)現(xiàn)方式的變遷
Chrome V8 API
1、是啥:即使用 Node
自身各種 API
以及 Chrome V8
的 API
2、存在的問題
這些寫好的代碼只能在特定的 Node 版本下編譯,因?yàn)槠渲懈鞣N API、函數(shù)聲明等的變化會(huì)很大,舉個(gè)例子
Handle<Value> Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo<value>& args); // 6.x
NAN 時(shí)代
1、是啥:
Native Abstractions for Node.js
,即Node.js
原生模塊抽象接口集- 代碼只需要隨著
NAN
的升級(jí)做改變,它會(huì)幫我們兼容各個(gè)版本
2、存在的問題
- 一次寫好的代碼在不用版本的
Node.js
下也需要重新編譯,如果版本不符合,Node.js
就無法正常載入一個(gè)C++
擴(kuò)展- NAN 的封裝方式是使用了一堆宏,在不同的 Node 版本下使用不同的宏變量、函數(shù)等,所以針對(duì)用戶使用不同的 Node 版本,是需要進(jìn)行重新編譯的
符合 ABI 的 N-API
1、是啥
- 自從 2017 年 Node.js v8.0.0 發(fā)布之后,Node.js 推出了全新的用于開發(fā) C++ 原生模塊的接口-> N-API
- 與 NAN 相比,它把 Node.js 的所有底層數(shù)據(jù)結(jié)構(gòu)全部黑盒化,抽象成 N-API 中的接口;不同版本的 Node.js 使用同樣的接口,這些接口穩(wěn)定且 ABI 化。只要 ABI 版本號(hào)一致,編譯好的 C++ 擴(kuò)展就可以直接使用,而不需要重新編譯
- ABI 化:(Application Binary Interface)應(yīng)用程序二進(jìn)制接口;可以理解為一種約定,是 API 的編譯版本;ABI 允許編譯好的目標(biāo)代碼在使用兼容 ABI 的系統(tǒng)中無需改動(dòng)就能運(yùn)行;一套完整的 ABI 可以讓程序在所有支持該 ABI 的系統(tǒng)上運(yùn)行,無需對(duì)程序進(jìn)行修改
- API:(Application programming interface),應(yīng)用編程接口
- 主要收益:消除了 Nodejs 版本之間的差異
2、N-API 的使用姿勢(shì)
- 提供頭文件 node_api.h
- 使用文檔:nodejs.org/api/n-api.h…
3、node-addon-api 是啥?
- 可以理解為是對(duì) N-API 更進(jìn)一步的封裝,更加便于我們開發(fā)
- 舉個(gè)例子
// 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++ 基礎(chǔ)語(yǔ)法
宏的定義:#define 是定義一個(gè)宏的指令(預(yù)編譯指令),它用來將一個(gè)標(biāo)識(shí)符定義為一個(gè)字符串,該標(biāo)識(shí)符被稱為宏,被定義的字符串被稱為替換文本,
- 簡(jiǎn)單的宏定義和帶參數(shù)的宏定義
- 當(dāng)宏出現(xiàn)在一個(gè)文件中時(shí),在該文件后續(xù)出現(xiàn)的所有宏都將被替換為 《替換文本》
- 常用于條件編譯情況下,比如版本號(hào)的不同而編譯不同的邏輯
// 簡(jiǎn)單的宏定義 #define PI 1415926 // 宏名 字符串 // 帶參數(shù)的宏定義 #define A(x) x // 宏名(參數(shù)表) 宏體
- 類
- 公有繼承、私有繼承、保護(hù)繼承
- 公有繼承:繼承父類 public 和 protected 的方法和變量,不能訪問 private
- 私有繼承:繼承父類的 public 和 protected 的方法和變量作為私有成員,不能被該類的子類再次訪問
- 保護(hù)繼承:繼承父類 public 和 protected 成員作為保護(hù)成員
- 公有、私有、受保護(hù)的成員
- public:可被子類繼承或在類外內(nèi)訪問
- private:僅限類內(nèi)部使用,不可被繼承和訪問
- protected: 可被子類繼承,但不能在類外訪問
- 公有繼承、私有繼承、保護(hù)繼承
// test.h class Test : public B { // private || protected public: private: protected: int pro = 1; } // 類外 #include "test.h" Test test; // 實(shí)例化 Test 類 std::cout << test.pro << std::endl; // error -> 不可在類外被訪問
- 構(gòu)造函數(shù)和析構(gòu)函數(shù)
- 構(gòu)造函數(shù):類的構(gòu)造函數(shù)是類的一種特殊的成員函數(shù),它會(huì)在每次創(chuàng)建類的新對(duì)象時(shí)執(zhí)行。類似于 JS 中的 constructor; 可自己實(shí)現(xiàn),也可使用編譯器生成的默認(rèn)構(gòu)造函數(shù),即與類名相同的函數(shù);
- 析構(gòu)函數(shù):類的析構(gòu)函數(shù)是類的一種特殊的成員函數(shù),它會(huì)在每次刪除所創(chuàng)建的對(duì)象時(shí)執(zhí)行。析構(gòu)函數(shù)的名稱與類的名稱是完全相同的,只是在前面加了個(gè)波浪號(hào)(~)作為前綴,它不會(huì)返回任何值,也不能帶有任何參數(shù)。析構(gòu)函數(shù)有助于在跳出程序(比如關(guān)閉文件、釋放內(nèi)存等)前釋放資源。
- 虛函數(shù)、純虛函數(shù)是啥
- virtual:虛函數(shù)關(guān)鍵字,子類可選擇自己實(shí)現(xiàn)或使用父類原有方法
- = 0:純虛函數(shù)關(guān)鍵字,子類必須自己實(shí)現(xiàn),如果不實(shí)現(xiàn),編譯階段將會(huì)報(bào)錯(cuò)
- override:是一個(gè)覆蓋虛函數(shù)的標(biāo)識(shí)符
2、熟悉 addon 語(yǔ)法
1、如何讓 js require?無后綴情況下的 .js -> .json -> .node
- 在 js 中,使用 commonJs 語(yǔ)法即可讓該模塊被其他模塊 require,addon 中則也是類似的想法
- 在 addon 中,提供了 NODE_API_MODULE 宏方法,用這個(gè)方法即可實(shí)現(xiàn)外部 require 效果,方法接收兩個(gè)參數(shù),即模塊名字和導(dǎo)出的方法
- 具體實(shí)現(xiàn)?
Napi::Object InitAll(Napi::Env env, Napi::Object exports) { return Link::Init(env, exports); } NODE_API_MODULE(link, InitAll);
2、定義一個(gè)類以及注冊(cè)方法
- 在 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 中
- 接收多個(gè)參數(shù):
- 定義好每個(gè)參數(shù)的類型接收
- 統(tǒng)一在 CallbackInfo 中接收:github.com/nodejs/node…
- 接收一個(gè)對(duì)象
- 也是從 info[0] 中去拿到這個(gè)對(duì)象,然后用 object.Get(key) 方法拿到對(duì)應(yīng)的參數(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 一樣,寫個(gè) return 就可以了
5、env
- 是什么
- 可以理解為是 node addon 運(yùn)行時(shí)的請(qǐng)求環(huán)境
- Env 對(duì)象通常由 Node.js 運(yùn)行時(shí)或 node-addon-api 基礎(chǔ)結(jié)構(gòu)創(chuàng)建和傳遞
- 怎么用:github.com/nodejs/node…
- 類型聲明:Napi::Env
- 取值:info.Env()
- 為什么需要構(gòu)建這個(gè)環(huán)境
3、熟悉業(yè)務(wù)邏輯
有了上面兩個(gè)知識(shí)儲(chǔ)備后,下一步我們就要根據(jù)實(shí)際的業(yè)務(wù)場(chǎng)景,去寫 addon 邏輯了
如何向外暴露方法
這個(gè)例子可結(jié)合上面的 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 來構(gòu)建,最終產(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": [ # 可引入一個(gè)靜態(tài)庫(kù) ] }, "xcode_settings": { "OTHER_CFLAGS": [ "-std=c++17", "-fexceptions", ], }, 'defines': [ 'MACOS', ], "cflags_cc": [ "-std=c++17" ] } ], ] }, ], }
3、執(zhí)行 node-gyp rebuild
命令即可生成 require
方法可引入的 .node
文件
結(jié)語(yǔ)
根據(jù)以上步驟,可實(shí)現(xiàn)一個(gè)極為簡(jiǎn)單的 node addon
擴(kuò)展,但是在實(shí)際開發(fā)過程中,會(huì)面臨更多的問題解決,歡迎討論~
到此這篇關(guān)于Node中完整的 node addon 實(shí)現(xiàn)流程的文章就介紹到這了,更多相關(guān)node addon 流程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解webpack打包nodejs項(xiàng)目(前端代碼)
這篇文章主要介紹了webpack打包nodejs項(xiàng)目(前端代碼),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09從零學(xué)習(xí)node.js之express入門(六)
相信大家都知道Express是一個(gè)簡(jiǎn)潔而靈活的 node.js Web應(yīng)用框架, 提供了一系列強(qiáng)大特性幫助你創(chuàng)建各種 Web 應(yīng)用,和豐富的 HTTP 工具。下面這篇文章主要介紹了node.js中express的入門知識(shí),需要的朋友可以參考下。2017-02-02NodeJs實(shí)現(xiàn)簡(jiǎn)單的爬蟲功能案例分析
爬蟲,是一種按照一定的規(guī)則,自動(dòng)地抓取網(wǎng)頁(yè)信息的程序或者腳本。這篇文章通過一個(gè)案例給大家分享NodeJs實(shí)現(xiàn)簡(jiǎn)單的爬蟲功能,感興趣的朋友一起看看吧2018-12-12windows系統(tǒng)下安裝npm(Node.js)方法教程
在Windows環(huán)境下進(jìn)行Node.js的安裝并不是一件復(fù)雜的事情,但是在安裝過程中需要注意一些細(xì)節(jié),下面這篇文章主要給大家介紹了關(guān)于windows系統(tǒng)下安裝npm(Node.js)的相關(guān)資料,需要的朋友可以參考下2023-12-12node.js中的fs.realpathSync方法使用說明
這篇文章主要介紹了node.js中的fs.realpathSync方法使用說明,本文介紹了fs.realpathSync的方法說明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12Node.js安裝及npm國(guó)內(nèi)鏡像配置的方法實(shí)現(xiàn)
本文主要介紹了Node.js安裝及npm國(guó)內(nèi)鏡像配置,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06使用Jasmine和Karma對(duì)AngularJS頁(yè)面程序進(jìn)行測(cè)試
這篇文章主要介紹了使用Jasmine和Karma對(duì)AngularJS頁(yè)面程序進(jìn)行測(cè)試的方法,以Node.js為環(huán)境,非常適合JavaScript的全棧開發(fā)時(shí)使用,需要的朋友可以參考下2016-03-03node.js中的fs.readFileSync方法使用說明
這篇文章主要介紹了node.js中的fs.readFileSync方法使用說明,本文介紹了fs.readFileSync的方法說明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12