如何寫一個簡單的node.js c++擴展

node 是由 c++ 編寫的,核心的 node 模塊也都是由 c++ 代碼來實現,因此一樣 node 也開放了讓使用者編寫 c++ 擴展來實現一些操做的窗口。
若是你們對於 require 函數的描述還有印象的話,就會記得若是不寫文件後綴,它是有一個特定的匹配規則的:
LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP

能夠看到,最後會匹配一個 .node,然後邊的描述也表示該後綴的文件爲一個二進制的資源。
而這個 .node 文件通常就會是咱們所編譯好的 c++ 擴展了。javascript

爲何要寫 c++ 擴展

能夠簡單理解爲,若是想基於 node 寫一些代碼,作一些事情,那麼有這麼幾種選擇:前端

  1. 寫一段 JS 代碼,而後 require 執行
  2. 寫一段 c++ 代碼,編譯後 require 執行
  3. 打開 node 源碼,把你想要的代碼寫進去,而後從新編譯

平常的開發其實只用第一項就夠了,咱們用本身熟悉的語言,寫一段熟悉的代碼,而後發佈在 NPM 之類的平臺上,其餘有相同需求的人就能夠下載咱們上傳的包,而後在TA的項目中使用。
但有的時候可能純粹寫 JS 知足不了咱們的需求,也許是工期趕不上,也許是執行效率不讓人滿意,也有多是語言限制。
因此咱們會採用直接編寫一些 c++ 代碼,來建立一個 c++ 擴展讓 node 來加載並執行。
何況若是已經有了 c++ 版本的輪子,咱們經過擴展的方式來調用執行而不是本身從頭實現一套,也是避免重複造輪子的方法。 java

一個簡單的例子,若是你們接觸過 webpack 而且用過 sass 的話,那麼在安裝的過程當中極可能會遇到各類各樣的報錯問題,也許會看到 gyp 的關鍵字,其實緣由就是 sass 內部有使用一些 c++ 擴展來輔助完成一些操做,而 gyp 就是用來編譯 c++ 擴展的一種工具。 node

image

https://github.com/sass/node-sasswebpack

固然,上邊也提到了還有第三種操做方法,咱們能夠直接魔改 node 源碼,可是若是你只是想要寫一些原生 JS 實現起來沒有那麼美好的模塊,那麼是沒有必要去魔改源碼的,畢竟改完了之後還要編譯,若是其餘人須要用你的邏輯,還須要安裝你所編譯好的特殊版本。
這樣的操做時很不易於傳播的,你們不會想使用 sass 就須要安裝一個 sass 版本的 node 吧。
就像爲了看星戰還要專門下載一個優酷- -。 c++

簡單總結一下,寫 c++ 的擴展大概有這麼幾個好處:git

  1. 能夠複用 node 的模塊管理機制
  2. 有比 JS 更高效的執行效率
  3. 有更多的 c++ 版本的輪子能夠拿來用

怎麼去寫一個簡單的擴展

node 從問世到如今已經走過了 11 年,經過早期的資料、博客等各類信息渠道能夠看到以前開發一個 c++ 擴展並非很容易,但通過了這麼些年迭代,各類大佬們的努力,咱們再去編寫一個 c++ 擴展已是比較輕鬆的事情了。
這裏直入正題,放出今天比較關鍵的一個工具:node-addon-api module
以及這裏是官方提供的各類簡單 demo 來讓你們熟悉這是一個什麼樣的工具: node-addon-examples github

須要注意的一點是, demo 目錄下會分爲三個子目錄,在 readme 中也有寫,分別是三種不一樣的 c++ 擴展的寫法(基於不一樣的工具)。
咱們本次介紹的是在 node-addon-api 目錄下的,算是三種裏邊最爲易用的一種了。 web

首先是咱們比較熟悉的 package.json 文件,咱們須要依賴兩個組件來完成開發,分別是 bindingsnode-addon-api算法

而後咱們還須要簡單瞭解一下 gyp 的用法,由於編譯一個 c++ 擴展須要用到它。
就像 helloworld 示例中的 binding.gyp 文件示例:

