农场老马的专栏

老码农一枚!

JNI官方规范中文版——如何在JNI中使用己有的C/C++库

JNI的一个使用方式就是编写一些本地方法来使用己有的本地库。本章介绍了一种生成一个包含一系列本地函数的类库的经典的方式。

本章首先用一对一映射这种(one-to-one mapping)最直接的方式来写封装类.接下来,我们会介绍一种叫做共享stubs(shared stubs)的技术来简化编写封装类的任务。然后,在本章的最后,我们会讨论怎么样使用peer classes来封装本地数据结构。

本章介绍的方式都是通过本地方法直接使用一个本地库,这样的话,应用程序调用本地方法时会依赖于本地库。这样应用程序只能运行在支持这个本地库的操作系统上面。一个更好的办法是声明一些与操作系统无关的本地方法,让这些方法来调用本地库。这样,当我们移植程序时,只需要修改这些实现中间层本地方法的本地函数就可以了,而不必动应用程序和这些中间层本地方法。

9.1 一对一映射(one-to-one mapping)

我们从一个简单的例子开始。假设我们想写一个封装类,它向标准C库提供atol函数:long atol(const char *str);

这个函数解析一个字符串并返回十进制数字。首先,我们像下面这样写:

public class C {

     public static native int atol(String str);

     ...

 }

为了演示如何使用C++进行JNI编程,我们用C++来实现本地方法:

JNIEXPORT jint JNICALL 

 Java_C_atol(JNIEnv *env, jclass cls, jstring str)

 {

     const char *cstr = env->GetStringUTFChars(str, 0);

     if (cstr == NULL) {

         return 0; /* out of memory */

     }

     int result = atol(cstr);

     env->ReleaseStringUTFChars(str, cstr);

     return result;

 }

9.2 Shred Stubs

一对一映射要求你为每一个你想封装的本地函数写一个stub函数,那么,当你需要为大量本地函数写封装类时,你的工作会很烦琐。本节中,我们介绍shared stubs的思想来简化工作量。

Shared stubs负责把调用者的请求分发到相应的本地函数,并负责把调用者提供的参数类型转化成本地函数需要的类型。

我们先看一下shared stubs怎么样简化C.atol方法的实现,然后还会介绍一个使用了shared stub思想的类CFunction。

public class C {

     private static CFunction c_atol =

         new CFunction("msvcrt.dll", // native library name

                       "atol",       // C function name

                       "C");         // calling convention

     public static int atol(String str) {

         return c_atol.callInt(new Object[] {str});

     }

     ...

 }

C.atol不再是一个本地方法,而是使用CFunction类来定义。这个类内部实现了一个shared stub。静态变量C.c_atol存储了一个CFunction对象,这个对象对应了msvcrt.dll库中的C函数atol。一旦c_atol这个字段初始化,对C.atol的调用只需要调用c_atol.callInt这个shared stub。

一个CFunction类代表一个指向C函数的指针。

CFunction的类层次结构图如下:

public class CFunction extends CPointer {

     public CFunction(String lib,     // native library name

                      String fname,   // C function name

                      String conv) {  // calling convention

         ...

     }

     public native int callInt(Object[] args);

     ...

 }

callInt方法接收一个java.lang.Object对象的数组作为参数,它检查数组中每一个元素的具体类型,并把它们转化成相应的C类型(比如,把String转化成char*)。然后把它们传递给相应的C函数,最后返回一个int型的结果。CFunction类还可以定义许多类似的方法,如callFloat、callDouble等来处理其它返回类型的C函数。

CPointor的定义如下:

public abstract class CPointer {

     public native void copyIn(

              int bOff,     // offset from a C pointer

              int[] buf,    // source data

              int off,      // offset into source

              int len);     // number of elements to be copied

     public native void copyOut(...);

     ...

 }

CPointer是一个抽象类,它支持对任意C指针的访问。例如copyIn这个方法,它会把一个int数组里面的元素复制到C指针指向的位置中去。但是这种操作方式可以访问地址空间里面任意的内存位置,一定要小心地使用。像CPointer.copyIn这样的本地方法可对直接对C指针进行操作,是不安全的。CMalloc是CPointer的一个子类,它指向内存中由malloc在heap上分配的一块儿内存。

public class CMalloc extends CPointer {

     public CMalloc(int size) throws OutOfMemoryError { ... }

     public native void free();

     ...

 }

CMalloc的构造函数根据给定的大小,在C的heap上创建一块儿内存。CMalloc.free方法用来释放这个内存块儿。我们可以用CFunction和CMalloc重新实现Win32.CreateFile:

