通过ffi在nodejs中调用动态链接库(.so文件)

通过ffi在nodejs中调用动态链接库(.so文件)通过 ffi 在 nodejs 中调用 C 接口概述获取 C 接口的指针内容异步调用实际案例通过 ffi 在 nodejs 中调用 C 接口概述注意事项 ffi 只能调用 C 风格的模块 需要将 C 源码 build 成动态链接库以供调用 在 Linux 下将 C 源码 build 成 so 文件 在 windows 下 build 成 dll 文件 获取 C 接口的指针内容异步调用实际案例

通过ffi在nodejs中调用动态链接库(.so文件)

概述

为什么要在node.js中调用动态链接库

  1. 由于腾讯体系下的许多公共的后台服务(L5, CKV, msgQ等)已经有了非常成熟的C/C++编写的API,以供应用程序调用,node.js作为在公司内新兴的后台runtime在调用这些公共服务的时候没必要再造一遍轮子,而是可以将这些API编译成.so文件直接使用。
  2. 对于一些密集计算型的任务可以由C++编写好模块,生成.so文件后由node.js调用。

ffi简介与安装

我们使用node-ffi来帮助我们调用动态链接库。

FFI的全称是Foreign Function Interface,该项目生来就是解决NodeJS的本地调用问题的,其流程就相当于Windows下的LoadLibrary()和GetProcAddress(),亦可以理解为NodeJS下的平台调用。为了调用一个小小的本地函数而创建一个addon实在是有点过头了,这个时候,FFI这把杀鸡刀就顺手得多了。有了它,本地调用变得异常简单,因为它在NodeJS环境中为JavaScript提供了一套强大的工具集用来调用动态链接库。

notice: 本人的node使用环境是64bit的Linux系统。
安装ffi:

  1. 全局或局部安装node-gyp: npm install -g node-gyp,装之前要安装python 2.7,而node-gyp不支持Python 3.x,所以安装了多个版本Python的读者得留意一下自己当前的Python版本了。Linux下pythonbrew一键搞定,Windows下还得去改环境变量。并且,如果你使用的node.js版本是4.0+,node-gyp的安装依赖支持C++11语法的gcc,你需要确定当前环境的gcc版本至少高于4.8。
  2. 安装ffi:npm install ffi

注意事项!

  • ffi只能调用C风格的模块。
  • 需要将C源码build成动态链接库以供调用,在Linux下将C源码build成.so文件,在windows下build成.dll文件。本文只阐述.so文件的调用方法,调用.dll差别不大。
  • 在Linux下如果使用C++编写的addon来调用.so文件,需要将.so文件为系统共享。具体方法可以参看ldconfig命令,这是一个Linux下的动态链接库管理命令。ldconfig命令的主要用途是在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索出可共享的动态链接库(格式如lib*.so*),进而创建出动态装入程序(ld.so)所需的连接和缓存文件。缓存文件默认为 /etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表。ldconfig通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令。

煎蛋栗子

int send_msg(char * phone, char * content) 

参数是手机号和短信内容,类型都是char *,返回的retcode是一个整型,返回0就代表发送成功,其他就是失败,方法名是send_msg。下面是如果利用ffi在nodejs中调用这个接口,该接口的源码已经被封装成libsend_msg.so这个动态链接库了,我们直接调用就好。

