什麼是N-API
N-API爲開發者提供了一套C/C++ API用於開發Node.js的Native擴展模塊。從Node.js 8.0.0開始,N-API以實驗性特性作爲Node.js本身的一部分被引入,並且從Node.js 10.0.0開始正式全面支持N-API。
Hello N-API
本文將使用一個簡單的模塊作爲示例介紹N-API。我們將編寫一個hello
模塊,其中包括一個返回Hello N-API!
字符串的方法greeting
。其實現的功能相當於下列Javascript代碼:
const greeting = () => {
return 'Hello N-API!';
}
module.exports = {
greeting,
};
greeting方法定義
首先,我們需要定義greeting
方法,並返回值爲Hello N-API!
的字符串。爲了使用N-API提供的接口及類型定義,我們需要引入node_api.h
頭文件。使用N-API定義的方法需要滿足napi_callback
類型,其定義爲:
typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info);
napi_callback
是使用N-API開發的Native函數的函數指針類型,其接受類型分別爲napi_env
以及napi_callback_info
的兩個參數,並返回類型爲napi_value
的值。greeting
方法中涉及到的幾個類型定義及其用途如下:
napi_value
類型是一個用於表示Javascript值的指針napi_env
類型用於存儲Javascript虛擬機的上下文napi_callback_info
類型用於調用回調函數時,傳遞調用時的上下文信息
我們定義的greeting
方法如下:
napi_value greeting(napi_env env, napi_callback_info info) {
napi_status status;
napi_value word;
char *str = "Hello N-API!";
status = napi_create_string_utf8(env, str, strlen(str), &word);
assert(status == napi_ok);
return word;
}
在greeting
方法中,我們通過napi_create_string_utf8
函數創建了值爲"Hello N-API!"
的Javascript字符串對象,並將其作爲該方法的返回值返回。napi_create_string_utf8
用於創建一個UTF-8類型的字符串對象,其值來自於參數傳遞的UTF-8編碼字符串,函數原型如下:
napi_status napi_create_string_utf8(napi_env env,
const char *str,
size_t length,
napi_value* result);
env
:傳遞當前VM的上下文信息str
:UTF-8編碼的字符序列length
:字符序列str
的長度result
:用於表示創建的Javascript字符串對象的指針
napi_create_string_utf8
返回一個napi_status
類型的值,當其值爲napi_ok
時代表完成字符串對象的創建。如示例中代碼所示,我們在調用napi_create_string_utf8
後,便使用assert
判斷其返回值是否爲napi_ok
。
napi_status
是一個用於指示N-API中狀態的枚舉類型,其值可參考napi_status。
模塊註冊
在完成了greeting
方法後,我們還需要註冊我們的hello
模塊。N-API通過NAPI_MODULE(modname, regfunc)
宏進行模塊的註冊。其接受兩個參數,分別爲模塊名及模塊初始化函數。模塊初始化函數需要滿足下列函數簽名:
napi_value (*)(napi_env env, napi_value exports);
在模塊的初始化中,我們可以定義模塊需要暴露的方法及屬性。我們的模塊初始化函數如下所示:
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_property_descriptor descriptor = {
"greeting",
0,
greeting,
0,
0,
0,
napi_default,
0,
};
status = napi_define_properties(env, exports, 1, &descriptor);
assert(status == napi_ok);
return exports;
}
NAPI_MODULE(hello, init);
在我們的的初始化函數中,需要在模塊的exports
對象中定義greeting
屬性。在定義屬性之前,我們需要創建一個napi_property_descriptor
類型的屬性描述符,該類型的定義如下:
typedef struct {
const char* utf8name;
napi_value name;
napi_callback method;
napi_callback getter;
napi_callback setter;
napi_value value;
napi_property_attributes attributes;
void* data;
} napi_property_descriptor;
對於本文示例中需要使用的屬性值描述如下所示,關於napi_property_descriptor
的更多描述可參考napi_property_descriptor。
utf8name
:UTF-8編碼的字符序列name
:由Javascript對象表示的字符串或者Symbol
utf8name
以及name
二者中必須且只能有一個被提供,其代表屬性的名稱。
method
:將該屬性設置爲表示一個Javascript方法(function)attributes
:屬性的行爲控制標誌,示例中使用了默認的napi_default
值,更多描述可參考napi_property_attributes
我們需要定義的greeting
屬性是一個方法,所以我們所創建的屬性描述符主要傳遞了utf8name
以及method
屬性。
在創建屬性描述符後,便需要將其在模塊的exports
對象中定義,使Javascript代碼能夠訪問。對象屬性的定義使用了napi_define_properties
函數,它可以快速的爲一個對象定義指定數量的屬性。該函數定義爲:
napi_status napi_define_properties(napi_env env,
napi_value object,
size_t property_count,
const napi_property_descriptor *properties);
object
:需要定義屬性的Javascript對象property_count
:屬性數量properties
:屬性描述符數組
同樣,napi_define_properties
也返回了一個napi_status
類型的值表示函數調用是否成功。
最後,我們只需要在模塊初始化函數中返回exports
對象,並通過NAPI_MODULE(hello, init)
註冊hello
模塊。到此爲止,我們的hello
模塊便編寫完成了。
模塊編譯
Native模塊的構建可選擇node-gyp
或者cmake.js
,二者的使用需要安裝C/C++工具鏈,本文選擇了node-gyp
作爲示例的構建工具。node-gyp
是基於Google的gyp
工具開發,它除了必要的C/C++編譯器以外,還需要安裝Python以及make工具。對於Windows用戶,使用node-gyp
需要安裝Python並通過npm安裝windows-build-tools
(npm install --global --production windows-build-tools
)。
接下來,需要定義binding,gyp
文件。binding,gyp
是node-gyp的JSON類型配置文件,文中示例程序使用的binding.gyp
內容如下所示:
{
"targets": [
{
"target_name": "hello",
"sources": [
"hello.c"
]
}
]
}
如示例所示,binding,gyp
文件中定義了targets
,它定義了一組gyp能生成的目標。targets
中定義了一個對象,其包括了target_name
和sources
兩個屬性。target_name
定義了該Native包的名稱,sources
定義了需要編譯的文件。
對於gyp文件的更多配置,可參考nodejs/node-gyp、GYP User Documentation以及GYP Input Format Reference。
接下來便可以使用node-gyp
構建示例中編寫的Native模塊。
$ node-gyp configure build
在完成構建後,將會在當前目錄下產生一個build
文件,其中包括了生成的各個中間文件以及.node
文件。.node
文件本質上即一個動態的鏈接庫,Node.js會調用dlopen
函數用於加載.node
文件。
測試
在構建Native模塊後,就將在js代碼中引入生成的.node
文件,並調用上文模塊中定義greeting
方法。
const hello = require('./build/Release/hello.node');
console.log(hello.greeting());
運行該程序,將得到下面的輸出結果:
$ node index.js
Hello N-API!
若安裝了bindings
依賴,便可將const hello = require('./build/Release/hello.node');
修改爲const hello = require('bindings')('hello');
。
const hello = require('bindings')('hello');
console.log(hello.greeting());
結束語
對於Node.js Native擴展模塊的開發,除了使用N-API提供的API以外,還可選擇nodejs/nan或者nodejs/node-addon-api。
N-API提供的接口爲純C的風格,對於C++開發者可選用node-addon-api,其在N-API的基礎上提供了C++對象模型以及異常處理。