public class Win32 {

     private static CFunction c_CreateFile = 

         new CFunction ("kernel32.dll",   // native library name

                        "CreateFileA",    // native function

                        "JNI");           // calling convention

 

     public static 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

     {

         CMalloc cSecAttrs = null;

         if (secAttrs != null) {

             cSecAttrs = new CMalloc(secAttrs.length * 4);

             cSecAttrs.copyIn(0, secAttrs, 0, secAttrs.length);

         }

         try {

             return c_CreateFile.callInt(new Object[] {

                            fileName,

                            new Integer(desiredAccess),

                            new Integer(shareMode),

                            cSecAttrs,

                            new Integer(creationDistribution),

                            new Integer(flagsAndAttributes),

                            new Integer(templateFile)});

         } finally {

             if (secAttrs != null) {

                 cSecAttrs.free();

             }

         }

     }

     ...

 }

我们在一个静态变量当中缓存CFunction对象,Win32的CreateFile 这个API从kernel32.dll中通过调用方法CreateFileA来访问,另外一个方法CreateFileW需要传入一个Unicode字符串参数作为文件名。CFunction负责做标准的Win32调用转换(stdcall)。

上面的代码中,首先在C的heap上面分配一个足够大的内存块儿来存储安全属性,然后把所有的参数打包成一个数组并通过CFunction这个函数调用处理器来调用底层的C函数CreateFileA。最后释放掉存储安全属性的C内存块儿。

9.3 一对一映射(one-to-one mapping)和Shared Stubs的对比

这是两种把本地库封装成包装类的方式,各有自己的优点。

Shared Stubs的主要优点是程序员不必在本地代码中写一大堆的stub函数。一旦像CFunction这样的shared stub被创建以后,程序员可能就不用写代码了。

但是,使用shared stubs时一定要非常小心,因为这相当于程序员在JAVA语言中写C代码,已经违反了JAVA中的类型安全机制。一旦使用的过程中出现错误,就有可能引起内存破坏甚至程序崩溃。

一对一映射的优点是高效,因为它不需要太多附加的数据类型转换。而这一点正是shared stubs的缺点,例如CFunction.callInt必须为每一个int创建一个Integer对象。

9.4 如何实现Shared Stubs

到现在为止,我们一直是把CFunction、CPointer、CMalloc这三个类当作黑匣子的。本节中,我们就来详细描述一下它们是如何使用JNI实现的。

9.4.1 CPointer的实现

抽象类CPointer包含了一个64位的字段peer,它里面存放的是一个C指针:

public abstract class CPointer {

     protected long peer;

     public native void copyIn(int bOff, int[] buf,

                               int off,int len);

     public native void copyOut(...);

     ...

 }

对于像copyIn这样的本地方法的C++实现是比较简明的:

JNIEXPORT void JNICALL

 Java_CPointer_copyIn__I_3III(JNIEnv *env, jobject self,

     jint boff, jintArray arr, jint off, jint len)

 {

     long peer = env->GetLongField(self, FID_CPointer_peer);

     env->GetIntArrayRegion(arr, off, len, (jint *)peer + boff);

 }

在这里,我们假设FID_CPointer_peer是CPointer.peer的字段ID,是被提前计算出来。

9.4.2 CMalloc

CMalloc这个类中添加了两个本地方法用来分配和释放C内存块儿:

public class CMalloc extends CPointer {

     private static native long malloc(int size);

     public CMalloc(int size) throws OutOfMemoryError {

         peer = malloc(size);

         if (peer == 0) {

             throw new OutOfMemoryError();

         }

     }

     public native void free();

     ...

 }

这个类的构造方法调用了本地方法CMalloc.malloc,如果CMalloc.malloc分配失败的话,会抛出一个OutOfMemoryError。我们可以像下面这样实现CMalloc.malloc和CMalloc.free两个方法:

JNIEXPORT jlong JNICALL

 Java_CMalloc_malloc(JNIEnv *env, jclass cls, jint size)

 {

     return (jlong)malloc(size);

 }

 

 JNIEXPORT void JNICALL

 Java_CMalloc_free(JNIEnv *env, jobject self)

 {

     long peer = env->GetLongField(self, FID_CPointer_peer);

     free((void *)peer);

 }

9.4.3 CFunction

这个类的实现要求操作系统支持动态链接,下面的代码是针对Win32/Intel X86平台的。一旦你理解了CFunction这个类背后的设计思想,你可以把它扩展到其它平台。

