有时候Java需要通过调用dll来操作底层的硬件或者现有的组件,直接使用JNI是容易出错的,且需要编写本地的C代码。这里介绍另一个调用dll的更简单的方法:JNA。这里介绍JNA调用dll的本地方法、JNA回调Java方法以及JNA相关的一些知识点。如果去留意的话,会发现现在很多jar包都用JNA来调用dll了。
这篇文章原发布在Iteye的个人博客上http://wen866595.iteye.com/admin/blogs/974826,现在迁移到自己的站点上。
使用JNA简单调用DLL里的函数
- 在VC下创建一个动态链接库项目testJNA
-
在头文件里声明函数
extern "C" _declspec(dllexport) int add(int first, int second);
-
在源码里实现函数
int add(int first, int second) {
printf("(c) test jna : %d + %d = %d", first, second, first + second);
return first + second;
}
-
生成dll文件:
testJNA.dll
-
在Java里定义一个表示链接库的接口,接口
TestJnaLib
继承自com.sun.jna.Library
,此接口有一个实例:
TestJnaLib INSTANCE = (TestJnaLib)Native.loadLibrary("testJNA.dll", TestJnaLib.class);
此实例由JNA通过反射自动生成。 -
定义对应dll里的方法:
int add(int first, int second);
-
调用本地方法:
TestJnaLib.INSTANCE.add(3, 5);
JNA回调Java方法
-
在C语言部分定义带回调函数的函数:
extern "C" _declspec(dllexport) void methodWithCallback(int (*fp)(int left, int right), int left, int right);
-
Java里定义一个回调接口
必须继承自com.sun.jna.Callback
接口
public interface FunCallBack extends Callback {
int invoke(int left, int right);
}
`Invoke`方法里的参数顺序与C函数的对应。
- 定义回调接口的实现
public class CallbackFunImpl implements FunCallBack {
@Override
public int invoke(int left, int right) {
System.out.printf("in java :%d + %d = %d\n", left, right, left + right);
return left + right;
}
}
-
在表示链接库实现的接口里定义要回调的本地函数
void methodWithCallback(Callback callback, int left, int right);
本地函数的函数指针用Callback
接口替代。 -
调用带函数指针的本地函数
TestJnaLib.INSTANCE.methodWithCallback(new CallbackFunImpl(), 4, 6)
JNA相关知识
使用JNA调用原生函数的模式:
- JNA不使用
native
关键字。 - JNI使用
native
关键字,使用一个java方法来代表外部的原生函数。 - JNA使用一个java接口来代表一个动态链接库发布的所有函数。
- 对于不需要的原生函数,可以不在java接口中声明java方法原型。
使用JNI,需要使用System.loadLibrary
方法,把专门为JNI编写的动态链接库载入进来,这个动态链接库实际上是真正需要的动态链接库的代理。
使用JNA类库的Native
类的loadLibrary
方法,是直接把需要的动态链接库载入进来。使用JNA不需要编写作为代理的动态链接库,不需要编写一行原生代码。
跨平台、跨语言调用原则:
- 尽量使用基本、简单的数据类型;
- 尽量少跨平台、跨语言传递数据!
Java调用原生函数时,会把数据固定在内存中,这样原生函数才可以访问这些Java数据。这些数据,JVM的GC不能管理,会造成内存碎片。
C语言的结构体是一个严格的规范,定义了内存的次序。因此,JNA中模拟的结构体的变量顺序绝对不能错。
Java调用动态链接库中的C函数,实际上就是把一段内存作为函数的参数传递给C函数。动态链接库以为这个参数就是C语言传过来的参数。
Structure
类的write()
方法会把结构体的所有字段固定住,是原生函数可以访问。
JNI技术是双向的,既可以从Java代码中调用原生函数,也可以从原生函数中直接创建Java虚拟机,并调用Java代码。
原生函数可以通过函数指针实现函数回调,调用外部函数来执行任务。这就是策略模式。
Callback
任务回调定义必须继承自com.sun.jna.Callback
接口,子类必须定义单个公有方法或一个名为callback的公有方法。必须持有到回调对象的一个存活引用。一个回调应该不抛出异常。
com.sun.jna.Library
代表本地链接库的定义。可用如下形式定义:
MyNativeLibrary INSTANCE = (MyNativeLibrary)Native.loadLibrary("mylib", MyNativeLibrary.class);
虽然结构体和结构体字段的名字可以是任意的,但他们应该尽可能接近本地定义。参数名也一样。此接口支持在Java端多线程、并发调用本地方法。如果本地类库不是线程安全的,可用Native.synchronizedLibrary(com.sun.jna.Library)
来阻止多线程同时访问本地代码。
链接库的搜索路径
jna.library.path
用户定义的路径;Jna.platform.library.path
平台定义的路径,
com.sun.jna.Structure
代表本地结构体的对等Java类。
当作为参数或返回值使用时,这个类被传递给struct *
(指针);当作为另一个结构体的字段时,它被传递给struct
(值传递)。
标记接口Structure.ByReference
和Structure.ByValue
被用来提醒默认的行为。
Structure属性传递给本地字段必须是public的。可以由下面可选的修饰符:
volatile
: JNA不会修改字段,除非被要求,通过writeField(String)
;final
: JNA会通过read()
覆写字段,字段在java这边是不可变的。
Structure
属性的顺序与类型必须与本地结构体的字段一一对应。
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。