JavaJNA(三)——结构体使⽤及简单⽰例
JNA简介
JNA全称Java Native Access,是⼀个建⽴在经典的JNI技术之上的Java开源框架()。JNA提供⼀组Java⼯具类⽤于在运⾏期动态访问系统本地库(native library:如Window的dll)⽽不需要编写任何Native/JNI代码。开发⼈员只要在⼀个java接⼝中描述⽬标native library的函数与结构,JNA将⾃动实现Java接⼝到native function的映射。
JNA包:
JNA在线帮助⽂档:
JNA⼊门⽰例:
1,dll和so是C函数的集合和容器,这与Java中的接⼝概念吻合,所以JNA把dll⽂件和so⽂件看成⼀个个接⼝。在JNA中定义⼀个接⼝就是相当于了定义⼀个DLL/SO⽂件的描述⽂件,该接⼝代表了动态链接库中发布的所有函数。⽽且,对于程序不需要的函数,可以不在接⼝中声明。
2,JNA定义的接⼝⼀般继承com.sun.jna.Library接⼝,如果dll⽂件中的函数是以stdcall⽅式输出函数,那么,该接⼝就应该继承
com.sun.jna.win32.StdCallLibrary接⼝。
3,Jna难点:编程语⾔之间的数据类型不⼀致。
Java和C的数据类型对照
Java和C的数据类型对照表
Java 类型 C 类型原⽣表现
boolean int 32位整数(可定制)
byte char 8位整数
char wchar_t平台依赖
short short 16位整数
int int 32位整数
long long long,
__int64
64位整数
float float 32位浮点数
double double 64位浮点数
Buffer/Pointer pointer平台依赖(32或64位指针)
<T>[] (基本类型的数组) pointer/array
32或64位指针(参数/返回值)
邻接内存(结构体成员)
String char*/0结束的数组 (native encoding ding)
WString wchar_t* /0结束的数组(unicode)
String[] char** /0结束的数组的数组
WString[] wchar_t** /0结束的宽字符数组的数组
Structure struct*/struct指向结构体的指针(参数或返回值) (或者明确指定是结构体指针)结构体(结构体的成员) (或者明确指
定是结构体)
Union union 等同于结构体
Structure[] struct[]结构体的数组,邻接内存
Callback <T> (*fp)() Java函数指针或原⽣函数指针
NativeMapped varies依赖于定义
NativeLong long平台依赖(32或64位整数)
PointerType pointer和Pointer相同
通⽤⼊门案例
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {
// This is the standard, stable way of mapping, which supports extensive
// customization and mapping of Java to native types.
public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary)
Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
CLibrary.class);
void printf(String format, args);
}
public static void main(String[] args) {
CLibrary.INSTANCE.printf("Hello, World\n");
for (int i=0;i < args.length;i++) {
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
}
}
}
运⾏程序,如果没有带参数则只打印出“Hello, World”,如果带了参数,则会打印出所有的参数。
很简单,不需要写⼀⾏C代码,就可以直接在Java中调⽤外部动态链接库中的函数!
下⾯来解释下这个程序。
(1)需要定义⼀个接⼝,继承⾃Library 或StdCallLibrary
默认的是继承Library ,如果动态链接库⾥的函数是以stdcall⽅式输出的,那么就继承StdCallLibrary,⽐如众所周知的kernel32库。⽐如上例中的接⼝定义:
public interface CLibrary extends Library {
}
(2)接⼝内部定义
接⼝内部需要⼀个公共静态常量:INSTANCE,通过这个常量,就可以获得这个接⼝的实例,从⽽使⽤接⼝的⽅法,也就是调⽤外部dll/so的函数。
该常量通过Native.loadLibrary()这个API函数获得,该函数有2个参数:
第⼀个参数是动态链接库dll/so的名称,但不带.dll或.so这样的后缀,这符合JNI的规范,因为带了后缀名就不可以跨操作系统平台了。
搜索动态链接库路径的顺序是:先从当前类的当前⽂件夹,如果没有到,再在⼯程当前⽂件夹下
⾯win32/win64⽂件夹,到后搜索对应的dll⽂件,如果不到再到WINDOWS下⾯去搜索,再不到就会抛异常了。⽐如上例中printf函数在Windows平台下所在的dll库名称是msvcrt,⽽在其它平台如Linux下的so库名称是c。
第⼆个参数是本接⼝的Class类型。JNA通过这个Class类型,根据指定的.dll/.so⽂件,动态创建接⼝的实例。该实例由JNA通过反射⾃动⽣成。
CLibrary INSTANCE = (CLibrary)
Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
CLibrary.class);
接⼝中只需要定义你要⽤到的函数或者公共变量,不需要的可以不定义,如上例只定义printf函数:
void printf(String format, args);
注意参数和返回值的类型,应该和链接库中的函数类型保持⼀致。
(3)调⽤链接库中的函数
定义好接⼝后,就可以使⽤接⼝中的函数即相应dll/so中的函数了,前⾯说过调⽤⽅法就是通过接⼝中的实例进⾏调⽤,⾮常简单,如上例中:
CLibrary.INSTANCE.printf("Hello, World\n");
for (int i=0;i < args.length;i++) {
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
}
这就是JNA使⽤的简单例⼦,可能有⼈认为这个例⼦太简单了,因为使⽤的是系统⾃带的动态链接库,应该还给出⼀个⾃⼰实现的库函数例
⼦。其实我觉得这个完全没有必要,这也是JNA的⽅便之处,不像JNI使⽤⽤户⾃定义库时还得定义⼀⼤堆配置信息,对于JNA来说,使⽤⽤户⾃定义库与使⽤系统⾃带的库是完全⼀样的⽅法,不需要额外配置什么信息。⽐如我在Windows下建⽴⼀个动态库程序:
#include "stdafx.h"
c语言struct用法例子extern "C"_declspec(dllexport) int add(int a, int b);
int add(int a, int b) {
return a + b;
}
然后编译成⼀个dll⽂件(⽐如CDLL.dll),放到当前⽬录下,然后编写JNA程序调⽤即可:
public class DllTest {
public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary)Native.loadLibrary("CDLL", CLibrary.class);
int add(int a, int b);
}
public static void main(String[] args) {
int sum = CLibrary.INSTANCE.add(3, 6);
System.out.println(sum);
}
}
简单案例
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
interface HelloInter extends Library {
int toupper(int ch);
double pow(double x, double y);
void printf(String format, args);
}
public class HelloWorld {
public static void main(String[] args) {
HelloInter INSTANCE = (HelloInter) Native.loadLibrary(Platform.isWindows() ? "msvcrt" : "c", HelloInter.class);
INSTANCE.printf("Hello, Worldn");
String[] strs = new String[] { "芙蓉", "如花", "凤" };
for (int i = 0; i < strs.length; i++) {
INSTANCE.printf("⼈物 %d: %sn", i, strs[i]);
}
System.out.println("pow(2d,3d)==" + INSTANCE.pow(2d, 3d));
System.out.println("toupper('a')==" + (char) upper((int) 'a'));
}
}
显⽰结果:
pow(2d,3d)==8.0
toupper('a')==A
Hello, Worldn⼈物 0: 芙蓉n⼈物 1: 如花n⼈物 2: 凤n
说明:
HelloInter接⼝中定义的3个函数全是C语⾔函数库中的函数,其定义格式如下:
int toupper(int ch)
double pow( double x, double y )
int printf(const char* format, ...)
C语⾔函数库中有很多个函数,但是我们只⽤到了这3个函数,所以其他的函数不需要声明在接⼝中。
JNA模拟结构体
例:使⽤ JNA调⽤使⽤ Struct的 C函数
假设我们现在有这样⼀个C 语⾔结构体
struct UserStruct{
long id;
wchar_t* name;
int age;
};
使⽤上述结构体的函数
#define MYLIBAPI extern "C" __declspec( dllexport )
MYLIBAPI void sayUser(UserStruct* pUserStruct);
对应的Java 程序中,在例1 的接⼝中添加下列代码:
public static class UserStruct extends Structure{
public NativeLong id;
public WString name;
public int age;
public static class ByReference extends UserStruct implements Structure.ByReference {}
public static class ByValue extends UserStruct implements Structure.ByValue {}
@Override
protected List getFieldOrder() {
return Arrays.asList(new String[] { "id", "name", "age"});
}
}
public void sayUser(UserStruct.ByReference struct);
Java中的代码
UserStruct userStruct=new UserStruct ();
userStruct.id=new NativeLong(100);
userStruct.age=30;
userStruct.name=new WString("奥巴马");
TestDll1.INSTANCE.sayUser(userStruct);
Structure说明
现在,我们就在Java 中实现了对C 语⾔的结构体的模拟。这⾥,我们继承了Structure 类,⽤这个类来模拟C 语⾔的结构体。必须注
意,Structure ⼦类中的公共字段的顺序,必须与C 语⾔中的结构的顺序⼀致。否则会报错!因为,Java 调⽤动态链接库中的C 函数,实际上就是⼀段内存作为函数的参数传递给C函数。动态链接库以为这个参数就是C 语⾔传过来的参数。同时,C 语⾔的结构体是⼀个严格的规范,它定义了内存的次序。因此,JNA 中模拟的结构体的变量顺序绝对不能错。
如果⼀个Struct 有2 个int 变量。Int a, int b如果JNA 中的次序和C 语⾔中的次序相反,那么不会报错,但是数据将会被传递到错误的字段中去。
Structure 类代表了⼀个原⽣结构体。当Structure 对象作为⼀个函数的参数或者返回值传递时,它代表结构体指针。当它被⽤在另⼀个结构体内部作为⼀个字段时,它代表结构体本⾝。
另外,Structure 类有两个内部接⼝Structure.ByReference 和Structure.ByValue。这两个接⼝仅仅是标记,如果⼀个类实现
Structure.ByReference 接⼝,就表⽰这个类代表结构体指针。
如果⼀个类实现Structure.ByValue 接⼝,就表⽰这个类代表结构体本⾝。使⽤这两个接⼝的实现类,可以明确定义我们的Structure 实例表⽰的是结构体的指针还是结构体本⾝。上⾯的例⼦中,由于Structure 实例作为函数的参数使⽤,因此是结构体指针。所以这⾥直接使⽤了UserStruct userStruct=
new UserStruct ();也可以使⽤UserStruct userStruct=new UserStruct.ByReference ();明确指出userStruct 对象是结构体指针⽽不是结构体本⾝。
JNA模拟复杂结构体C 语⾔最主要的数据类型就是结构体。结构体可以内部可以嵌套结构体,这使它可以拟任何类型的对象。JNA 也可以模拟这类复杂的结构体,结构体内部可以包含结构体对象的指针的数组
struct CompanyStruct{
long id;
wchar_t* name;
UserStruct users[100];
int count;
};
JNA 中可以这样模拟:
public static class CompanyStruct extends Structure{
public NativeLong id;
public WString name;
public UserStruct.ByValue[] users=new UserStruct.ByValue[100];
public int count;
@Override
protected List getFieldOrder() {
return Arrays.asList(new String[] { "id", "name",,"users" "count"});
}
}
这⾥,必须给users 字段赋值,否则不会分配100 个UserStruct 结构体的内存,这样JNA中的内存⼤⼩和原⽣代码中结构体的内存⼤⼩不⼀致,调⽤就会失败。
测试代码:
CompanyStruct2.ByReference companyStruct2=new CompanyStruct2.ByReference();
companyStruct2.id=new NativeLong(2);
companyStruct2.name=new WString("Yahoo");
UserStruct.ByReference pUserStruct=new UserStruct.ByReference();
pUserStruct.id=new NativeLong(90);
pUserStruct.age=99;
pUserStruct.name=new WString("杨致远");
// pUserStruct.write();
for(int i=0;i&unt;i++){
companyStruct2.users[i]=pUserStruct;
}
TestDll1.INSTANCE.sayCompany2(companyStruct2);
执⾏测试代码,报错了。这是怎么回事?
考察JNI 技术,我们发现Java 调⽤原⽣函数时,会把传递给原⽣函数的Java 数据固定在内存中,这样原⽣函数才可以访问这些Java 数据。对于没有固定住的Java 对象,GC 可以删除它,也可以移动它在内存中的位置,以使堆上的内存连续。如果原⽣函数访问没有被固定住的Java 对象,就会导致调⽤失败。固定住哪些java 对象,是JVM 根据原⽣函数调⽤⾃动判断的。⽽上⾯的CompanyStruct2结构体中的⼀个字段是UserStruct 对象指针的数组,因此,JVM 在执⾏时只是固定住了CompanyStruct2 对象的内存,⽽没有固定住users 字段引⽤的UserStruct 数组。因此,造成了错误。我们需要把users 字段引⽤的UserStruct 数组的所有成员也全部固定住,禁⽌GC 移动或者删除。如果我们执⾏了pUserStruct.write();这段代码,那么就可以成功执⾏上述代码。
Structure 类的write()⽅法会把结构体的所有字段固定住,使原⽣函数可以访问。
总结
使⽤JNA的过程中也不⼀定会⼀帆风顺,⽐如会抛出”⾮法内存访问”,这时候检查⼀下变量是否==null。还有内存对齐的问题,当从内存中获取图⽚信息进⾏保存的时候,如果内存对齐处理不好,就会抛出很严重的异常,导致JVM异常退出,JNA提供了四种内存对齐的⽅式,分别是:ALIGN_DEFAULT、ALIGN_NONE、ALIGN_GNUC和ALIGN_MSVC。ALIGN_DEFAULT采⽤平台默认的对齐⽅式(推
荐);ALIGN_NONE是不采⽤对齐⽅式;ALIGN_GNUC为针对linux/gcc操作系统的对齐⽅式。ALIGN_MSVC为针对win32/msvc架构的内存对齐⽅式。
JNA也提供了⼀种保护机制.⽐如防⽌JNA出现异常不会导致JVM异常退出,默认是开启这个功能的,开启⽅式为
System.setProperty(“jna.protected”,”true”); 记得要在JNA加载dll⽂件之前调⽤,然后try {...} catch(Throwable e)异常,不过你也不要期望过⾼,不要以为加上这个就万事⼤吉,出现”⾮法内存访问”的时候还是会束⼿⽆策。JNA也提供了⼀种保护机制.⽐如防⽌JNA 出现异常不会导致JVM异常退出,默认是开启这个功能的,开启⽅式为 System.setProperty(“jna.protected”,”true”); 记得要在JNA加载dll⽂件之前调⽤,然后try {...} catch(Throwable e)异常,不过你也不要期望过⾼,不要以为加上这个就万事⼤吉,出现”⾮法内存访问”的时候还是会束⼿⽆策。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论