public class CFunction extends CPointer {

     private static final int CONV_C = 0;

     private static final int CONV_JNI = 1;

     private int conv;

     private native long find(String lib, String fname);

 

     public CFunction(String lib,     // native library name

                      String fname,   // C function name

                      String conv) {  // calling convention

         if (conv.equals("C")) {

             conv = CONV_C;

         } else if (conv.equals("JNI")) {

             conv = CONV_JNI;

         } else {

             throw new IllegalArgumentException(

                         "bad calling convention");

         }

         peer = find(lib, fname);

     }

     public native int callInt(Object[] args);

     ...

 }

类中使用了一个conv字段来保存C函数的调用转换类型。

JNIEXPORT jlong JNICALL

 Java_CFunction_find(JNIEnv *env, jobject self, jstring lib,

                     jstring fun)

 {

     void *handle;

     void *func;

     char *libname;

     char *funname;

 

     if ((libname = JNU_GetStringNativeChars(env, lib))) {

         if ((funname = JNU_GetStringNativeChars(env, fun))) {

             if ((handle = LoadLibrary(libname))) {

                 if (!(func = GetProcAddress(handle, funname))) {

                     JNU_ThrowByName(env, 

                         "java/lang/UnsatisfiedLinkError",

                         funname);

                 }

             } else {

                 JNU_ThrowByName(env, 

                         "java/lang/UnsatisfiedLinkError",

                         libname);

             }

             free(funname);

         }

         free(libname);

     }

     return (jlong)func;

 }

CFunction.find把库名和函数名转化成本地C字符串,然后调用Win32下的APILoadLibrary和GetProcAddress来定义本地库中的函数。

方法callInt的实现如下:

JNIEXPORT jint JNICALL

 Java_CFunction_callInt(JNIEnv *env, jobject self,

                        jobjectArray arr)

 {

 #define MAX_NARGS 32

     jint ires;

     int nargs, nwords;

     jboolean is_string[MAX_NARGS];

     word_t args[MAX_NARGS];

 

     nargs = env->GetArrayLength(arr);

     if (nargs > MAX_NARGS) {

         JNU_ThrowByName(env, 

                     "java/lang/IllegalArgumentException",

                     "too many arguments");

         return 0;

     }

 

     // convert arguments

     for (nwords = 0; nwords < nargs; nwords++) {

         is_string[nwords] = JNI_FALSE;

         jobject arg = env->GetObjectArrayElement(arr, nwords);

 

         if (arg == NULL) {

             args[nwords].p = NULL;

         } else if (env->IsInstanceOf(arg, Class_Integer)) {

             args[nwords].i =

                 env->GetIntField(arg, FID_Integer_value);

         } else if (env->IsInstanceOf(arg, Class_Float)) {

             args[nwords].f =

                 env->GetFloatField(arg, FID_Float_value);

         } else if (env->IsInstanceOf(arg, Class_CPointer)) {

             args[nwords].p = (void *)

                 env->GetLongField(arg, FID_CPointer_peer);

         } else if (env->IsInstanceOf(arg, Class_String)) {

             char * cstr =

                 JNU_GetStringNativeChars(env, (jstring)arg);

             if ((args[nwords].p = cstr) == NULL) {

                 goto cleanup; // error thrown

             }

             is_string[nwords] = JNI_TRUE;

         } else {

             JNU_ThrowByName(env,

                 "java/lang/IllegalArgumentException",

                 "unrecognized argument type");

             goto cleanup;

         }

         env->DeleteLocalRef(arg);

     }

     void *func = 

         (void *)env->GetLongField(self, FID_CPointer_peer);

     int conv = env->GetIntField(self, FID_CFunction_conv);

 

     // now transfer control to func.

     ires = asm_dispatch(func, nwords, args, conv);

 

 cleanup:

     // free all the native strings we have created

     for (int i = 0; i < nwords; i++) {

         if (is_string[i]) {

             free(args[i].p);

         }

     }

     return ires;

 }

上面的代码中我们假设已经有了一些全局变量来缓存一些类引用和字段ID。例如,全局变量FID_CPointer_peer缓存了CPointer.peer的字段ID,而全局变量Class_String是对java.lang.String类对象的全局引用。类型word_t定义如下:

typedef union {

     jint i;

     jfloat f;

     void *p;

 } word_t;

函数Java_CFunction_callInt遍历参数数组并检查每一个元素的类型。

1、 如果元素是null,向C函数传递一个NULL指针。

2、 如果参数是java.lang.Integer类的实例,取出其中的int值并传递给C函数。

