如何在 Node 中創建 C/C++ 插件
Node.js 之所以偉大有很多原因,其中之一是您可以構建有意義的應用程序的速度。然而,眾所周知,這是以性能為代價的(與本機代碼相比)。為了解決這個問題,您可以編寫代碼以與用 C 或 C++ 編寫的更快的代碼交互。我們需要做的就是讓 Node 知道在哪裡可以找到這段代碼以及如何與之交互。
根據您想要的抽象級別,有幾種方法可以解決此問題。我們將從最低的抽像開始,即 Node Addon。
插件
插件通過提供 Node 和 C/C++ 庫之間的粘合劑來工作。對於典型的 Node 開發人員來說,這可能有點複雜,因為您必須實際編寫 C/C++ 代碼來設置接口。但是,在本文和 Node 文檔之間,您應該能夠讓一些簡單的接口正常工作。
在開始創建插件之前,我們需要完成一些事情。首先,我們需要知道如何編譯(Node 開發人員樂於忘記的)本機代碼。這是使用 node-gyp 完成的。然後,我們將簡要討論 nan,它有助於處理不同的 Node API 版本。
節點-gyp
那裡有許多不同類型的處理器(x86、ARM、PowerPC 等),甚至在編譯代碼時需要處理更多的操作系統。幸運的是,node-gyp
為您處理所有這些。正如他們的 Github 頁面所述,node-gyp
是“用 Node.js 編寫的用於為 Node.js 編譯本機插件模塊的跨平台命令行工具”。本質上,node-gyp
只是 gyp 的一個封裝,由 Chromium 團隊製作。
該項目的自述文件有一些關於如何安裝和使用包的很好的說明,所以你應該閱讀它以獲得更多詳細信息。總之,要使用 node-gyp
您需要執行以下操作。
進入你的項目目錄:
$ cd my_node_addon
使用 configure
生成適當的構建文件 命令,它將創建一個 Makefile
(在 Unix 上)或 vcxproj
(在 Windows 上):
$ node-gyp configure
最後,構建項目:
$ node-gyp build
這將生成一個 /build
目錄,其中包含已編譯的二進製文件。
即使使用像 ffi
這樣的更高抽象層 包,了解幕後發生的事情仍然很好,所以我建議您花時間了解 node-gyp
的來龍去脈 .
南
nan
(Native Abstractions for Node) 是一個容易被忽視的模塊,但它會為您節省數小時的挫敗感。 Node 版本之間 v0.8
, v0.10
, 和 v0.12
,使用的 V8 版本經歷了一些大的變化(除了 Node 本身的變化),所以 nan
有助於對您隱藏這些更改,並提供一個漂亮、一致的界面。
這種原生抽象通過在 #include <nan.h>
中提供 C/C++ 對象/函數來工作 頭文件。
要使用它,請安裝 nan
包裝:
$ npm install --save nan
將這些行添加到您的 binding.gyp 文件中:
"include_dirs" : [
"<!(node -e \"require('nan')\")"
]
你已經準備好使用 nan.h
中的方法/函數了 在你的鉤子裡,而不是原來的 #include <node.h>
代碼。我強烈建議您使用 nan
.在這種情況下,重新發明輪子沒有多大意義。
創建插件
在開始使用插件之前,請確保您花一些時間熟悉以下庫:
- V8 JavaScript C++ 庫,用於實際與 JavaScript 交互(如創建函數、調用對像等)。
- 注意 :
node.h
是建議的默認文件,但實際上是nan.h
應該改用
- 注意 :
- libuv,一個用 C 語言編寫的跨平台異步 I/O 庫。該庫對於在本機中執行任何類型的 I/O(打開文件、寫入網絡、設置計時器等)非常有用庫,您需要使其異步。
- 內部節點庫。要理解的更重要的對象之一是
node::ObjectWrap
,大多數對像都是從它派生的。
在本節的其餘部分,我將引導您完成一個實際示例。在這種情況下,我們將為 C++ <cmath>
創建一個掛鉤 庫的 pow
功能。因為你應該幾乎總是使用 nan
,這就是我將在整個示例中使用的內容。
免費電子書:Git Essentials
查看我們的 Git 學習實踐指南,其中包含最佳實踐、行業認可的標準以及隨附的備忘單。停止谷歌搜索 Git 命令並真正學習 它!
對於這個例子,在你的插件項目中你應該至少有這些文件:
pow.cpp
binding.gyp
package.json
C++ 文件不需要命名為 pow.cpp
,但名稱通常反映它是一個插件,或者它的特定功能。
// pow.cpp
#include <cmath>
#include <nan.h>
void Pow(const Nan::FunctionCallbackInfo<v8::Value>& info) {
if (info.Length() < 2) {
Nan::ThrowTypeError("Wrong number of arguments");
return;
}
if (!info[0]->IsNumber() || !info[1]->IsNumber()) {
Nan::ThrowTypeError("Both arguments should be numbers");
return;
}
double arg0 = info[0]->NumberValue();
double arg1 = info[1]->NumberValue();
v8::Local<v8::Number> num = Nan::New(pow(arg0, arg1));
info.GetReturnValue().Set(num);
}
void Init(v8::Local<v8::Object> exports) {
exports->Set(Nan::New("pow").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Pow)->GetFunction());
}
NODE_MODULE(pow, Init)
注意沒有分號 (;
) 在 NODE_MODULE
的末尾 .這是有意完成的,因為 NODE_MODULE
實際上不是一個函數 - 它是一個宏。
對於那些有一段時間(或曾經)沒有寫過任何 C++ 的人來說,上面的代碼一開始可能看起來有點令人生畏,但它確實並不難理解。 Pow
函數是代碼的核心,我們檢查傳遞的參數數量、參數類型、調用原生 pow
函數,並將結果返回給 Node 應用程序。 info
object 包含我們需要知道的調用的所有內容,包括參數(及其類型)和返回結果的位置。
Init
函數大多只是關聯 Pow
具有
“pow”名稱和NODE_MODULE
的函數 宏實際上處理向 Node 註冊插件。
package.json
文件與普通的 Node 模塊沒有太大區別。雖然它似乎不是必需的,但大多數插件模塊都有 "gypfile": true
在其中設置,但沒有它,構建過程似乎仍然可以正常工作。這是我用於此示例的內容:
{
"name": "addon-hook",
"version": "0.0.0",
"description": "Node.js Addon Example",
"main": "index.js",
"dependencies": {
"nan": "^2.0.0"
},
"scripts": {
"test": "node index.js"
}
}
接下來,需要將此代碼構建到“pow.node”文件中,該文件是插件的二進製文件。為此,您需要告訴 node-gyp
它需要編譯哪些文件以及二進製文件的結果文件名。雖然您可以使用許多其他選項/配置與 node-gyp
,對於這個例子,我們不需要很多。 binding.gyp
文件可以很簡單:
{
"targets": [
{
"target_name": "pow",
"sources": [ "pow.cpp" ],
"include_dirs": [
"<!(node -e \"require('nan')\")"
]
}
]
}
現在,使用 node-gyp
,為給定平台生成合適的項目構建文件:
$ node-gyp configure
最後,構建項目:
$ node-gyp build
這應該會導致 pow.node
正在創建的文件,該文件將駐留在 build/Release/
目錄。要在您的應用程序代碼中使用此掛鉤,只需 require
在 pow.node
文件(沒有“.node”擴展名):
var addon = require('./build/Release/pow');
console.log(addon.pow(4, 2)); // Prints '16'
節點外部函數接口
注意 :ffi
包以前稱為 node-ffi
.請務必添加較新的 ffi
為您的依賴項命名以避免在 npm install
期間出現很多混淆 :)
雖然 Node 提供的插件功能為您提供了所需的所有靈活性,但並非所有開發人員/項目都需要它。在許多情況下,像 ffi
這樣的抽象 會做的很好,通常需要很少甚至不需要 C/C++ 編程。
ffi
只加載動態庫,這可能會限制某些人,但它也使掛鉤更容易設置。
var ffi = require('ffi');
var libm = ffi.Library('libm', {
'pow': [ 'double', [ 'double', 'double' ] ]
});
console.log(libm.pow(4, 2)); // 16
上面的代碼通過指定要加載的庫 (libm) 來工作,特別是從該庫 (pow) 加載哪些方法。 [ 'double', [ 'double', 'double' ] ]
行告訴 ffi
方法的返回類型和參數是什麼,在本例中是兩個double
參數和一個 double
回來了。
結論
雖然一開始可能看起來很嚇人,但在您有機會自己完成一個像這樣的小例子之後,創建一個插件真的不是太糟糕。如果可能的話,我建議掛鉤到一個動態庫,以使創建界面和加載代碼更加容易,儘管對於許多項目來說這可能是不可能的或最佳選擇。
是否有任何您想查看綁定的庫示例?請在評論中告訴我們!