在编写Java程序时,我们偶尔会调用一些其他语言(主要是C和C++)写成的第三方库。它们多以.dll或.so文件的形式存在,称为动态链接库(dynamic link library),也经常称为本地库(native library)。最近工作中遇到了需要调本地库的需求,做个简单记录。
传统方法自然是使用大名鼎鼎的JNI(Java Native Interface),步骤如下:
- 在Java代码中定义native方法的签名,并用javah命令生成对应的头文件;
- 将生成的头文件和本地库一起导入一个中间库项目,编写C/C++代码调用本地库的逻辑;
- 把编译好的中间库放入Java项目,调用System.loadLibrary()方法载入,再调用第1步定义的native方法。
以最简单的Hello World为例(这里的“本地库”就是C标准库),代码最终长这样:
- HelloWorld.java
class HelloWorld {
private native void print();
static {
System.loadLibrary("HelloWorld");
}
public static void main(String[] args) {
new HelloWorld().print();
}
}
- HelloWorld.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */
#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: print
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_print
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
- libHelloWorld.c
#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL
Java_HelloWorld_print(JNIEnv *env, jobject obj) {
printf("Hello World!\n");
return;
}
由此可见,使用JNI的过程还是非常繁琐的:
- 不能直接调用,需要建立一个中间项目做桥接(bridge);
- 会生成大量胶水代码(glue code);
- 桥接项目调用第三方库时必须遵循JNI的固定模式(boilerplate)。
JNA(Java Native Access)的出现则大大降低了Java代码调用本地库的难度:不需要再建立额外的项目和写其他的C/C++代码,只需要在Java程序里添加一个接口,就能方便地直接代理本地库的方法了。
首先添加JNA的Maven依赖:
<dependencies>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>4.5.2</version>
</dependency>
</dependencies>
以下是最简单的示例,代理C标准库的printf()函数:
public class HelloJNA {
public interface CLibrary extends Library {
CLibrary instance = (CLibrary) Native.loadLibrary("c", CLibrary.class);
// int printf(const char *format, ...)
// char *对应String,可变参数列表对应Object...
int printf(String format, Object... args);
}
public static void main(String[] args) {
CLibrary.instance.printf("Hello World! %d", 123456);
}
}
比JNI清爽多了。究其原因,JNA(在JNI的基础上)多做了以下两件事:
- 将本地库抽象为继承了Library接口的接口。由于.dll、.so文件本身就是对外提供C/C++方法的容器,所以抽象成接口更符合Java程序员的思维,用起来也方便。
- 维护了Java数据类型与C/C++数据类型之间的动态映射。众所周知,跨平台调用时数据类型不兼容是老大难问题,JNI的烦人之处就在于我们必须手动处理。而JNA可以自动转换两个平台的数据类型,编写Java代码时只需要记住映射关系就可以了。
关于JNA的具体机制,在其官方文档中有简要的说明,看官可以参考,此处不再赘述。
简单数据类型的映射关系如下表所示,可以满足绝大多数需求了。
下面讨论一下两种比较特殊的情况,即指针和回调函数。
- 指针类型
假设本地库代码中,一个用来分配缓存空间的C函数声明如下:
void allocate_buffer(char **bufp, int *lenp);
Java没有显式指针的概念,只有引用,所以JNA内提供了一组ByReference类型用来表示各种数据类型的指针。我们在Library接口的实现类中,可以这样代理上述函数:
void allocate_buffer(PointerByReference bufp, IntByReference lenp);
可见,整形指针用IntByReference来表示,而字符串指针(即上大一时老师会说的“指向指针的指针”)可以用PointerByReference表示,很容易理解。调用该函数的示例:
PointerByReference bufp = new PointerByReference();
IntByReference lenp = new IntByReference();
SomeLibrary.instance.allocate_buffer(bufp, lenp);
// 取出缓存空间
Pointer p = bufp.getValue();
byte[] buffer = p.getByteArray(0, lenp.getValue());
当然,由于单个指针指向的是一个内存单元,所以我们也可以用只有一个元素的数组来代替它们,在语言层面是等价的。但是这样会对理解造成困扰,所以JNA才专门提供了ByReference类型进行区分。
- 回调函数
大家都知道回调函数是什么,就不废话了,贴张图吧。
C语言的回调函数本质上是当做参数传递的函数指针,前面的ByReference无法处理这种情况,所以JNA又专门提供了Callback接口,使用起来也不难。举个例子,在Linux的信号(软中断)机制中,有如下定义:
#include <signal.h>
typedef void (*sighandler_t) (int);
sighandler_t signal(int signum, sighandler_t func);
int raise(int signum);
signal()函数接收两个参数,一是信号的值,二是收到信号后的回调函数。raise()函数则用来触发信号。下面给出用JNA调用它们的完整示例:
public class JNAWithCallbackExample {
private static int SIGUSR1 = 30;
public interface CLibrary extends Library {
CLibrary instance = Native.loadLibrary("c", CLibrary.class);
interface sighandler_t extends Callback {
void invoke(int signum);
}
sighandler_t signal(int signum, sighandler_t func);
int raise(int signum);
}
public static void main(String[] args) {
// 保持引用
CLibrary.sighandler_t func = new sighandler_t() {
public void invoke(int signum) {
System.out.println("Signal " + signum + " raised");
}
};
CLibrary.sighandler_t signal = CLibrary.instance.signal(SIGUSR1, func);
int result = CLibrary.instance.raise(SIGUSR1);
}
}
可见,我们可以通过定义与回调函数类型同名的、继承Callback的接口来实现回调。需要注意,在回调函数完成之前,我们必须要保持对回调对象的引用,以防止回调逻辑被GC掉出现异常。
最后一个问题,如果需要调用的本地库函数特别多,或者存在struct、union这样的复杂结构,难道还要看着文档手动将它们翻译成Java代码吗?简单的方法自然是有的,可以借助JNAerator工具,根据C/C++代码自动生成对应的JNA代码。具体用法请参考官方Wiki。
民那晚安。