napi cmake 将c++库编译为node包

napi cmake 编译node包

使用napi提供的方法把cmake c++项目打成node包,提供给javaScript用require引用

环境准备

第一步: 下载cmake-js
1
 npm i cmake-js -g
第一步: 改造CMakeLists.txt文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
cmake_minimum_required(VERSION 3.9)

# 0042索引对应的策略使用新策略,具体策略未知
cmake_policy(SET CMP0042 NEW)
set (CMAKE_CXX_STANDARD 11)

project (build-node-addon-api-with-cmake)

# conan相关, 原项目的内容,不必理会, 如果不用conan,请删除下面两行
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

# 使用utf-8解析代码, 原项目的内容
if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc /utf-8")
endif()

# napi 头文件目录
include_directories(${CMAKE_JS_INC})

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SRC)

# 生成库时增加了cmake-js的东西
add_library(${PROJECT_NAME} SHARED ${SRC} ${CMAKE_JS_SRC})

# SUFFIX 导入.node后缀的库,而不仅仅是.dll
# PREFIX 导入库的前缀,而不仅仅是lib
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")

# 链接cmake-js的库和conan的库,如果不用conan不要加 ${CONAN_LIBS}
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB} ${CONAN_LIBS})

# 打印命令结果到变量中,命令会返回addon的c头文件目录
execute_process(COMMAND node -p "require('node-addon-api').include"
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        OUTPUT_VARIABLE NODE_ADDON_API_DIR
        )
#去除换行符
string(REGEX REPLACE "[\r\n"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})

# 给该target指定头文件发现目录
target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR})

# define NPI_VERSION
add_definitions(-DNAPI_VERSION=3)
第三步: 增加package.json文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{<!-- -->
  "name": "build-node-addon-api-with-cmake",
  "version": "0.0.0",
  "description": "Build N-API native addon with CMake and node-addon-api C++ wrapper.",
  "main": "hello.js",
  "private": true,
  "dependencies": {<!-- -->
    "bindings": "~1.2.1",
    "node-addon-api": "^1.0.0"
  },
  "scripts": {<!-- -->
    "install": "cmake-js compile",
    "test": "node hello.js"
  }
}
第四步: 增加给javaScript调用的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <napi.h>

#include "test.h"

static Napi::String MMethod(const Napi::CallbackInfo& info) {<!-- -->
 Napi::Env env = info.Env();
 return Napi::String::New(env, PrintHello());
}

static Napi::Boolean GetBool(const Napi::CallbackInfo& info) {<!-- -->
   Napi::Env env = info.Env();
   Compair(0,0);
   if (info.Length() < 2) {<!-- -->
       Napi::TypeError::New(env, "Wrong number of arguments")
           .ThrowAsJavaScriptException();
       return Napi::Boolean::New(env, Compair(0,2));
   }
   std::string arg0 = info[0].As<Napi::String>();
   std::string arg1 = info[1].As<Napi::String>();

   return Napi::Boolean::New(env, Compair(arg0,arg1));
}


static Napi::Object Init(Napi::Env env, Napi::Object exports) {<!-- -->
 exports.Set(Napi::String::New(env, "gethello"),
             Napi::Function::New(env, MMethod));
 exports.Set(Napi::String::New(env, "getbool"),
             Napi::Function::New(env, GetBool));
 return exports;
}

NODE_API_MODULE(addon, Init)

其中CNODE_API_MODULE(addon, Init)这一行, 第一个参数可以随便写,应该是暴露出去的类名, 第二个和Init函数同名, Init函数中调用exports.Set可以把想要暴露的接口和变量注册进去。

前面都是函数的定义:

第一个MMethod函数,接收0个参数,返回一个字符串,这个字符串是通过test.h中的c++函数PrintHello()生成的。
第二个函数GetBool接收两个字符串参数,如果参数不足2个,会向javaScript抛出一个异常,如果参数满足条件,会调用c++的Compair返回一个bool值。

需要注意的是,在c++函数中获取javaScript参数的方法:

如果是字符串, 直接使用std::string arg0 = info[0].As()获取就可以了。
但是如果是其他类型,如double, 就需要用下面的方式获取

1
double arg0 = info[0].As<Napi::Number>().DoubleValue()

获取数字类型,有两个步骤,第一个是获取到Napi::Number类型,然后在调用DoubleValue()转换为c++的double类型。Napi::Number定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  /// A JavaScript number value.
   class Number : public Value {<!-- -->
      public:
           static Number New(
                   napi_env env, ///< N-API environment
                   double value  ///< Number value
                   );

           Number();                               ///< Creates a new _empty_ Number instance.
           Number(napi_env env, napi_value value); ///< Wraps a N-API value primitive.

           operator int32_t() const;  ///< Converts a Number value to a 32-bit signed integer value.
           operator uint32_t() const; ///< Converts a Number value to a 32-bit unsigned integer value.
           operator int64_t() const;  ///< Converts a Number value to a 64-bit signed integer value.
           operator float() const;    ///< Converts a Number value to a 32-bit floating-point value.
           operator double() const;   ///< Converts a Number value to a 64-bit floating-point value.

           int32_t Int32Value() const;   ///< Converts a Number value to a 32-bit signed integer value.
           uint32_t Uint32Value() const; ///< Converts a Number value to a 32-bit unsigned integer value.
           int64_t Int64Value() const;   ///< Converts a Number value to a 64-bit signed integer value.
           float FloatValue() const;     ///< Converts a Number value to a 32-bit floating-point value.
           double DoubleValue() const;   ///< Converts a Number value to a 64-bit floating-point value.
   };

