在Node.js中使用C++模組

counterxing發表於2018-05-11

JavaScript程式設計師來說,Node.js確實是我們作為服務端開發的首選語言。Node.js的效能優勢源於其使用GoogleV8引擎,使用非阻塞式的I / O模型,依靠事件驅動。但涉及密集型計算的場景時,Node.js不一定能夠有很優秀的表現。還好有C++ Addons的機制,能夠使得我們編寫原生的C++模組,並且能夠在Node.js中呼叫它。

為何要使用C++模組

  • C++社群龐大,我想在我們現成的Node.js應用中使用某個C++模組。
  • 密集型計算場景,並且對效能有極大要求。

舉個例子:Fabonacci

斐波那契數列通常解法是以遞迴地方式來完成,在這裡,為了體現Node.js中呼叫C++模組的優勢,我們並不在Fabonacci中使用快取的機制。

Node.js中,根據Fabonacci定義,我們編寫了如下程式碼,fabonacci.js

// fabonacci.js
function fabonacciNodeJS(n) {
  if (n === 0) {
    return 0;
  }
  if (n === 1) {
    return 1;
  }
  return fabonacciNodeJS(n - 1) + fabonacciNodeJS(n - 2);
}

function TestFabonnacci(func, env, n) {
  const start = (new Date()).getTime();
  const result = func(n);
  const end = (new Date()).getTime();
  console.log(`fabonacci(${n}) run in ${env} result is ${result}, cost time is ${end - start} ms.`);
}

TestFabonnacci(fabonacciNodeJS, 'Native Node.js', 40);
複製程式碼

可以在命令列中執行這一段程式,結果如下:

fabonacci(40) run in Native Node.js result is 102334155, cost time is 1125 ms.
複製程式碼

為了體現密集型計算場景時在Node.js中使用C++擴充模組的優勢,我根據C++ Addons編寫了如下程式碼,fabonacci.cc

// fabonacci.cc
#include <node.h>

namespace fabonacci {

  using namespace v8;

  static inline size_t runFabonacci(size_t n) {
    if (n == 0)
    {
      return 0;
    }
    if (n == 1)
    {
      return 1;
    }
    return runFabonacci(n - 1) + runFabonacci(n - 2);
  }

  static void Fabonacci(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    // 檢查引數型別
    if (!args[0]->IsNumber())
    {
      isolate->ThrowException(Exception::Error(String::NewFromUtf8(isolate, "argument type must be Number")));
    }
    size_t n = args[0]->NumberValue();
    Local<Number> num = Number::New(isolate, runFabonacci(n));
    args.GetReturnValue().Set(num);
  }

  void init(Local<Object> exports, Local<Object> module) {
    NODE_SET_METHOD(module, "exports", Fabonacci);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, init)

}
複製程式碼

修改之前的fabonacci.js,測試以上C++擴充程式:

// fabonacci.js
const fabonacciCPP = require('./build/Release/fabonacci');

function fabonacciNodeJS(n) {
  if (n === 0) {
    return 0;
  }
  if (n === 1) {
    return 1;
  }
  return fabonacciNodeJS(n - 1) + fabonacciNodeJS(n - 2);
}

function TestFabonnacci(func, env, n) {
  const start = (new Date()).getTime();
  const result = func(n);
  const end = (new Date()).getTime();
  console.log(`fabonacci(${n}) run in ${env} result is ${result}, cost time is ${end - start} ms.`);
}

TestFabonnacci(fabonacciNodeJS, 'Native Node.js', 40);
TestFabonnacci(fabonacciCPP, 'C++ Addon', 40);

複製程式碼

執行上述程式,結果如下:

fabonacci(40) run in Native Node.js result is 102334155, cost time is 1120 ms.
fabonacci(40) run in C++ Addon result is 102334155, cost time is 587 ms.
複製程式碼

可以看到,在Node.js中呼叫C++擴充模組,計算n = 40的斐波那契數,速度快了接近一倍。

Hello World開始

現在,我們可以從書寫一個Hello World來介紹如何編寫一個C++擴充,並在Node.js模組中呼叫:

以下是一個使用C++ Addons編寫的一個Hello World模組,我們可以在Node.js程式碼中呼叫這一個模組。

#include <node.h>

namespace helloWorld {

  using namespace v8;

  static void HelloWorld(const FunctionCallbackInfo<Value>& args) {
    // isolate當前的V8執行環境,每個isolate執行環境相互獨立
    Isolate* isolate = args.GetIsolate();
    // 設定返回值
    args.GetReturnValue().Set(String::NewFromUtf8(isolate, "Hello, World!"));
  }

  static void init(Local<Object> exports, Local<Object> module) {
    // 設定module.exports為HelloWorld函式
    NODE_SET_METHOD(module, "exports", HelloWorld);
  }
  // 所有的 Node.js 外掛必須以以下形式模式的初始化函式
  NODE_MODULE(NODE_GYP_MODULE_NAME, init)

}
複製程式碼