'use strict' / * 短信下发服务模块 * 由于项目是使用node 5.0+,所以安装node-ffi模块需要依赖gcc 4.8+以上版本 */ var ffi = require('ffi'); // int send_msg(char * phone, char * content) var libm = ffi.Library(__dirname + '/msgQ/libsend_msg', { 
    'send_msg': ['int', ['string', 'string']] }); let smsExport = { 
    sendMsg(opt) { 
    let phone = opt.phone; let content = opt.text; // 调用c接口,发送短信 let retcode = libm.send_msg(phone, content); if (retcode === 0) { 
    // TODO succ } else { 
    // TODO fail } } }; module.exports = smsExport; 

可以看到,在使用ffi调用C接口传参时,C的char *类型在nodejs源码中可以直接用string类型表示,而对于nodejs没有的int类型,我们也可以直接写成int。并且可以看出来,这里我们使用同步的方式调用send_msg方法的。

获取C接口的指针内容

double do_some_number_fudging(double a, int b); myobj * create_object(); double do_stuff_with_object(myobj *obj); void use_string_with_object(myobj *obj, char *value); void delete_object(myobj *obj); 

可以看到这些接口,有的方法的出参是一个指向object类型的指针,有的入参是一个指向object类型的指针,如果使用C语言调用这5个接口,可能会是这样:

#include "mylibrary.h" int main() { myobj *fun_object; double res, fun; res = do_some_number_fudging(1.5, 5); fun_object = create_object(); if (fun_object == NULL) { printf("Oh no! Couldn't create object!\n"); exit(2); } use_string_with_object(fun_object, "Hello World!"); fun = do_stuff_with_object(fun_object); delete_object(fun_object); } 

那用JS如何调用这些接口呢?我们先使用ffi来包装一下这些接口:

var ref = require("ref"); var ffi = require("ffi"); // typedefs var myobj = ref.types.void // 仅仅只是用来演示如何用ref创建C语言中的类型,由于我们这里不知道myobj将来会是啥类型,所以先定义成void类型 var MyLibrary = ffi.Library('libmylibrary', { 
    "do_some_number_fudging": [ 'double', [ 'double', 'int' ] ], "create_object": [ "pointer", [] ], "do_stuff_with_object": [ "double", [ "pointer" ] ], "use_string_with_object": [ "void", [ "pointer", "string" ] ], "delete_object": [ "void", [ "pointer" ] ] }); 

好啦,下面编写JS代码来调用这些接口:

var res = MyLibrary.do_some_number_fudging(1.5, 5); // 单纯的计算 var fun_object = MyLibrary.create_object(); // 调用接口,创建一个指向object类型的指针 if (fun_object.isNull()) { 
    console.log("Oh no! Couldn't create object!\n"); } else { 
    // 将刚刚创建的指针作为入参传入其他方法。 MyLibrary.use_string_with_object(fun_object, "Hello World!"); var fun = MyLibrary.do_stuff_with_object(fun_object); MyLibrary.delete_object(fun_object); // 使用完之后记得调用C接口释放指针指向的内存 } 
void* xyz_create(int id, unsigned int network_timeout_ms, float something_timeout_s); int xyz_get(int id, void *obj, const char *key, char val, int *something); int xyz_set(int id, void *obj, const char *key, const char *val, int *something); void xyz_destroy(void *obj); void xyz_free(char *p); 

ffi包装C接口生成的动态链接库,并使用ref进行一些类型映射。

'use strict' const ref = require("ref"); const ffi = require("ffi"); // 生成兼容C的指向string类型的指针,即char let stringPointer = ref.refType(ref.types.CString); let libxyz = ffi.Library(__dirname + '/so/example.so', { 
    'xyz_create': ['pointer', ['int', 'uint', 'float']], 'xyz_get': ['int', ['int', 'pointer', 'string', stringPointer, 'int *']], 'xyz_set': ['int', ['int', 'pointer', 'string', 'string', 'int *']], 'xyz_destroy': ['void', ['pointer']], 'xyz_free': ['void', ['string']] }); 

使用JS调用C接口:

