一种在nodejs中调用c++的简单方法

背景:

    目前nodejs调用c++主流的有两种方法,分别是addons和ffi

    addons是nodejs官方的c++扩展实现方案,但是由于需要使用模版,并且要对v8引擎有一定的了解,入门门槛较高。

    ffi是nodejs直接调用so库的一种实现,可以调用纯c的接口。

    以上两种方法有一个共同的缺陷,就是当nodejs调用的时候,如果不能立即返回的话,c++代码会卡住单线程的js代码。

    在这种情况下,如果c++代码没有io操作而只是纯计算的话是没有问题的,但是如果有同步io操作,势必会降低nodejs的执行效率。这时的解决方案有两个思路

    1.在js代码中使用child_process模块创建一个子进程,子进程再调用c扩展(此时子进程会被扩展卡住),保证主进程可以充分利用cpu。

    2.js在c代码中注册回调,c代码中维护多线程,不让同步io操作卡住函数返回,在处理完成后调用回调函数

    这两种方案的实现成本都比较高,所以在c代码非纯cpu计算的情况下,笔者提出了一种方案,使得nodejs调用c的开发成本降低,并且能够保证js的单核利用率不受影响。


主体思路:

    通过child_process直接创建一个子进程,这个子进程是c的可执行程序,主进程(js)和子进程(c)通过管道进行通信,

    这样主进程在需要调用一个c函数的时候会向子进程发送一个消息,并不会卡住主进程js继续执行,在c程序执行完成之后将消息返回回来。

    举一个例子:我有一段c代码,这时一段机器学习代码,可能会要访问gpu,我想要用js去调用这段c代码,如果我简单的用addons来封装,做一个同步调用,那么在gpu进行运算的时候,js的线程会被卡住,cpu没有得到利用,如果是后台服务的话会导致并发处理能力降低。这时就非常适合用上面的方式创建一个子进程,js和c程序通过管道消息来协同工作。

    当然,这种模式是有适用范围的,前提是你的c程序的分离成子进程后通信的耗时相比于c执行的耗时在一个可接受的范围内。

    比如,如果你的c程序只是简单的逻辑运算,每一次计算都只要微秒级别的,但是由于创建了一个子进程,进程切换和通信带来了毫秒级别的代价,这时就不划算了。但是如上面的例子,如果你的c程序中出现了大量的io操作或者需要协处理器工作的情况,并且时间较高(100毫秒级别左右),这时进程切换和通信带来的开销就可以忽略。

实现:

    笔者为这种模式实现了一个简单的框架

    https://github.com/freelw/nodejscallc

    下面介绍一下使用方法

    在clone到本地之后你的目录结构应该是这样

一种在nodejs中调用c++的简单方法_第1张图片

 首先我们执行

npm install

来安装需要的依赖

之后我们需要定义一个接口描述文件,json格式

{
    "func_name" : "test",
    "req_params" : [
        {
            "name" : "param_long",
            "type" : "long"
        },
        {
            "name" : "param_string",
            "type" : "string"
        }
    ],
    "rsp_params" : [
        {
            "name" : "rsp_param_string",
            "type" : "string"
        },
        {
            "name" : "rsp_param_long",
            "type" : "long"
        }
    ]
}

这个接口描述文件主要分为3部分

1.func_name,表示你所要调用的函数名称

2.req_params, 是输入参数列表,列表中的每个元素有两个字段,name表示参数名,type表示参数类型,目前仅支持long和string

3.rsp_params,是返回参数列表,内容同2

在这个例子中,有两个输入参数 param_long long类型, param_string string类型

有两个输出参数rsp_param_string string类型, rsp_param_long long类型

我们把这个文件保存为desc.json

然后我们执行初始化工具来生成调用函数所需的js代码和c代码

node init.js -i desc.json

这时便会按照你的接口描述文件创建三个文件

一种在nodejs中调用c++的简单方法_第2张图片

由于func_name是test,所以生成的js和cpp文件的前缀是test

test_proxy.js是js的函数调用代理,这里的代码是用户完全不用修改的

test_imp.cpp是c函数的实现部分,用户需要实现test函数


在这里我们假设需要实现一个逻辑,将param_string从rsp_param_string输出

并且需要rsp_param_long=param_long+1

于是我们有以下代码


这时我们进入build/c/下面进行编译

cd build/c
make

这时会在release下面生成一些文件

一种在nodejs中调用c++的简单方法_第3张图片

test_proxy.js是刚才js文件夹下生成的,make执行是自动拷贝到此处

test是test_imp.cpp编译后生成到可执行文件

我们现在写一个测试脚本来调用以下我们写的函数

const test = new (require('./build/release/test_proxy'))();

test.do({
    param_long: 123,
    param_string: 'teststring',
}, (rsp) => {
    console.log('rsp : ', rsp);
});

这里面test是new出来的一个实例,对应着一个创建出来的c程序进程, test.do可以被调用多次,如果c程序中有全局变量在每次调用时改变, 那么就会影响之后的调用结果, 就是说每new一次是一个执行session,这种接口很适合加载需要很久,执行很快的程序,不用每次创建进程进行初始化。另外,如果你想要让c程序多进程,只要new多个实例就好。

执行

node test.js
rsp :  { rsp_param_string: 'teststring', rsp_param_long: 124 }

调用成功

所以整个过程就是这样

“定义接口文件”-->“通过工具生成js和c文件”-->“补充c文件中的实现逻辑”-->“编译c文件”-->“调用js文件”

本框架还有一些欠缺,比如c程序在向js传递消息的时候是使用的同步的write方法,会导致c程序的cpu使用率达不到极限,笔者之后会使用非阻塞io的模型优化这块代码。

如果你喜欢这篇文章,请麻烦帮我star哦。


你可能感兴趣的:(nodejs)