原文 http://blog.youkuaiyun.com/shendl/archive/2008/12/26/3599849.aspx
2 , C 语言中,结构体内部必须进行数组定义。 Java 中最好也这样做。
C 语言的结构体是一段连续的内存,内存的大小是编译时确定的。
因此,数组必须定义。否则编译不会成功。
对应的 Java 类中,我们也应该在类定义时为数组定义。尽管实际上在使用时再赋值也可以。
但是,强烈不建议你这样做。
如,上面
public UserStruct.ByValue[] users = new UserStruct.ByValue[100];
定义 100 个元素的数组,如果你不再类内部定义。
而在使用时定义,如果你没有正确赋值,没有定义为 100 个元素,就会出错。
从表面上看, CompanyStruct 类 占用的内存是:
NativeLong id
WString name ;
public UserStruct.ByValue[] users = new UserStruct.ByValue[100];
public int count ;
这 4 个元素占用的内存的总和。
由于 Java 的数组是一个对象, users 中实际保存的也应该是一个引用,也就是指针, 32bit 。
那么 CompanyStruct 类占用的内存就比对应的 C 结构体:
struct CompanyStruct{
long id;
wchar_t * name;
UserStruct users[100];
int count;
};
小很多。内存少用很多。
我在例 3 的说明中曾经说过:
“
Java 调用 dll 中的 C 函数,实际上就是一段内存作为函数的参数传递给 dll 。
Dll 以为这个参数就是 C 语言传过来的参数。
”
那么, JNA 怎么还能正确调用 C 函数呢。
事实上,在调用 C 函数时, JNA 会把 UserStruct 类的实例的所有数据全部 *100 倍。(我的例子里数组是 100 个,你的例子当然可以有不一样的数值)
这样, Java 中的结构体的内存量就和 C 语言的 CompanyStruct 结构体占据相同大小和结构的内存,从而正确调用 C 函数。
例 5 使用 JNA 调用使用嵌套 Struct 的指针的数组的 C 函数
现在给大家看看最复杂的 Struct 的例子。
Struct 中嵌套的是一个结构体的指针的数组。
C 语言代码
struct CompanyStruct2{
long id;
wchar_t * name;
UserStruct* users[100];
// UserStruct users[100];
int count;
};
MYLIBAPI void sayCompany2(CompanyStruct2* pCompanyStruct);
这里,把刚才使用的 UserStruct 数组换成 UserStruct 指针的数组。
JNA 代码
public static class CompanyStruct2 extends Structure{
public static class ByReference extends CompanyStruct2 implements Structure.ByReference { }
public NativeLong id ;
public WString name ;
public UserStruct.ByReference[] users = new UserStruct.ByReference[100];
public int count ;
}
public void sayCompany2(CompanyStruct2.ByReference pCompanyStruct);
测试代码:
CompanyStruct2.ByReference companyStruct2= new CompanyStruct2.ByReference();
companyStruct2. id = new NativeLong(2);
companyStruct2. name = new WString( "Yahoo" );
companyStruct2. count =10;
UserStruct.ByReference pUserStruct= new UserStruct.ByReference();
pUserStruct. id = new NativeLong(90);
pUserStruct. age =99;
pUserStruct. name = new WString( " 良少 " );
pUserStruct.write();
// TestDll1.INSTANCE.sayUser(pUserStruct);
for ( int i=0;i<companyStruct2. count ;i++){
companyStruct2. users [i]=pUserStruct;
}
TestDll1. INSTANCE .sayCompany2(companyStruct2);
程序说明 ----Pin 锁住 Java 对象:
因为是结构体的指针的数组,所以,我们使用了
public UserStruct.ByReference[] users=new UserStruct.ByReference[100];
来对应 C 语言中的
UserStruct* users[100];
但是,有问题,如果去除
pUserStruct.write();
这一行代码,就会报错。
如果去除 pUserStruct.write();
但是使用 TestDll1.INSTANCE.sayUser(pUserStruct);
也不会有问题。
这是怎么回事?
原来,错误的原因就是内存锁定!
java 内存锁定机制和 JNI , JNA 调用
我们知道, java 的内存是 GC 管理的。它会自动管理 JVM 使用的堆内存。删除不再使用的内存,并把 Java 对象使用的内存自动移动,以防止内存碎片增多。
因此,虽然 Java 的引用实际上就是指针,但还是和指针不同。 Java 中不能直接使用指针。因为对象在内存中的地址都不是固定的。说不准什么时候 GC 就把它给移位了。
如果使用 JNI , JNA 等技术调用 C 函数。那么就会有问题。因此, C 语言使用的是指针。如果想要获取 C 函数的返回值。那么我们必须提供一块内存给 C 语言访问。而 C 语言是不知道你 Java 的引用的。它只能访问固定的内存地址。
如果 GC 把 Java 对象移来移去,那么 C 函数就没办法和 Java 交互了。
因此,在使用 JNI 和 JNA 时,都会把 Java 对象锁住。 GC 不再管理。不删除,也不移动位置。由此出现的内存碎片,也不管了!
这种技术的术语是 PIN 。 .NET 也有同样的概念。
上面 TestDll1.INSTANCE.sayUser(pUserStruct); 这个调用是 JNA 调用。这个操作就把 pUserStruct 这个 Java 对象锁住了。
例 4 中,嵌套的是结构体,因此也会直接把 CompanyStruct 类的实例锁住。
但是例 5 中,嵌套的是结构体的指针的数组。 CompanyStruct 2 类的实例 companyStruct2 在执行
TestDll1.INSTANCE.sayCompany2(companyStruct2);
时是被锁住了,可以 companyStruct2 的 users 指针数组 的成员:
pUserStruct 都没有被锁住。
怎么办呢?
难道每一个 UserStruct.ByReference 的实例都先调用一遍不需要的
TestDll1.INSTANCE.sayUser(pUserStruct); 方法?
没事! JNA 开发人员早已想到了:
Structure 类中有方法:
write
public void write
()
Writes the fields of the struct to native memory
writeField
public void writeField
(
String
name)
Write the given field to native memory. The current value in the Java field will be translated into native memory.
Throws:
IllegalArgumentException
- if no field exists with the given name
这些 write 方法,会把 Java 的内存 Pin 住。 就是 C 语言可以使用。 GC 不再管理它们。
现在只要调用
pUserStruct.write();
把 java 模拟结构体实例给 Pin 住就可以了。
题外话, C# 定义了语法,可以使用关键字 pin 锁住 .NET 对象。
结论:
结构体是 C 语言模拟 OOP 开发中经常使用的一种数据组织形式。搞定了结构体 Struct ,我们就可以放心大胆、轻轻松松地把 C 程序随便拿来用了!