在JNI中调用本地带结构体参数的函数

说起JNI,《TheJavaNativeInterface--Programmer'sGuideandSpecification》我认为是挺好的入门教程。浅显易懂,而且也附有参考。对很多问题和陷阱也进行了讲解和提示。可以在Sun的官网上免费下载到这本书,下载地址:http://java.sun.com/docs/books/jni/download/jni.pdf。

但是我认为这本书在第9章LeveragingExistingNativeLibaries中对开头所讲的一段程序的解释有点潦草。内容大意是有一个Win32API,CreateFile。它带了很多参数。有constchar*、DWORD、HANDLE,最重要的是它带了一个SECURITY_ATTRIBUTES*,一个结构体指针。

HANDLE CreateFile(
	const char *fileName, // file name
	DWORD desiredAccess, // access (read-write) mode
	DWORD shareMode, // share mode
	SECURITY_ATTRIBUTES *attrs, // security attributes
	DWORD creationDistribution, // how to create
	DWORD flagsAndAttributes, // file attributes
	HANDLE templateFile // file with attr. to copy
);

这个SECURITY_ATTRIBUTES是自定义的。具体的定义就不给出了。书中也给出了对应的Java函数。

public class Win32 {
	public static native int CreateFile(
		String fileName, // file name
		int desiredAccess, // access (read-write) mode
		int shareMode, // share mode
		int[] secAttrs, // security attributes
		int creationDistribution, // how to create
		int flagsAndAttributes, // file attributes
		int templateFile); // file with attr. to copy
	...
}

但怪的是,作者使用了一个int[]来对应SECURITY_ATTRIBUTES*。对此,作者只有一段短小的解释。

引用

Becauseofpotentialdifferencesinhowfieldsarelaidoutinmemory,wedo

notmapCstructurestoclassesintheJavaprogramminglanguage.Instead,weuse

anarraytostorethecontentsoftheCstructureSECURITY_ATTRIBUTES.Thecaller

mayalsopassnullassecAttrstospecifythedefaultWin32securityattributes.

WewillnotdiscussthecontentsoftheSECURITY_ATTRIBUTESstructureorhowto

encodethatinanintarray.

我认为这个解释是为了让读者耐心读下去而写的。这里用int[]是说不通的。可以理解作者。书中这时的重点还是讲解如何调用一个普通函数。在读完这章的时候,我才明白,这里应该用后面所介绍的PeerClass来做。但我后面给出的解决方法并不是针对这个例子的。但是,看完后您可以自已写一个解决方法。因为后面的示例给出一个解决Java调用C/C++带结构体参数函数的思路。

PeerClass,用我的话来说就是一个Java类,它包含了一个C/C++对象的指针。一个通常的PeerClass长成这个样子:

public class PeerClass {

	private long peer;
	...

}

引用《TheJavaNativeInterface--Programmer'sGuideandSpecification》,java.io.FileDescriptor也包含一个intfd。用来保存对应本地结构体的一个指针。所以说,PeerClass的使命就是提供一个对C/C++结构体或类的一个包装,使得Java可以使用。故,解决上面CreateFile函数参数问题的办法就是针对SECURITY_ATTRIBUTES结构体定义一个包装类。具体的实现我用另外一个例子。因为CreateFile的签名太长,有点吓人(#o#)

假定,Java一段程序需要调用C++一个函数CppFunc(STRUCT*stru)。这个STRUCT包含一个long成员变量和一个char成员变量。CppFunc会打印出这个结构体实例中变量的值。为了构造一个这样的C++结构体,我们给它做一个包装类。也就是一个PeerClass。不妨叫StructWrapper。通过构造StructWrapper,Java程序给其对应的C++结构体赋值。再将这个StructWrapper实例传给Java里对CppFunc的包装函数,从而达到目的。

示例中的C++工程是一个Win32DLL。下面的代码展示可能为了逻辑的连贯性而把一个文件的内容分开来。读者实践的时候可以自行合并。

首先,定义C++的CppFunc和STRUCT结构体。

// Entry.cpp

using namespace std;

