通过ffi在Node.js中调用动态链接库(.so/.dll文件)

作者:link

概述

为什么要在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通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令。

煎蛋栗子

这里就不演示利用node-gyp将.cc文件生成.node文件了,一般我都是找后台同学帮我把C源码文件编译成.so文件,然后直接拿过来用!哈哈哈!

这个栗子是nodejs调用C接口发送短信,这个C的API也非常简单:

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接口的指针内容

上面这个栗子非常简单,主要是简单在传参和出参的类型。由于javascript和C这两种语言的基本类型并不能完全对齐,所以有时候在调用的时候,对于传参出参的处理比较麻烦。经常遇到的一个问题就是如何在JS中针对C的指针类型进行操作。

例如有5个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接口释放指针指向的内存
}

有时候,有时候,我会相信一切有尽头,相爱分离都有时候,没有什么会永垂不朽。。。 不对,跑偏了。有时候,我们会把一个指针作为入参传给一个C接口,接口方法执行完之后会给这个指针指向的内存地址赋值,那么我们如何把这个值取出来呢?下面给出一个栗子。

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()就好了,不过其灵活性显然是不如以上思路的。

原文链接:http://ivweb.io/topic/57732fbef0a5487b05f325bf

推荐阅读:

原文链接:

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏linux驱动个人学习

编译选项含义

编译选项: 现在我们Makefile中的编译选项有: ? -g :可执行程序包含调试信息;(给gdb调试使用) -O2:该优化选项会牺牲部分编译速度,除了执行-...

3716
来自专栏大数据钻研

Web前端中的命名规则

规范目的 为提高团队协作效率, 便于后台人员添加功能及前端后期优化维护, 输出高质量的文档, 特制订此文档. 本规范文档一经确认, 前端开发人员必须按本文档规范...

2899
来自专栏大史住在大前端

javascript基础修炼(4)——UMD规范的代码推演

UMD规范,就是所有规范里长得最丑的那个,没有之一!!!它是为了让模块同时兼容AMD和CommonJs规范而出现的,多被一些需要同时支持浏览器端和服务端引用的第...

873
来自专栏前端知识分享

第17天:CSS引入、选择器优先级(中级)

   <div class="fr" style="color:red;">aa</div>

703
来自专栏Elasticsearch实验室

Elasitcsearch 底层系列 Lucene 内核解析之 Stored Fields

Lucene 的 stored fields 主要用于行存文档需要保存的字段内容,每个文档的所有 stored fields 保存在一起,在查询请求需要返回字段...

1322
来自专栏IMWeb前端团队

通过ffi在node.js中调用动态链接库(.so/.dll文件)

? 概述 为什么要在node.js中调用动态链接库 由于腾讯体系下的许多公共的后台服务(L5, CKV, msgQ等)已经有了非常成熟的C/C++编写的API...

2897
来自专栏MasiMaro 的技术博文

Windows服务框架与服务的编写

从NT内核开始,服务程序已经变为一种非常重要的系统进程,一般的驻守进程和普通的程序必须在桌面登录的情况下才能运行,而许多系统的基础程序必须在用户登录桌面之前就要...

481
来自专栏代码GG之家

google 进入分屏后在横屏模式按home键界面错乱( 四)

google 进入分屏后在横屏模式按home键界面错乱( 四) 你确定你了解分屏的整个流程? ? 代码阅读,请到此处http://androidxref.com...

1908
来自专栏前端杂货铺

objC与js通信实现--WebViewJavascriptBridge

场景   在移动端开发中,最为流行的开发模式就是hybmid开发,在这种native和h5的杂糅下,既能在某些需求中保证足够的性能,也可以在某些列表详情的需求下...

36310
来自专栏用户2442861的专栏

Emmet for Dreamweaver:HTML/CSS代码快速编写神器

Emmet的前身是大名鼎鼎的Zen coding,如果你从事Web前端开发的话,对该插件一定不会陌生。它使用仿CSS选择器的语法来生成代码,大大提高了HTML...

512

扫码关注云+社区