通過N-API使用C/C++開發Node.js Native模塊

什麼是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-toolsnpm 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_namesources兩個屬性。target_name定義了該Native包的名稱,sources定義了需要編譯的文件。

對於gyp文件的更多配置,可參考nodejs/node-gypGYP 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++對象模型以及異常處理。

參考資料

  1. N-API - Node.js v12 Documentation
  2. node-addon-examples - GitHub
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章