{
  "targets": [
    {
      // 導出的文件名
      "target_name": "hello",
      // 編譯標識的定義 禁用異常機制(注意感嘆號表示排除過濾)
      "cflags!": [ "-fno-exceptions" ],
      // c++ 編譯標識的定義 禁用異常機制(注意感嘆號表示排除過濾,也就是 c++ 編譯器會去除該標識)
      "cflags_cc!": [ "-fno-exceptions" ],
      // 源碼入口文件
      "sources": [ "hello.cc" ],
      // 源碼包含的目錄
      "include_dirs": [
        // 這裏表示一段 shell 的運行,用來獲取 node-addon-api 的一些參數,有興趣的老鐵能夠自行 node -p "require('node-addon-api').include" 來看效果
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      // 環境變量的定義
      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}

gyp 的語法挺多的,此次並非單獨針對 gyp 的一次記錄,因此就不過多的介紹。

從最簡單的數字相加來實現

而後咱們來實現一個簡單的建立一個函數,讓兩個參數相加,並返回結果。

源碼位置: https://github.com/Jiasm/node...

咱們須要這樣的一個 binding.gyp 文件:

{
  "targets": [
    {
      "target_name": "add",
      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ],
      "sources": [ "add.cc" ],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}

而後咱們在項目根目錄建立 package.json 文件,並安裝 bindingsnode-addon-api 兩個依賴。

接下來就是去編寫咱們的 c++ 代碼了:

#include <napi.h>

// 定義 Add 函數
Napi::Value Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  // 接收第一個參數
  double arg0 = info[0].As<Napi::Number>().DoubleValue();
  // 接收第二個參數
  double arg1 = info[1].As<Napi::Number>().DoubleValue();
  // 將兩個參數相加並返回
  Napi::Number num = Napi::Number::New(env, arg0 + arg1);

  return num;
}

// 入口函數,用於註冊咱們的函數、對象等等
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  // 將一個名爲 add 的函數掛載到 exports 上
  exports.Set(Napi::String::New(env, "add"), Napi::Function::New(env, Add));
  return exports;
}

// 固定的宏使用
NODE_API_MODULE(addon, Init)

在 c++ 代碼完成之後就是須要用到 node-gyp 的時候了,建議全局安裝 node-gyp,避免一個項目中出現多個 node_modules 目錄的時候使用 npx 會出現一些不可預料的問題:

> npm i -g node-gyp
# 生成構建文件
> node-gyp configure
# 構建
> node-gyp build

這時候你會發現項目目錄下已經生成了一個名爲 add.node 的文件,就是咱們在 binding.gyp 裏邊的 target_name 所設置的值了。
最後咱們就是要寫一段 JS 代碼來調用所生成的 .node 文件了:

const { add } = require('bindings')('add.node')

console.log(add(1, 2))     // 3
console.log(add(0.1, 0.2)) // 熟悉的 0.3XXXXX

實現一個函數柯里化

接下來咱們來整點好玩的,實現一個前端的高頻考題,如何實現一個函數柯里化,定義以下:

add(1)(2)(3) // => 6
add(1, 2, 3) // => 6
源碼位置: https://github.com/Jiasm/node...

咱們會用到的一些技術點:

  • 如何在 c++ 函數中返回一個函數供 JS 調用
  • 如何讓返回值既支持函數調用又支持取值操做
  • 如何處理非固定數量的參數(其實這個很簡單了,從上邊也能看出來,自己就是一個數組)

再也不贅述 binding.gyp 與 package.json 的配置,咱們直接上 c++ 代碼:

#include <napi.h>

// 用來覆蓋 valueOf 實現的函數
Napi::Value GetValue(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  // 獲取咱們在建立 valueOf 函數的時候傳入的 result
  double* storageData = reinterpret_cast<double*>(info.Data());

  // 避免空指針狀況
  if (storageData == NULL) {
    return Napi::Number::New(env, 0);
  } else {
    return Napi::Number::New(env, *storageData);
  }

}

Napi::Function CurryAdd(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  // 獲取咱們下邊在建立 curryAdd 函數的時候傳入的 result
  double* storageData = reinterpret_cast<double*>(info.Data());

  double* result = new double;

  // 遍歷傳入的全部參數
  long len, index;
  for (len = info.Length(), index = 0; index < len; index++) {
    double arg = info[index].As<Napi::Number>().DoubleValue();

    *result += arg;
  }

  // 用於屢次的計算
  if (storageData != NULL) {
    *result += *storageData;
  }

  // 建立一個新的函數用於函數的返回值
  Napi::Function fn = Napi::Function::New(env, CurryAdd, "curryAdd", result);

  // 篡改 valueOf 方法,用於輸出結果
  fn.Set("valueOf", Napi::Function::New(env, GetValue, "valueOf", result));

  return fn;
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  Napi::Function fn = Napi::Function::New(env, CurryAdd, "curryAdd");

  exports.Set(Napi::String::New(env, "curryAdd"), fn);

  return exports;
}

NODE_API_MODULE(curryadd, Init)

編譯完成之後,再寫一段簡單的 JS 代碼來調用驗證結果便可:

const { curryAdd } = require('bindings')('curry-add');

const fn = curryAdd(1, 2, 3);
const fn2 = fn(4);

console.log(fn.valueOf())     // => 6
console.log(fn2.valueOf())    // => 10
console.log(fn2(5).valueOf()) // => 15

而後能夠講一下上邊列出來的三個技術點是如何解決的:

  • 如何在 c++ 函數中返回一個函數供 JS 調用

    • 經過 Napi::Function::New 建立新的函數,並將計算結果存入函數能夠獲取到的地方供下次使用
  • 如何讓返回值既支持函數調用又支持取值操做

    • 經過 fn.Set 篡改 valueOf 函數並返回結果
  • 如何處理非固定數量的參數(其實這個很簡單了,從上邊也能看出來,自己就是一個數組)

    • 經過拿到 infoLength 來遍歷獲取

與 JS 進行對比

固然,就例如柯里化之類的函數,拿JS來實現的話會很是簡單,配合 reduce 函數基本上五行之內就能夠寫出來。
那咱們折騰這麼多到底是爲了什麼呢?
這就要回到開頭所說的優點了: 執行效率

採用冒泡排序來對比

爲了證實效率的差別,咱們選擇用一個排序算法來驗證,採用了最簡單易懂的冒泡排序來作,首先是 JS 版本的:

源碼位置: https://github.com/Jiasm/node...
function bubble (arr) {
  for (let i = 0, len = arr.length; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] < arr[j]) {
        [arr[i], arr[j]] = [arr[j], arr[i]]
      }
    }
  }

  return arr
}