void CppFunc(STRUCT* stru)
{
	cout << "long value:\t" << stru->l << endl;
	cout << "char value:\t" << stru->c << endl;
}


// Struct.h

#ifndef _Included_STRUCT
#define _Included_STRUCT

typedef struct _STRUCT
{
	long l;
	char c;
} STRUCT;

#endif

再次,定义STRUCT的包装类StructWrapper。

// StructWrapper.java

public class StructWrapper {

	private long peer;
	
	public long getPeer() {
		return peer;
	}
	
	private native long initialize(long l, char c);
	
	private native void destroy(long peer);
	
	public StructWrapper(long l, char c) {
		peer = initialize(l, c);
	}
	
	public synchronized void destroy() {
		if (peer != 0) {
			destroy(peer);
			peer = 0;
		}
	}
	
	protected void finalize() {
		destroy();
	}
	
	static {
		System.loadLibrary("TestJNI");
	}
	
}

initialize函数的作用就是调用C++代码来初始化一个C++下的STRUCT对象,并返回这个对象的指针。StructWrapper在构造时把这个指针保存在peer中。这里peer其实可以用int。因为C++下指针是四字节,Java下int也是四字节。destroy设计为线程安全的原因是因为在finalize方法中会自动调用destroy。而用户也可能会手动销毁对象。有可能出现并发的情况。

还缺一个针对CppFunc的Java包装函数。

// Entry.java

public class Entry {
	
	private native void callCppFunc(long structWrapperPeer);

	public static void main(String[] args) {
		...
	}
	
}

callCppFunc(long)这个Java函数。它提供了一个CppFunc的包装。它所需要的参数实际上是StructWrapper实例中所保存的C++STRUCT结构体实例的地址。这样就实现了把Java对象传给C++。

大家看到,这里StructWrapper用到了几个本地函数。我们一样得在C++中实现它们。

// StructWrapper.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class StructWrapper */

#ifndef _Included_StructWrapper
#define _Included_StructWrapper
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     StructWrapper
 * Method:    initialize
 * Signature: (JC)J
 */
JNIEXPORT jlong JNICALL Java_StructWrapper_initialize
  (JNIEnv *, jobject, jlong, jchar);

/*
 * Class:     StructWrapper
 * Method:    destroy
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_StructWrapper_destroy
  (JNIEnv *, jobject, jlong);

#ifdef __cplusplus
}
#endif
#endif


// StructWrapper.cpp

#include "StructWrapper.h"
#include "Struct.h"

JNIEXPORT jlong JNICALL
Java_StructWrapper_initialize(JNIEnv* env, jobject self, jlong l, jchar c)
{
	STRUCT* peer = new STRUCT();
	peer->l = (long)l;
	peer->c = (char)c;
	return (jlong)peer;
}

JNIEXPORT void JNICALL
Java_StructWrapper_destroy(JNIEnv* env, jobject self, jlong peer)
{
	delete (STRUCT*)peer;
}

Okay,现在可以定义主函数了。

// Entry.java

public class Entry {
	
	private native void callCppFunc(long strutWrapperPeer);

	public static void main(String[] args) {
		StructWrapper struct = new StructWrapper(1, 'a');
		
		new Entry().callCppFunc(struct.getPeer());
		
		struct.destroy();
	}
	
}

其对应的C++实现,

// Entry.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Entry */

#ifndef _Included_Entry
#define _Included_Entry
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Entry
 * Method:    callCppFunc
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_Entry_callCppFunc
  (JNIEnv *, jobject, jlong);

#ifdef __cplusplus
}
#endif
#endif

// Entry.cpp

JNIEXPORT void JNICALL
Java_Entry_callCppFunc(JNIEnv* env, jobject self, jlong structWrapperPeer)
{
	STRUCT* structPointer = (STRUCT*)structWrapperPeer;
	CppFunc(structPointer);
}

如果顺利的话,您现在跑这个Java程序,应该输出

引用

longvalue:1

charvalue:a

至此,示例结束。但引出一个问题,如何把C++中的结构传给Java?一样,通过PeerClass。我觉得各位肯定会举一反三,想出解决办法的。

相关推荐