'use strict' let id = 123; let network_timeout_ms = 3000; let something_timeout_s = 0.3; let obj = libxyz .xyz_create(id , config.cmdid, network_timeout_ms, something_timeout_s); // 调用C接口创建一个指针 if (obj .isNull()) { 
    console.log("Oh no! Couldn't create object!\n"); } else { 
    let pointerSomething= ref.alloc(ref.types.int, 666); // something的值,固定为666,将js中的number类型转化成C中的int *  let key = 'key'; let value = 'value'; let retcode = libxyz.cmem_set(id , obj, key, value , pointerSomething); if(retcode === 0) { 
    let val2 = ref.alloc('string'); // 声明一个char 类型的指针,即指向string的指针 // 如果设置key/value成功,我们可以利用key取出刚刚设置的value值,并进行比较,看看有木有设置正确。取出来的值,是存在val2这个值里面的,但val2是一个指向string的指针类型,我们来看看如何取出val2的值,并与value进行比较。 let getRetcode = libcmem.cmem_get(config.bid, obj, key, val2, pointerSomething); if(getRetcode === 0) { 
    if(value === ref.readPointer(val2, 0, value.length)) { 
    console.log('set value succ!'); } } else { 
    console.log('get value failed!'); } } else { 
    console.log('set key/value failed!'); } } 

关于ref的详细api可以参看他们的官方文档:https://github.com/TooTallNate/ref

值得一提的是,还有一个名为edge.js的开源项目,整个流程和FFI类似,不过支持调用C#、Python,相当有意思。这样一来,NodeJS相当于可以用C/C++、C#、Python扩展了,潜力无限啊。当然,你可以说我直接拿其它语言写程序然后NodeJS里fork()就好了,不过其灵活性显然是不如以上思路的。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/232017.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • redis 发布订阅模式及应用场景

    redis 发布订阅模式及应用场景一 命令简介 从 redis 手册上面可以看到 其实 发布 订阅 模式才区区 6 个命令 下面听我一一解说下哈 nbsp 1 subscribeSUB channel 订阅给定的一个或多个频道的信息 nbsp nbsp nbsp 从上面的官方解释上来看 它的玩法有一点像现实生活中我们听收音机一个道理 要想听收音机 我们要做什么 肯定就是调频啦 只有在正确的频道上面 我们才能听得到好听

    2026年1月19日
    1
  • 【LeetCode】一文吃透差分数组(附例题)

    【LeetCode】一文吃透差分数组(附例题)因为我们还原数组时候需要从头开始 是有顺序的 也就是使用 map 代替了数组 注意初始数组都为 0 每次 book 都需要将区间内元素加 1 表示预定次数 然后求得区间元素最大值即为所求 区间更新问题除了最适用的线段树维护之后 还可以使用差分数组维护 顾名思义 差分数组元素就是原数组中两个元素之差 例如假设原数组为 差分数组是把原数组中后一个元素减前一个元素的差构成一个新的数组 作为辅助数组使用 这样就省去了遍历操作 因为原数组的值可以通过差分数组两端的数求得

    2025年10月23日
    3
  • div:给div加滚动条 div的滚动条设置

    div:给div加滚动条 div的滚动条设置
    今天做了个例子:
    div的滚动条问题:
    两种方法:
    一、

    记住宽和高一定要设置噢,否则不成的

    不过在不超出时,会有下面的滚动条,所以不是最好的选择
     
    二、

    2022年7月12日
    20
  • 运用NDWI指数对高分1号进行水体提取–以matlab为例

    运用NDWI指数对高分1号进行水体提取–以matlab为例clcclearpath1=’D:\…\GF1_PMS2_E116.7_N40.2_20130501_L1A0000152355-MSS2.tiff’;image=imread(path1);R=image(:,:,1);G=image(:,:,2);B=image(:,:,3);Nir=image(:,:,4);[row,column,band]=size(image);N=

    2025年6月5日
    3
  • 卸载symantec AntiVirus Client客户端,要求输入密码。。。。

    卸载symantec AntiVirus Client客户端,要求输入密码。。。。本文只针对WindowsNT/2000/XP。对于Windows95/98/ME,请参阅文章:如何手动卸载用于Windows95/98/Me的NortonAntiVirus企业版7.x客户端。从计算机删除NortonAntiVirus企业版(NAVCE)7.5或7.6的最简便方法是从WindowsNT控制面板的“添加/删除程序”中运行内置的卸

    2022年5月22日
    54
  • html字体属性控制,水平线,上下标,插入图片

    html字体属性控制,水平线,上下标,插入图片

    2021年9月9日
    87

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号