bubble([7, 2, 1, 5, 3, 4])

而後是咱們的 c++ 版本,由於是一個 JS 的擴展,因此會涉及到數據類型轉換的問題,大體代碼以下:

#include <napi.h>

void bubbleSort(double* arr, int len) {
  double temp;
  int i, j;
  for (i = 0; i < len; i++) {
    for (j = i + 1; j < len; j++) {
      if (*(arr + i) < *(arr + j)) {
        temp = *(arr + i);
        *(arr + i) = *(arr + j);
        *(arr + j) = temp;
      }
    }
  }
}

Napi::Value Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  Napi::Array array = info[0].As<Napi::Array>();


  int len = array.Length(), i;

  // 返回值
  Napi::Array arr = Napi::Array::New(env, len);

  double* list = new double[len];

  // 將 Array 轉換爲 c++ 可方便使用的 double 數組
  for (i = 0; i < len; i++) {
    Napi::Value i_v = array[i];

    list[i] = i_v.ToNumber().DoubleValue();
  }

  // 執行排序
  bubbleSort(list, len);

  // 將 double 數組轉換爲要傳遞給 JS 的數據類型
  for (i = 0; i < len; i++) {
    arr[i] = Napi::Number::New(env, list[i]);
  }

  return arr;
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "bubble"), Napi::Function::New(env, Add));
  return exports;
}

NODE_API_MODULE(bubble, Init)

而後咱們經過一個隨機生成的數組來對比耗時:

const { bubble } = require('bindings')('bubble.node')

const arr = Array.from(new Array(1e3), () => Math.random() * 1e6 | 0)

console.time('c++')
const a = bubble(arr)
console.timeEnd('c++')

function bubbleJS (arr) {
  for (let i = 0, len = arr.length; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] < arr[j]) {
        [arr[i], arr[j]] = [arr[j], arr[i]]
      }
    }
  }

  return arr
}

console.time('js')
bubbleJS(arr)
console.timeEnd('js')

1,000 數據量的時候耗時差距大概在 6 倍左右,在 10,000 數據量的時候耗時差距大概在 3 倍左右。
也是簡單的證明了在相同算法狀況下 c++ 效率確實是會比 JS 高一些。

固然了,也經過上邊的 bubble sort 能夠來證明另外一個觀點: 有更多的 c++ 版本的輪子能夠拿來用
就好比上邊的 bubbleSort 函數,可能就是一個其餘的加密算法實現、SDK 封裝,若是沒有 node 版本,而咱們要使用就須要參考它的邏輯從新實現一遍,但若是採用 c++ 擴展的方式,徹底能夠基於原有的 c++ 函數進行一次簡單的封裝就擁有了一個 node 版本的 函數/SDK。

後記

上邊的一些內容就是如何使用 node-addon-api 來快速開發一個 c++ 擴展,以及如何使用 node-gyp 進行編譯,還有最後的如何使用 JS 調用 c++ 擴展。
在開發 node 程序的過程當中,若是可以適當的利用 c++ 的能力是會對項目有很大的幫助的,在一些比較關鍵的地方,亦或者 node 弱項的地方,使用更鋒利的 c++ 來幫助咱們解決問題。
不要讓編程語言限制了你的想象力

參考資料

相關文章
相關標籤/搜索