前端輪子千千萬, 但仍是有些瓶頸, 公司須要在前端調用自有 tcp 協議, 該協議只有 c++ 的封裝版本. 領導但願能夠直接調該模塊, 不要重複造輪子.html
實話說我對 C 還有點印象, 畢竟也是有二級 C 語言證的人..可是已經好久沒用了, 看着一大堆的C 語言類型的定義, 讓我這個常年使用隱式類型的 jser 情何以堪.這是我從業以來最難實現的 hello world
項目.前端
一個 Native Addon 在 Nodejs 的環境裏就是一個二進制文件, 這個文件是由低級語言, 好比 C 或 C++實現, 咱們能夠像調用其餘模塊同樣 require() 導入 Native Addonnode
Native Addon 與其餘.js 的結尾的同樣, 會暴露出 module.exports
或者 exports
對象, 這些被封裝到 node 模塊中的文件也被成爲 Native Module(原生模塊).python
那麼如何讓 Native Addon 能夠加載並運行在 js 的應用中? 讓 Native Addon 能夠兼容 js 的環境而且暴露的 API 能夠像正常 node 模塊同樣被使用呢?linux
這裏不得不說下 DLL(Dynamic Linked Library)動態庫, 他是由 C 或 C++使用標準編譯器編譯而成, 在 linux 或 macOS 中也被稱做 Shared Library. 一個 DLL 能夠被一個程序在運行時動態加載, DLL 包含源 C 或 C++代碼以及可通訊的 API. 有動態是否還有靜態的呢? 還真有~ 能夠參考這裏來看這二者的區別, 簡單來講靜態比動態更快, 由於靜態不須要再去查找依賴文件並加載, 可是動態能夠顆粒度更小的修改打包的文件.ios
在 Nodejs 中, 當編譯出 DLL 的時候, 會被導出爲.node 的後綴文件. 而後能夠 require 該文件, 像 js 文件同樣.不過代碼提示是不可能有的了.c++
Nodejs 實際上是不少開源庫的集合,能夠看看他的倉庫, 在 package.json 中找 deps. 使用的是谷歌開源的 V8 引擎來執行 js 代碼, 而 V8恰好是使用 C++寫的, 不信你看 v8 的倉庫. 而對於像異步 IO, 事件循環和其餘低級的特性則是依賴 Libuv 庫.git
當安裝完 nodejs 以後, 其實是安裝了一個包含整個 Nodejs 以及其依賴的源代碼的編譯版本, 這樣就不用一個一個手動安裝這些依賴而. 不過Nodejs也能夠由這些庫的源代碼編譯而來. 那麼跟 Native Addon 有什麼關係呢? 由於 Nodejs 是由低層級的 C 和 C++編譯而成的, 因此自己就具備與 C 和 C++相互調用的能力.github
Nodejs 能夠動態加載 C 和 C++的 DLL 文件, 而且使用其 API 在 js 程序中進行操做. 以上就是基本的 Native Addon 在 Nodejs 中的工做原理.npm
ABI 是特指應用去訪問編譯好|compiled的程序, 跟 API(Application Programming Interface)很是類似, 只不過是與二進制文件進行交互, 並且是訪問內存地址去查找 Symbols, 好比 numbers, objects, classes和 functions
那麼這個 ABI 跟 Native Addon 有什麼關係呢? 他是 Native Addon 與 Nodejs 進行通訊的橋樑. DDL 文件其實是經過 Nodejs 提供的ABI 來註冊或者訪問到值, 而且經過Nodejs暴露的 API和庫來執行命令.
舉個例子, 有個 Native Addon 想添加一個sayHello
的方法到exports
對象上, 他能夠經過訪問 Libuv 的 API 來建立一個新的線程,異步的執行任務, 執行完畢以後再調用回調函數. 這樣 Nodejs 提供的 ABI 的工做就完成了.
一般來講, 都會將 C 或 C++編譯爲 DLL, 會使用到一些被稱做header 頭文件的元數據. 都是以.h
結尾.固然這些頭文件中, 能夠是 Nodejs及node的庫暴露出去的可讓 Native Addon引用的.頭文件的資料可參考
一個典型的引用是使用#include
好比#inlude<v8.h>
, 而後使用聲明來寫 Nodejs 可執行的代碼.有如下四種方式來使用頭文件.
好比v8.h
-> v8引擎, uv.h
-> Libuv庫這兩個文件都在 node 的安裝目錄中. 可是這樣的問題就是 Native Addon 和 Nodejs 之間的依賴程度過高了.由於 Nodejs 的這些庫有可能隨着 Node 版本的更新而更改, 那麼每次更改以後是否還要去適配更改 Native Addon? 這樣的維護成本較高.你能夠看看 node 官方文檔中對這種方法的描述, 下面有更好的方法
NAN 項目最開始就是爲了抽象 nodejs 和 v8 引擎的內部實現. 基本概念就是提供了一個 npm 的安裝包, 能夠經過前端的包管理工具yarn
或npm
進行安裝, 他包含了nan.h
的頭文件, 裏面對 nodejs 模塊和 v8 進行了抽象. 可是 NAN 有如下缺點:
因此更推薦如下兩種方式
N-API相似於 NAN 項目, 可是是由 nodejs 官方維護, 今後就不須要安裝外部的依賴來導入到頭文件. 而且提供了可靠的抽象層 他暴露了node_api.h
頭文件, 抽象了 nodejs 和包的內部實現, 每次 Nodejs 更新, N-API 就會同步進行優化保證 ABI 的可靠性 這裏是 N-API 的全部接口文檔, 這裏是官方對 N-API 的 ABI 穩定性的描述
N-API 同時適合於 C 和 C++, 可是 C++的 API 使用起來更加的簡單, 因而, node-addon-api 就應運而生.
跟上述兩個同樣, 他有本身的頭文件napi.h
, 包含了 N-API 的全部對 C++的封裝, 而且跟 N-API 同樣是由官方維護, 點這裏查看倉庫.由於他的使用相較於其餘更加的簡單, 因此在進行 C++API 封裝的時候優先選擇該方法.
須要全局安裝yarn global add node-gyp
, 由於還依賴於 Python, (GYP 全稱是 Generate Your Project, 是一個用 Python 寫成的工具). 具體制定 python 的環境及路徑參考文檔.
安裝完成後就有了一個生成編譯 C 或 C++到 Native Addon 或 DLL的模板代碼的CLI, 一頓操做猛如虎後,會生成一個.node
文件. 可是這個模板是怎麼生成的呢?就是下面這個 binding.gyp
文件
binding.gyp
binding.gyp
包含了模塊的名字, 哪些文件應該被編譯等. 模板會根據不一樣的平臺或架構(32仍是 64)包含必要的構建指令文件, 也提供了必要的 header 或 source 文件去編譯 C 或 C++, 相似於 JSON 的格式, 詳情可點擊查看.
安裝依賴後, 真正開始咱們的 hello world 項目, 總體的項目文件結構爲:
├── binding.gyp
├── index.js
├── package.json
├── src
│ ├── greeting.cpp
│ ├── greeting.h
│ └── index.cpp
└── yarn.lock
複製代碼
Native Module 跟正常的 node 模塊或其餘 NPM 包同樣. 先yarn init -y
初始化項目, 再安裝node-addon-apiyarn add node-addon-api
.
建立 greeting.h 文件
#include <string> std::string helloUser(std::string name); 複製代碼
建立 greeting.cpp 文件
#include <iostream> #include <string> #include "greeting.h" std::string helloUser(std::string name) { return "Hello " + name + "!"; } 複製代碼
建立 index.cpp 文件, 該文件會包含 napi.h
#include <napi.h>
#include <string>
#include "greeting.h"
// 定義一個返回類型爲 Napi String 的 greetHello 函數, 注意此處的 info
Napi::String greetHello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
std::string result = helloUser('Lorry');
return Napi::String::New(env, result);
}
// 設置相似於 exports = {key:value}的模塊導出
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(
Napi::String::New(env, "greetHello"), // key
Napi::Function::New(env, greetHello) // value
);
return exports;
}
NODE_API_MODULE(greet, Init)
複製代碼
注意這裏你看到不少的 Napi:: 這樣的書寫, 其實這就是在 js 與 C++之間的數據格式橋樑, 定義雙方都看得懂的數據類型. 這裏經歷瞭如下流程:
napi.h
頭文件, 他會解析到下面會說的 binding.gyp 指定的路徑中greeting.h
自定義頭文件. 注意使用 ""和<>的區別, ""會查找當前路徑, 詳情請查看node-addon-api
的頭文件. Napi 是一個命名空間. 由於宏不支持命名空間, 因此 NODE_API_MODULE
前沒有NODE_API_MODULE
是一個node-api
(N-API)中封裝的NAPI_MODULE
宏中提供的函數(宏). 它將會在js 使用require
導入 Native Addon的時候被調用.binding.gyp
中的 target_name 保持一致, 只不過這裏是使用一個標籤 label 而不是字符串的格式env
和 exports
參數env
值是Napi::env
類型, 包含了註冊模塊時的環境(environment), 這個在 N-API 操做時被使用. Napi::String::New
表示建立一個新的Napi::String
類型的值.這樣就將 helloUser的std:string
轉換成了Napi::String
exports
是一個module.exports
的低級 API, 他是Napi::Object
類型, 可使用Set
方法添加屬性, 參考文檔, 該函數必定要返回一個exports
建立binding.gyp
文件
{
"targets": [
{
"target_name": "greet", // 定義文件名
"cflags!": [ "-fno-exceptions" ], // 不要報錯
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [ // 包含的待編譯爲 DLL 的文件們
"./src/greeting.cpp",
"./src/index.cpp"
],
"include_dirs": [ // 包含的頭文件路徑, 讓 sources 中的文件能夠找到頭文件
"<!@(node -p \"require('node-addon-api').include\")"
],
'defines': [
'NAPI_DISABLE_CPP_EXCEPTIONS' // 去掉全部報錯
],
}
]
}
複製代碼
生成模板文件
在 binding.gyp
同級目錄下使用
node-gyp configure
複製代碼
將會生成一個 build 文件夾, 會包含如下文件:
./build
├── Makefile // 包含如何構建 native 源代碼到 DLL 的指令, 而且兼容 Nodejs 的運行時
├── binding.Makefile // 生成文件的配置
├── config.gypi // 包含編譯時的配置列表
├── greet.target.mk // 這個 greet 就是以前配置的 target_name 和 NODE_API_MODULE 的第一個參數
└── gyp-mac-tool // mac 下打包的python 工具
複製代碼
構建並編譯
node-gyp build
複製代碼
將會構建出一個.node
文件
./build
├── Makefile
├── Release
│ ├── greet.node // 這個就是編譯出來的node文件, 可直接被 js require 引用
│ └── obj.target
│ └── greet
│ └── src
│ ├── greeting.o
│ └── index.o
├── binding.Makefile
├── config.gypi
├── greet.target.mk
└── gyp-mac-tool
複製代碼
走到這一步你會發現.node
文件是沒法被打開的, 由於他就不是給人讀的, 是一個二進制文件.這個時候就能夠嘗試一波
// index.js const addon = require('./build/Release/greet.node') console.log(addon.greetHello()) 複製代碼
直接使用node index.js
運行代碼你會發現打印出 Hello Lorry !
, 正是 helloUser 裏面的內容. 真是不容易啊.
僅僅到此嗎? 還不夠
傳參
上述代碼都是寫死的 Lorry, 我要是 Mike, Jane, 張三王五呢?並且不能傳參的函數不是好函數
因而以前說到的 info 就起做用了, 詳情可參考, 由於info的[]運算符重載, 能夠實現對類C++數組的訪問. 如下是對 index.cpp
文件的 greetHello
函數的修改:
Napi::String greetHello(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); std::string user = (std::string) info[0].ToString(); std::string result = helloUser(user); return Napi::String::New(env, result); } 複製代碼
而後使用
node-gyp rebuild
複製代碼
在修改下引用的 index.js 文件
const addon = require('./build/Release/greet.node') console.log(addon.greetHello('張三')) // Hello 張三! 複製代碼
至此, 終於算是比較完整的實現了咱們的 hello world.別急, 還有貨
若是要像其餘包同樣能夠進行發佈的話, 操做就跟正常的npm打包流程差很少了. 在package.json
中的 main 字段中指定 index.js
,而後修改index.js
內容爲:
const addon = require('./build/Release/greet.node') module.exports = addon.greetHello 複製代碼
再使用 yarn pack
便可打包出一個.tgz
, 在其餘項目中引入便可.還有沒有?還有一點點
一般在發佈模塊的時候, 不會把build
文件夾算在內, 可是.node
文件是放在裏面的. 並且.node
文件以前說了, 依賴於系統和架構, 若是是使用 macOS 打包的.node
確定是不能在 windows 上使用的. 那麼怎麼實現兼容性呢? 沒錯, 每次在用戶安裝的時候都從新按照對應硬件配置build 一遍, 也就是使用node-gyp rebuild
, npm或者 yarn 在安裝依賴過程當中發現了binding.gyp
的話會自動在本地安裝node-gyp
, 因此 rebuild
才能成功.
不過,還記得嗎? 處理 node-gyp 以外還有別的前提條件, 這就是爲何在安裝一些庫的時候常常會出現 node-gyp 的報錯.好比 python 的版本? node 的版本? 都有可能致使安裝這個模塊的用戶抓狂.因而還有一個辦法:爲每一個平臺架構打包一份.node 文件, 這能夠經過 pacakge.json 的 install 腳本實現區分安裝, 有一個第三方包 node-pre-gyp
能夠自動實現. 若是不想使用 node-pre-gyp 中那麼複雜的配置, 還能夠嘗試 prebuild-install
這個輪子
可是還有一個問題, 咱們如何實現打包出不一樣平臺和架構的文件? 難道我買各類硬件來打包?不現實. 沒事, 還有輪子 prebuild
, 能夠設置不一樣平臺, 架構甚至 node 版本都能指定.
PS: 這裏還有一個 vscode 的坑, 在使用 C++ 的 extension 進行代碼提示的時候總是提醒我#include <napi.h>
找不到文件,可是打包是徹底沒有問題的, 猜想是編輯器不支持識別 binding.gyp 裏的頭文件查找路徑, 找了不少地方沒有相應的解決辦法.最後翻這個插件的文檔發現能夠配置clang.cxxflags
, 因而乎我在裏面添加了一條頭文件的指定路徑-I${workspaceRoot}/node_modules/node-addon-api
就沒問題了, 能夠享受代碼提示了, 否則真的很容易寫錯啊!!