Tìm hiểu về C++ addon trong Nodejs

2017-10-29

Node.js vốn là một nền tảng chạy trên môi trường V8 JavaScript, một trình thông dịch JavaScript vốn nằm trong trình duyệt Chrome (mình vẫn thích SpiderMonkey của Firefox hơn <(“) ), engine này vốn được viết từ C++, nó sẽ compile JavaScript trực tiếp thành native code thay vì interpreting bytecode, điều này cho ta tốc độ nhanh hơn kha khá, nhưng ta vẫn có thể tăng tốc và tối ưu ứng dụng nhanh hơn nữa bằng việc dùng C++.

Thư viện

Ta cần sử dụng node-gyp, đây là một build tool dùng để compile native addon modules cho Nodejs:

npm install -g node-gyp

Bắt đầu

Init một cái project mới:

npm init -y

Ta sẽ thử viết một cái addon tên là hw.cc để in ra dòng chữ “Hello world” đơn giản xem sao:

#include <node.h>

namespace hw {

  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Object;
  using v8::String;
  using v8::Value;

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

  void init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "hw", Method);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, init)

}

Lưu ý rằng tất cả các addon đều phải tuân theo cái pattern này:

void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

Giống như trên thì đoạn:

void init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hw", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, init)

Cũng giống như:

module.exports = hw;

trong JavasScript vậy.

Build

Nodejs không thể require() file .cc vào file javascript được, mà nó phải dịch sang mã máy (cái đống 01100101011001010…) thành file có cái đuôi .node đã.
Đầu tiên, ta tạo file binding.gyp ở root folder của project, nó chính là 1 file dạng như JSON vậy, toàn bộ thông tin trong này sẽ là config cho node-gyp mà mình vừa install trên compile

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "hw.cc" ]
    }
  ]
}

Như ở trên, mình taọ một target với target name là “addon” (tên file sẽ được compile ra) và sources là file hw.cc nãy mình vừa mới code

Sau đó chạy:

node-gyp configure build

alt text

Mặc định của node-gyp là addon của mình sẽ nằm trong folder build/Release

Ta tạo file index.js và require cái addon ấy vào:

var addon = require('./build/Release/addon')

console.log(addon.hw())

Và đây là kết quả:

alt text

Argument

Ở trên ta đã viết được một addon đơn giản, nhưng nếu ta muốn truyền argument từ JavaScript truyền sang C++ thì sao, điều này khá dễ dàng, các argument đều được truyền từ const FunctionCallbackInfo<Value>& args, ta thử một ví dụ đơn giản để tính tổng hai số truyền vào:

#include <node.h>

namespace hw {

  using v8::Exception;
  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Number;
  using v8::Object;
  using v8::String;
  using v8::Value;

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

    // Cộng 2 argument lại với nhau
    double total = args[0]->NumberValue() + args[1]->NumberValue();
    Local<Number> num = Number::New(isolate, total);

    args.GetReturnValue().Set(num);
  }

  void Init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "sum", Sum);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}

Build

alt text

Ta sẽ thử truyền 2 số vào:

var addon = require('./build/Release/addon')

console.log(addon.sum(6, 9))

Và đây là kết quả:

alt text

Tốc độ giữa C++ và JavaScript

Mặc dù 2 thằng này, một thằng là Static Language, thằng kia là Dynamic Language thì ai cũng biết rõ rồi nhưng mình vẫn muốn thử xem tốc độ của 2 thằng như thế nào, bằng cách lặp từ 0 -> 100000000:

C++:

#include <node.h>

namespace sum {

  using v8::Exception;
  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Number;
  using v8::Object;
  using v8::String;
  using v8::Value;

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

    int a = 1, b = 2;
    for (int i = 0; i < 100000000; i++) {
      a += b;
    }

    Local<Number> total = Number::New(isolate, a);
    args.GetReturnValue().Set(total);
  }

  void Init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "cpp", Sum);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}

JavaScript:

var addon = require('./build/Release/addon')

function js () {
    let a = 1, b = 2;
    for (let i = 0; i < 100000000; i++) {
      a += b;
    }
    let total = a;
    return a;
}

console.time('c++')
let a = addon.cpp()
console.log(a)
console.timeEnd('c++')

console.time('js')
let b = js()
console.log(b)
console.timeEnd('js')

Kết quả:
alt text

Ta thấy C++ nhanh hơn gần 90 lần so với JavaScript. :))

Tổng kết

Còn rất nhiều thứ như Callbacks, Object factory,… mình sẽ giới thiệu sau. Mặc dù sử dụng C++ khá là phức tạp vì nó khá khó so với JavaScript nhưng đối với ai muốn tối ưu tốc độ app của mình thì dùng C++ sẽ là một giải pháp khá tốt để giải quyết vấn đề này.