还有一些特殊的参数,比如数组,对象,函数。数组和对象这里不做过多介绍,函数参数可以实现c++的回调函数,可以达到c++调用javaScript的目的,这里简单讲一下用法。

c++文件:

1
2
3
4
5
6
7
//这个函数接收一个javaScript的函数,参数,并且立即调用,
// 也可以将函数参数保持到c++的变量中,在需要的时机调用
void RunCallback(const Napi::CallbackInfo& info) {<!-- -->
 Napi::Env env = info.Env();
 Napi::Function cb = info[0].As<Napi::Function>();
 cb.Call(env.Global(), {<!-- -->Napi::String::New(env, "hello world callback")});
}

javascript文件:

1
2
3
addon.runCallback(function(msg){<!-- -->
 console.log(msg);
});
第五步: 写一个hell.js测试
1
2
3
4
var addon = require('./build/lib/build-node-addon-api-with-cmake');

console.log(addon.gethello()); // 'world'
console.log(addon.getbool('111', '111')); // 'world'

其中require的路径,就看你产物的目录了,你可以把产物迁移到其他目录

第六步: 编译运行
1
2
3
npm i // 下载package.josn中声明的js依赖
cmake-js build //构建, 或者执行 npm run install
node hello.js //运行js测试代码, 或者执行 npm run test

构建输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
?  cpp_to_nodejs git:(master) ? npm run install                                                                                          [20/463]
                                                                                                                                                 
> [email protected] install /home/打码/work/cpp_to_nodejs                                                                    
> cmake-js compile                                                                                                                              
                                                                                                                                                 
[                                                                                                                                                
  '/node/v14.5.0/bin/node',                                                                                              
  '/home/打码/.nvm/versions/node/v14.5.0/bin/cmake-js',                                                                                          
  'compile'                                                                                                                                      
]                                                                                                                                                
info TOOL Using Unix Makefiles generator.                                                                                                        
info CMD CONFIGURE                                                                                                                              
info RUN cmake "/home/打码/work/cpp_to_nodejs" --no-warn-unused-cli -G"Unix Makefiles" -DCMAKE_JS_VERSION="6.1.0" -DCMAKE_BUILD_TYPE="Release" -DC
MAKE_LIBRARY_OUTPUT_DIRECTORY="/home/打码/work/cpp_to_nodejs/build/Release" -DCMAKE_JS_INC="/home/打码/.cmake-js/node-x64/v14.5.0/include/node" -DC
MAKE_JS_SRC="" -DNODE_RUNTIME="node" -DNODE_RUNTIMEVERSION="14.5.0" -DNODE_ARCH="x64"                                                            
Not searching for unused variables given on the command line.                                                                                    
-- The C compiler identification is GNU 10.2.0                                                                                                  
-- The CXX compiler identification is GNU 10.2.0                                                                                                
-- Detecting C compiler ABI info                                                                                                                
-- Detecting C compiler ABI info - done                                                                                                          
-- Check for working C compiler: /usr/bin/cc - skipped                                                                                          
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Conan: Adjusting output directories
-- Conan: Using cmake global configuration
-- Conan: Adjusting default RPATHs Conan policies
-- Conan: Adjusting language standard
-- Current conanbuildinfo.cmake directory: /home/打码/work/cpp_to_nodejs/build
-- Conan: Compiler GCC>=5, checking major version 10
-- Conan: Checking correct version: 10
-- Configuring done
-- Generating done
-- Build files have been written to: /home/打码/work/cpp_to_nodejs/build
info CMD BUILD
info RUN cmake --build "/home/打码/work/cpp_to_nodejs/build" --config Release
Scanning dependencies of target build-node-addon-api-with-cmake
[ 33%] Building CXX object CMakeFiles/build-node-addon-api-with-cmake.dir/src/test.cpp.o
[ 66%] Building CXX object CMakeFiles/build-node-addon-api-with-cmake.dir/src/test_node_api.cpp.o
[100%] Linking CXX shared library lib/build-node-addon-api-with-cmake.node
[100%] Built target build-node-addon-api-with-cmake

测试demo输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
?  cpp_to_nodejs git:(master) ? npm run test  

> [email protected] test /home/打码/work/cpp_to_nodejs
> node hello.js

11:05:11.194 [thread 47105][info] [test::main][PrintHello:14]: logger sink flags: 1
11:05:11.194 [thread 47105][info] [test::main][PrintHello:15]: echo hello world!
11:05:11.194 [thread 47105][info] [test::main][PrintHello:16]: json string {<!-- -->"pi":3.14}
hello world!
11:05:11.199 [thread 47105][info] [test::main][Compair:28]: arg1: 0.0
11:05:11.199 [thread 47105][info] [test::main][Compair:29]: arg2: 0.0
11:05:11.199 [thread 47105][info] [test::main][Compair:22]: arg1: 111
11:05:11.199 [thread 47105][info] [test::main][Compair:23]: arg2: 111
true

输出的带时间戳的日志是c++打印的, hello world!true是js打印的。

总结

总体来看,还是比较简单的,比其使用x86架构交叉编译arm架构,或者交叉编译安卓,这种编译node的包的方法会简单很多。