原文 http://blog.youkuaiyun.com/shendl/archive/2008/12/26/3599849.aspx
深入解析 JNA— 模拟 C 语言结构体
前言
前几天写《 JNA--JNI 终结者》一文介绍 JNA 框架。写完之后才发现,忘了写比较有难度的 C 语言 Struct 的模拟了。
今天就补上这篇文章,介绍 Struct 。
不写怎样模拟 C 语言结构体,就不能算是真正解决了调用动态链接库的问题。
C 语言的结构体用得实在是太广泛了。
首先说明一点,本文中大量把模拟 Struct 的类写作为接口的内部类。
这不是 JNA 规定的,而是一个编程习惯。
因为这些结构体( Structure 类的子类),一般没有重用的价值,因此写成内部类比较方便。自然,你也可以把结构体写成一般的类。
例 3 使用 JNA 调用使用 Struct 的 C 函数
C 语言开发
继续使用例 2 中的那个 VSC++ 的 dll 项目。
增加一个结构和使用该结构的函数。
头文件增加如下:
#define MYLIBAPI extern "C" __declspec ( dllexport )
struct UserStruct{
long id;
wchar_t * name;
int age;
};
MYLIBAPI void sayUser(UserStruct* pUserStruct);
JNA 程序
对应的 Java 程序中,在例 2 的 接口
/*
* 定义一个类,模拟 C 语言的结构
* */
public static class UserStruct extends Structure{
public NativeLong id ;
public WString name ;
public int 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);
Struct 说明
现在,我们就在 Java 中实现了对 C 语言的结构的模拟。
这里,我们继承了 Structure 类,用这个类来模拟 C 语言的结构。
必须注意, Structure 子类中的公共字段的顺序,必须与 C 语言中的结构的顺序一致。否则会报错!
因为, Java 调用 dll 中的 C 函数,实际上就是一段内存作为函数的参数传递给 dll 。
Dll 以为这个参数就是 C 语言传过来的参数。
同时, C 语言的结构是一个严格的规范,它定义了内存的次序。因此, JNA 中模拟的结构的变量顺序绝对不能错。
如,一个 Struct 有 2 个 int 变量。 Int a, int b
如果 JNA 中的次序和 C 中的次序相反,那么不会报错,但是得到的结果是相反的!
例 4 使用 JNA 调用使用嵌套 Struct 数组的 C 函数
如果 C 语言中的结构体是复杂的嵌套的结构体,该怎么办呢?
继续在上面例 3 的基础上扩充。
C 语言开发
头文件增加如下:
struct CompanyStruct{
long id;
wchar_t * name;
UserStruct users[100];
int count;
};
MYLIBAPI void sayCompany(CompanyStruct* pCompanyStruct);
源文件:
void sayCompany(CompanyStruct* pCompanyStruct){
std::wcout.imbue(std::locale( "chs" ));
std::wcout<<L "ID:" <<pCompanyStruct->id<<std::endl;
std::wcout<<L "公司名称:" <<pCompanyStruct->name<<std::endl;
std::wcout<<L "员工总数:" <<pCompanyStruct->count<<std::endl;
for ( int i=0;i<pCompanyStruct->count;i++){
sayUser(&pCompanyStruct->users[i]);
}
}
JNA 程序
Java 程序中,在原来的接口上加上如下代码:
public static class CompanyStruct extends Structure{
public NativeLong id ;
public WString name ;
public UserStruct.ByValue[] users = new UserStruct.ByValue[100];
public int count ;
}
public void sayCompany(CompanyStruct pCompanyStruct);
对原来的 UserStruct 类进行改写:
/*
* 定义一个类,模拟 C 语言的结构
* */
public static class UserStruct extends Structure{
public static class ByReference extends UserStruct implements Structure.ByReference { }
public static class ByValue extends UserStruct implements Structure.ByValue
{ }
public NativeLong id ;
public WString name ;
public int age ;
}
调用 JNA 程序:
CompanyStruct companyStruct= new CompanyStruct();
companyStruct. id = new NativeLong(1);
companyStruct. name = new WString( "Google" );
companyStruct. count =9;
UserStruct.ByValue userStructValue= new UserStruct.ByValue();
userStructValue. id = new NativeLong(100);
userStructValue. age =30;
userStructValue. name = new WString( " 沈东良 " );
for ( int i=0;i<companyStruct. count ;i++){
companyStruct. users [i]=userStructValue;
}
TestDll1. INSTANCE .sayCompany(companyStruct);
说明
可以看到,程序正确输出了。
读者也许会有一些疑问。
1 ,为什么我们要给 UserStruct 这个结构添加 2 个内部类呢?
看 Structure 类的 API 说明,我们知道,这个类内部有 2 个接口:
|
|
|
|
这 2 个内部接口是标记,内部什么都没有。
在运行时, JNA 的执行框架会使用反射查看你是否实现了这 2 个接口,然后进行特定的处理。
(这种技术在 java 标注 Annotation 之前很流行。现在可以使用运行时 Annotation 实现同样的效果。
JNA 项目据说 1999 年就启动了,使用这样的老技术不足为奇。只是很奇怪,为什么国内都没怎么听说过。我也是最近偶然在国外的网站上发现它的。一试之下,爱不释手,令我又对 Java 的桌面应用信心百倍!
)
如果你的Struct
实现
Structure.ByReference
接口,那么JNA
认为你的Struct
是一个指针。指向C
语言的结构体。
如果你的Struct
实现
Structure.ByValue
接口,那么JNA
认为你的Struct
是值类型,就是C
语言的结构体。
如果你不实现这
2
个接口,那么就相当于你实现了
Structure.ByReference
接口。
因此,在例3
中,我没有实现这2
个接口。
2 , C 语言中,结构体内部必须进行数组定义。 Java 中最好也这样做。
C 语言的结构体是一段连续的内存,内存的大小是编译时确定的。
因此,数组必须定义。否则编译不会成功。
对应的 Java 类中,我们也应该在类定义时为数组定义。尽管实际上在使用时再赋值也可以。
但是,强烈不建议你这样做。
如,上面
public UserStruct.ByValue[] users = new UserStruct.ByValue[100];
定义 100 个元素的数组,如果你不再类内部定义。
而在使用时定义,如果你没有正确赋值,没有定义为 100 个元素,就会出错。
从表面上看, CompanyStruct 类 占用的内存是:
NativeLong id
WString name ;