【Node】Addon C++模組開發

6gallon發表於2019-02-16

Node Addon

Node Addon是官方的C++模組開發途徑,可實現Node與底層技術結合,如驅動、作業系統等。

開發環境

需要安裝

  • node v8.11.1
  • npm v5.6.0
  • node-gyp
  • python 2.7
  • VS2017 Community C/C++桌面開發(或 windwos-build-tools)
  • VSCode 或其他IDE

安裝步驟

  • 1.安裝node、npm(略)
  • 2.安裝python 2.7(略)
# npm中設定python環境變數
npm config set python=C:\Python27\python.exe
  • 3.安裝VS2017 Community C/C++桌面開發(略)
# npm設定vs版本環境變數
npm config set msvs_version=2017
  • 4.安裝node-gyp
# 全域性安裝node-gyp
npm install -g node-gyp
  • 5.VSCode 或其他IDE(略)

Hello World

從建立一個空專案資料夾開始

mkdir helloworld-addon
cd helloworld-addon
npm init
# 一頓回車
# 安裝addon開發所需要的基礎模組
npm install --save nan bindings

開始開發C++模組

建立一個原始檔![hello.cc](), 編寫如下程式碼:

// @file: hello.cc
// @author: jialun.liu
// @date: 2018-09-29

#include <nan.h>
#include <string.h>
#include <stdio.h>

using namespace std;
using namespace Nan;
using namespace v8;

void hello(const Nan::FunctionCallbackInfo<v8::Value> &info)
{
    if (info.Length() < 1 || !info[0]->IsString())
    {
        Nan::ThrowTypeError("Illegal argument: hello(`hello`)");
        return;
    }

    v8::String::Utf8Value input(info[0]->ToString());
    std::string output(*input, input.length());
    output = output + " world";
    // create string and return
    Local<v8::String> str = Nan::New(output).ToLocalChecked();
    info.GetReturnValue().Set(str);
}

void Init(v8::Local<v8::Object> exports)
{
    // export functions,like in nodejs: module.exports.hello = hello
    exports->Set(Nan::New("hello").ToLocalChecked(),
                 Nan::New<v8::FunctionTemplate>(hello)->GetFunction());
}

// self registry
NODE_MODULE(hello, Init)

編譯配置

建立編譯配置檔案binding.gyp,類似Makefile,注意這是一個python格式檔案

# -*- coding: UTF-8 -*-

# @file:binding.gyp
# @author:jialun.liu
# @date:2018-9-29
{
    # 跨作業系統配置,如不同作業系統可能連結不同的庫,使用不同的編譯引數
    "conditions":[
        [
            `OS=="linux"`,
            {
                "targets":[{
                    # 編譯後模組名稱
                    "target_name":"hello",
                    # 原始檔,如有多個一一列舉
                    "source":["hello.cc"],
                    "include_dirs":[
                        # 這裡加入我們需要查詢的標頭檔案路徑,nan是必須的
                        "<!(node -e "require(`nan`)")"
                    ],
                    "link_settings":{
                        # 連結庫設定,如我們需要連結其他的動態庫、靜態庫
                        "libraries":[
                            # 與gcc編譯引數一樣,區別是要指定連結庫完整路徑,網上說使用-L引數可指定libpath,我試過不好使
                            # <(module_root_dir)是指模組路徑,這樣模組在被別的模組安裝時就可以找到需要的庫了
                            # "-l<(module_root_dir)/lib/libXXX.so"
                        ]
                    }
                }]
            }
        ],[
            `OS=="win"`,
            {
                "targets":[{
                    # 編譯後模組名稱
                    "target_name":"hello",
                    # 原始檔,如有多個一一列舉
                    "source":["hello.cc"],
                    "include_dirs":[
                        # 這裡加入我們需要查詢的標頭檔案路徑,nan是必須的
                        "<!(node -e "require(`nan`)")"
                    ],
                    "link_settings":{
                        # 連結庫設定,如我們需要連結其他的動態庫、靜態庫
                        "libraries":[
                            # 與gcc編譯引數一樣,區別是要指定連結庫完整路徑,網上說使用-L引數可指定libpath,我試過不好使
                            # <(module_root_dir)是指模組路徑,這樣模組在被別的模組安裝時就可以找到需要的庫了
                            # 與linux差別是不需要指定字尾名,會自動找XXX.lib,也是因為這個才需要區別作業系統
                            # "-l<(module_root_dir)/lib/XXX"
                        ]
                    }
                }]
            }
        ]
    ]
}

package.json

設定package.json,注意 “gypfile”:true“main:build/Release/hello”

{
    "name": "helloworld-addon",
    "version": "1.0.0",
    "description": "",
    "main": "build/Release/hello",
    "scripts": {
        "test": "node hello-test.js"
    },
    "gypfile": true,
    "author": "jialun.liu",
    "license": "ISC",
    "dependencies": {
        "bindings": "^1.3.0",
        "nan": "^2.11.0"
    }
}

編譯

node-gyp clean
node-gyp configure
node-gyp build

# 合併只需執行rebuild
node-gyp rebuild

編譯如果沒有錯誤就可以進行測試了

測試

建立測試hello-test.js

const hello = require(`bindings`)(`hello`);

let ret = hello.hello();
// should output: world
console.log(ret);

進階

Addon中C++與Node資料型別轉換問題比較麻煩,總結幾個比較常見的資料型別

String/char */std::string

// 建立NodeString
char * str = "some string";
v8::Local<v8::String> nodeStr = Nan::New(str).ToLocalChecked();

// 轉化NodeString
// 從引數中讀取
v8::String::Utf8Value nodeStr(info[0]->ToString()); 
// 轉為std::string
std::string stdStr(*nodeStr, nodeStr.length());
// 轉為char*
char * cStr = stdStr.c_str();

數值型別

包括浮點數、整數。不支援64位整數。

Number/double

// 建立
double d = 123456.789;
v8::Local<v8::Number> nd = Nan::New(d);

// 從引數讀取
double d = info[0]->NumberValue();

Int32/int、UInt32/unsigned int

// 建立
int i32 = 123456;
v8::Local<v8::Int32> ni32 = Nan::New(i32);
unsigned ui32 = 0xffffffff;
v8::Local<v8::Uint32> nui32 = Nan::New(ui32);

// 從引數讀取
int32_t i32 = info[0]->Int32Value();
uint32_t u32 = info[1]->Uint32Value();

Buffer/char *

// 建立長度為8的Buffer
v8::Local<v8::Object> buf = Nan::NewBuffer(8).ToLocalChecked();
// 獲取buffer指標
long long * bp =  node::Buffer::Data(buf);
// 操作指標
*bp = 0x12345678;

// 從引數讀取
char * p = (char *)node::Buffer::Data(info[0]->ToObject());

Array/XX[]

// 建立一個長度為10的陣列
v8::Local<Array> array = Nan::New<Array>(10);
// 為陣列賦值
Nan::Set(array, 0, Nan::New("first"));
Nan::Set(array, 1, Nan::New("second"));

// TODO: 從引數讀取
// v8::Local<Array> array = Nan::Cast<v8::Array>(info[0]->ToObject());

踩坑

  • 1.c++程式碼中不要出現中文註釋,否則編碼錯誤會導致意想不到的問題,比如莫名某些程式碼會不生效(是因為編碼問題導致編譯器識別換行錯誤,程式碼被認為註釋掉了,切記!!!)

https://github.com/liujialun/…

相關文章