3、 如果元素是java.lang.Float类的实例,取出其中的float值传递给C函数。

4、 如果元素是一个CPointer类的实例,取出其中的peer指针并传递给C函数。

5、 如果参数是一个java.lang.String的实例,则把字符串转换成本地C字符串,然后传递给C函数。

6、 否则的话,抛出IllegalArgumentException

Java_CFunction_callInt函数之前,我们会在参数转换时检查可能会发生的错误,然后释放掉为C字符串临时分配的内存。

下面的代码需要把参数从临时缓冲区args中传递到C函数中,这个过程需要直接操作C的栈(stack),因此需要用到汇编,代码和对代码的解释不再翻译,懂得不多,翻译出来也是莫名其妙,不能保证正确性。

9.5 Peer

无论哪种封装方式,都会遇到一个问题,就是数据结构的传递。我们先看一下CPointer这个类的定义。

public abstract class CPointer {

     protected long peer;

     public native void copyIn(int bOff, int[] buf,

                               int off, int len);

     public native void copyOut(...);

     ...

 }

这个类中包含了一个指向本地数据结构的64位的peer字段。CPointer的子类用这个指针来操作C里面的数据结构:

CPointerCMalloc这些类被称作peer classes。你可以用这些类封装各种各样的本地数据结构,如:

1、 文件描述符(file descriptors)。

2、 Socket描述符(socket descriptors)。

3、 窗口或者其它UI元素。

9.5.1 JAVA平台下的Peer Classes

JDK中,java.iojava.netjava.awt等包的内部实现就是利用了peer classes。例如,一个java.io.FileDescriptor类的实例,其实就包含了一个私有的字段fd,而fd这个字段就是指向一个本地文件描述符。

// Implementation of the java.io.FileDescriptor class

 public final class FileDescriptor {

     private int fd;

     ...

 }

假如现在你想做一个JAVA平台的文件API不支持的操作,你可能就会通过本地方法中的JNI来找到一个java.io.FileDescriptor中的fd字段,然后试图去操作这个字段所代表的文件。这样会存在一些问题:

1、 首先,这种方式严重依赖于java.io.FileDescriptor的实现,如果有一天这个类的内部发生了变动,本地方法就要修改。

2、 你直接操作fd字段可能会破坏java.io.FileDescriptor内部的完整性。比如内部实现中,fd字段可能会和其它某个数据相关联。

解决这些问题最根本的方案就是定义你自己的peer classes来封装本地数据结构。在上面的情况中,你可以定义自己的peer class来包含file descriptor,并在这个peer class上面定义一些自己的操作。并且,你也可以很容易地定义一个自己的peer class来实现一个标准的JAVA API中的接口。

9.5.2 释放本地数据结构

Peer classes被定义在JAVA中,因此它们的实例对象会被自动回收,因此,你要保证在这些对象被回收的时候,它们所指向的C语言数据结构的内存块也要被释放。

前面提到过,CMalloc类包含一个用来手动释放被malloc分配的C内存的free方法:

public class CMalloc extends CPointer {

     public native void free();

     ...

 }

所以,有些人爱这么干:

public class CMalloc extends CPointer {

     public native synchronized void free();

     protected void finalize() {

         free();

     }

     ...

 }

JVM在回收CMalloc的对象实例之前,会调用对象的finalize方法。这样的话,即使你忘记调用freefinalize方法也会帮你释放掉malloc分配的内存。

可是,为了防止本地方法被重复调用,你不仅要在free方法前面加上synchronized关键字,还需要对CMalloc.free这个本地方法的实现做一些修改:

JNIEXPORT void JNICALL

 Java_CMalloc_free(JNIEnv *env, jobject self)

 {

     long peer = env->GetLongField(self, FID_CPointer_peer);

     if (peer == 0) {

         return; /* not an error, freed previously */

     }

     free((void *)peer);

     peer = 0;

     env->SetLongField(self, FID_CPointer_peer, peer);

 }

请注意,要设置peer的值的话,需要用两句来完成:

peer = 0;

env->SetLongField(self, FID_CPointer_peer, peer);

而不是一句:

env->SetLongField(self, FID_CPointer_peer, 0);

因为C++编译器会把0当作32int值来处理。

另外,定义finalize方法是一个很好的保障措施,但决不能把它作为释放本地C语言数据结构的主要方式:

1、 是本地数据结构可能会消耗比它们的peer对象实例更多的资源,但JVM看在眼里的是,这个对象只有一个long型的字段,这样JVM可能就会以为它占用很少的资源而不会及时回收掉。