以上C++程式碼相當於以下JavaScript程式碼:

module.exports.hello = () => 'world';
複製程式碼

首先,在工程根目錄下建立一個名為binding.gyp的檔案,如下:

{
  "targets": [
    {
      "target_name": "fabonacci",
      "sources": [ "fabonacci.cc" ]
    }
  ]
}
複製程式碼

binding.gyp使用一個類似JSON的格式來描述模組的構建配置。然後使用node-gyp把我們書寫的C++模組原始碼編譯為二進位制模組,我們可以使用sudo npm install -g node-gyp全域性安裝node-gyp

在專案根目錄下執行:

node-gyp configure
node-gyp build
複製程式碼

編譯構建成功之後,可執行檔案fabonacci.node會在專案根目錄下的/build/Release目錄下,我們可以在Node.js引入該模組:

const hello = require('./build/Release/hello');
console.log(hello()); // Hello, World!
複製程式碼

V8資料型別和JavaScript資料型別的轉換

V8資料型別轉換為JavaScript資料型別

根據v8文件使用v8::Local<v8::Value>宣告的資料將會被V8Garbage Collector管理。我們書寫如下的C++模組示例,在C++模組中宣告如下的V8型別的變數,並匯出給JavaScript模組使用:

#include <node.h>

namespace datas {
  using namespace v8;

  static void MyFunction(const FunctionCallbackInfo<Value> &args) {
    Isolate* isolate = args.GetIsolate();
    args.GetReturnValue().Set(String::NewFromUtf8(isolate, "MyFunctionReturn"));
  }

  static void Datas(const FunctionCallbackInfo<Value> &args) {
    Isolate* isolate = args.GetIsolate();

    // 宣告一個V8的Object型別的變數
    Local<Object> object = Object::New(isolate);
    // 宣告一個V8的Number型別的變數
    Local<Number> number = Number::New(isolate, 0);
    // 宣告一個V8的String型別的變數
    Local<String> string = String::NewFromUtf8(isolate, "string");
    // 宣告一個V8的Function型別的變數
    Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
    Local<Function> func = tpl->GetFunction();
    // 宣告一個V8的Array型別的變數
    Local<Array> array = Array::New(isolate);
    // 給array賦值
    for (int i = 0; i < 10; ++i)
    {
      array->Set(i, Number::New(isolate, i));
    }
    // 宣告一個V8的Boolean型別的變數
    Local<Boolean> boolean = Boolean::New(isolate, true);
    // 宣告一個V8的Undefined型別的變數
    Local<Value> undefined = Undefined(isolate);
    // 宣告一個V8的Null型別的變數
    Local<Value> nu = Null(isolate);
    // 設定函式的名稱
    func->SetName(String::NewFromUtf8(isolate, "MyFunction"));
    // 給物件賦值
    object->Set(String::NewFromUtf8(isolate, "number"), number);
    object->Set(String::NewFromUtf8(isolate, "string"), string);
    object->Set(String::NewFromUtf8(isolate, "function"), func);
    object->Set(String::NewFromUtf8(isolate, "array"), array);
    object->Set(String::NewFromUtf8(isolate, "boolean"), boolean);
    object->Set(String::NewFromUtf8(isolate, "undefined"), undefined);
    object->Set(String::NewFromUtf8(isolate, "null"), nu);
    args.GetReturnValue().Set(object);
  }

  static void init(Local<Object> exports, Local<Object> module) {
    NODE_SET_METHOD(module, "exports", Datas);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}
複製程式碼

使用node-gyp工具構建上述模組,在Node.js模組中引入:

const datas = require('./build/Release/datas');
console.log(datas());
複製程式碼

執行結果:

在Node.js中使用C++模組

JavaScript資料型別轉換為V8資料型別

例如我們在引數中傳入了一個Number資料型別的JavaScript變數,可以使用v8::Number::Cast方法將JavaScript資料型別轉換為V8資料型別,我們建立瞭如下模組factory.cc,一個工廠模式建立物件的示例:

#include <node.h>

namespace factory {
  using namespace v8;

  static void Factory(const FunctionCallbackInfo<Value> &args) {
    Isolate* isolate = args.GetIsolate();
    Local<Object> object = Object::New(isolate);

    object->Set(String::NewFromUtf8(isolate, "name"), Local<String>::Cast(args[0])); // Cast方法實現JavaScript轉換為V8資料型別
    object->Set(String::NewFromUtf8(isolate, "age"), Local<Number>::Cast(args[1])); // Cast方法實現JavaScript轉換為V8資料型別
    args.GetReturnValue().Set(object);
  }

  static void init(Local<Object> exports, Local<Object> module) {
    NODE_SET_METHOD(module, "exports", Factory);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}
複製程式碼

呼叫上述模組:

const factory = require('./build/Release/factory');
console.log(factory('counter', 21)); // { name: 'counter', age: 21 }
複製程式碼

關於其它型別的Cast呼叫,可查閱V8文件

相關文章