2、 定义了finalize方法的类,在对象的创建和回收时可能会比没有定义finalize方法的类在效率上要差些。

其实,你完全不必用finalize方法就可以手动保证一个本地C语言数据结构被释放。但这样的话,你就必须确保在所有的执行路径上面都要执行释放代码,否则可能会造成内存泄漏。比如下面这种情况就是需要提起注意的:

CMalloc cptr = new CMalloc(10);

 try {

     ... // use cptr

 } finally {

     cptr.free();

 }

9.5.3 peer对象背后的东西

前面我们介绍了一个peer class通常会包含一个指向本地数据结构的私有字段。其实,有些情况下,在本地数据结构中包含一个指向peer class的引用也是很有用的。比如,当本地代码需要回调peer class中的实例方法的时候。

假设KeyInput是一个UI控件:

class KeyInput {

     private long peer;

     private native long create();

     private native void destroy(long peer);

     public KeyInput() {

         peer = create();

     }

     public destroy() {

         destroy(peer);

     }

     private void keyPressed(int key) {

         ... /* process the key event */

     }

 }

还有一个本地数据结构key_input

// C++ structure, native counterpart of KeyInput

 struct key_input {

     jobject back_ptr;         // back pointer to peer instance

     int key_pressed(int key); // called by the operating system

 };

它们的关系如下:

整个流程是这样的,JAVA当中生成一个KeyInput对象用来处理按键。KeyInput对象生成的时候,会在本地内存中创建一个key_input结构,这个结构中包含一个方法key_pressed供操作系统在发生事件时调用。

当用户按某个键时,操作系统产生一个事件,并调用key_pressed(int key);,在这个方法里面,本地代码会调用KeyInputkeyPressed方法,并把键值传入。

KeyInput的两个本地方法实现如下:

JNIEXPORT jlong JNICALL

 Java_KeyInput_create(JNIEnv *env, jobject self)

 {

     key_input *cpp_obj = new key_input();

     cpp_obj->back_ptr = env->NewGlobalRef(self);

     return (jlong)cpp_obj;

 }

 

 JNIEXPORT void JNICALL

 Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer)

 {

     key_input *cpp_obj = (key_input*)peer;

     env->DeleteGlobalRef(cpp_obj->back_ptr);

     delete cpp_obj;

     return;

 }

本地方法create生成一个C++结构key_input,并初始化back_ptr字段。其中back_ptr是一个全局引用,指向KeyInput这个peer class对象的实例。本地方法destroy删除指向KeyInput对象的引用和KeyInput指向的本地数据结构。KeyInput构造方法调用本地方法create来建立KeyInput这个对象实例和它的副本key_input这个本地数据结构之间的链接。

Key_input::key_pressed(int key)方法的实现如下:

// returns 0 on success, -1 on failure

 int key_input::key_pressed(int key)

 {

     jboolean has_exception;

     JNIEnv *env = JNU_GetEnv();

     JNU_CallMethodByName(env, 

                          &has_exception,

                          java_peer,

                          "keyPressed",

                          "()V",

                          key);

     if (has_exception) {

         env->ExceptionClear();

         return -1;

     } else {

         return 0;

     }

 }

本节结束之间,我们还有最后一个话题需要讨论。假设,你为KeyInput类添加了一个finalize方法来避免内存泄漏。

class KeyInput {

     ...

     public synchronized destroy() {

         if (peer != 0) {

             destroy(peer);

             peer = 0;

         }

     }

     protect void finalize() {

         destroy();

     }

 }

考虑到多线程的情况,destroy方法被加上了synchronized关键字。但是,上面的代码不会像你期望的那样执行的,因为JVM永远不会回收KeyInput这个对象,除非你手动调用destory方法。因为,KeyInput的构造方法创建了一个到KeyInput对象的JNI全局引用,这个全局引用会阻止GC回收KeyInput的。解决办法就是使用弱引用来替代全局引用:

JNIEXPORT jlong JNICALL

 Java_KeyInput_create(JNIEnv *env, jobject self)

 {

     key_input *cpp_obj = new key_input();

     cpp_obj->back_ptr = env->NewWeakGlobalRef(self);

     return (jlong)cpp_obj;

 }

 

 JNIEXPORT void JNICALL

 Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer)

 {

     key_input *cpp_obj = (key_input*)peer;

     env->DeleteWeakGlobalRef(cpp_obj->back_ptr);

     delete cpp_obj;

     return;

 }

阅读更多
个人分类: JNI规范
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