JNI原理2

15.2  调用C程序

JNI规范最初便是针对Java调用C语言的,因此对C程序的调用具有一套约定俗成的步骤:

(1)编写作为主调方的Java类。Java类一方面声明将要调用的C函数,一方面载入本地的动态链接库文件(即.dll文件)。

(2)使用javac命令编译Java类。

(3)使用javah命令为C程序生成头文件(即.h文件)。在自动生成的头文件中将声明有待实现的C函数。

(4)编写C程序。在C程序中实现头文件中声明的函数。

(5)将C程序文件编译成动态链接库文件。任何C编译工具都能用来完成此项工作,如Windows平台的VC++和BCB、Linux平台的gcc、Solaris平台的cc。

15.2.1  在Windows平台上调用C函数

本章选择Windows平台作为实战JNI的第一站,本节我们将在Windows平台上完整地实现一个最简单的Java程序调用C函数的范例。使用的C程序编译工具是VC++;为了方便读者们理解,本节内容在编排上严格按照上述的开发步骤进行。

我们仍然以著名的“Hello World”为例,所不同之处在于,这次发出问候语的是动态链接库中的C函数,而非Java程序本身,Java程序仅仅负责调用C函数而已。

作为主调方的Java程序源代码如下。

代码清单15-1  在Windows平台上调用C函数的例程——HelloWorld

1.       public class HelloWorld

2.       {

3.         public native void displayHelloWorld();//定义本地方法

4.         public native void displayMyName();//定义本地方法

5.          

6.         static

7.         {

8.           System.loadLibrary("hello");//调入本地库

9.         }

10.     

11.      public static void main(String[] args)

12.      {

13.        new HelloWorld().displayHelloWorld();

14.        new HelloWorld().displayMyName();

15.      }

16.    }

观察这段Java程序,在结构上与普通的Java程序的不同之处在于,它首先声明了两个native方法——与在Interface中的声明方法很类似,但是两者却有本质的区别:Interface中声明的只是方法的结构而已,可以认为是对方法做出的定义,Interface自身并不实现方法,更没有能力提供方法;而native方法却可以认为是Class自身提供的方法,只不过这种方法不是由自身来实现的,而是依靠本地动态链接库输出的函数。

在接下来的部分是一段静态(static)代码,载入动态链接库,之前声明的native方法就将在载入的动态链接库中寻找。如果native方法分散在多个动态链接库中,则需要载入多个动态链接库。本例程载入的动态链接库命名为“hello”,这意味着在Windows平台上,Java程序将在java.library.path参数所指向的路径,以及PATH环境变量所指向的路径中寻找并载入hello.dll文件。

除了使用System.loadLibrary()来载入动态链接库之外,还可以使用System.load()以绝对路径的方式载入动态链接库,例如:System.load (“e:/somelibrary.dll”)。在采用System.load()的情况下,不必设置环境变量和参数。

 

抛开本地方法如何实现不说,我们可以看到在main()方法中,调用native方法和调用普通方法是完全相同的。

使用JDK提供的命令行编译HelloWord类、为准备提供函数的(尚未实现的)C程序生成头文件的命令行如下:

javac HelloWorld.java

javah HelloWorld

读者们不妨打开生成的头文件HelloWorld.h阅读,但是请不要修改。

代码清单15-2  在Windows平台上调用C函数的例程——HelloWorld.h

1.       /* DO NOT EDIT THIS FILE - it is machine generated */

2.       #include <jni.h>

3.       /* Header for class HelloWorld */

4.        

5.       #ifndef _Included_HelloWorld

6.       #define _Included_HelloWorld

7.       #ifdef __cplusplus

8.       extern "C" {

9.       #endif

10.    /*

11.     * Class:     HelloWorld

12.     * Method:    displayHelloWorld

13.     * Signature: ()V

14.     */

15.    JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld

16.      (JNIEnv *, jobject);

17.     

18.    /*

19.     * Class:     HelloWorld

20.     * Method:    displayMyName

21.     * Signature: ()V

22.     */

23.    JNIEXPORT void JNICALL Java_HelloWorld_displayMyName

24.      (JNIEnv *, jobject);

25.     

26.    #ifdef __cplusplus

27.    }

28.    #endif

29.    #endif

头文件定义了两个函数:Java_HelloWorld_displayHelloWorld()和Java_HelloWorld_ displayMyName()。javah工具在生成的头文件中,在方法名前面加上了“Java_类名”。另外请注意,JNI规范中为所有的函数均加上了两个参数:一个是指向JNIEnv类型的指针,另一个是jobject类型的变量。JNIEnv和jobject类型都是在jni.h中定义的,jni.h由各个平台的JDK自带(存在于JDK\include目录下)。本例程没有使用到这两个参数,更复杂的程序将涉及它们,我们放在下一节介绍。

本章的Java程序均没有打包,即没有定义Package,这是为了方便读者们理解。在实际的应用开发中,Java类一般需要打包,这时利用javah生成的头文件中的函数名将类似于“Java_PackageName_ClassName_Method Name()”。在实现这些函数时,自动生成的函数名请不要改动。

 

以上结论同样适用于本章后面将要介绍的“调用Delphi程序”的相关内容。

按照既定的步骤,接下来我们要编写C程序来实现这两个函数了。源代码如下。

代码清单15-3  在Windows平台上调用C函数的例程——hello.c

1.       #include <stdio.h>

2.       #include <jni.h>

3.       #include <HelloWorld.h>

4.        

5.       JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj)

6.       {

7.         printf("Hello world!\n");

8.         return;

9.       }

10.     

11.    JNIEXPORT void JNICALL Java_HelloWorld_displayMyName(JNIEnv *env, jobject obj)

12.    {

13.      printf("我叫XXX!\n");

14.      return;

15.    }

负责实现输出函数的hello.c首先引入了两个头文件,一个是JDK自带的jni.h,另一个则是使用javah工具产生的HelloWorld.h。

在Windows平台上可以选用VC++编译hello.c,从而产生最终需要的hello.dll动态链接库文件,在编译过程中需要引入jni.h等JDK为Windows平台提供的头文件。编译命令行如下:

cl -If:\jdk\include -If:\jdk\include\win32 -LD hello.c -Fehello.dll

命令行中的cl.exe是VC++提供的编译工具,存在于VC++\bin目录下。涉及的命令选项说明如下。

—  -I:指引入(Include)必要的头文件所在的路径。请读者们根据自己机器上JDK安装的路径自行调整路径;如果HelloWorld.h不在当前目录下,也请通过“-I”选项予以引入。

—  -LD:指载入(Load)源文件。

—  -Fe:编译后输出的文件。

如果一切无误的话,在当前目录下将产生hello.dll动态链接库文件。通过以下命令行执行Java程序:

java HelloWorld

如果执行过程中发生如下异常:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no hello in java.library.path

at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1410)

at java.lang.Runtime.loadLibrary0(Runtime.java:772)

at java.lang.System.loadLibrary(System.java:832)

则意味着hello.dll无法被载入,可以通过以下三种途径解决这个问题:

— 将hello.dll放置在当前目录下。

— 将hello.dll放置在PATH环境变量所指向的路径下。

— 启动JVM时指定选项“-Djava.library.path”,将hello.dll放置在该选项所指向的路径下。

15.2.2  在Linux平台上调用C函数

15.2.2.1  gcc简介

在Linux环境下,我们首先要通过某种编辑器编辑程序的源文件,比如常用的VIM和EMACS。如果要编译一个C语言源程序,我们可以使用GNU的gcc(GNU Compiler Collection)编译器。假设我们有一个非常简单的源程序hello.c,要编译这个程序,我们只要在命令行下执行:

gcc -o hello hello.c

gcc编译器就会为我们生成一个名为hello的可执行文件。执行“./hello”就可以看到程序的输出结果了。命令行中的“-o”选项表示我们要求编译器将我们输出的可执行文件命名为hello,而hello.c 是我们的源程序文件。

gcc编译器有许多选项,一般来说我们只要知道其中的几个常用的就够了:

—  -o选项如上所述,表示要求输出的可执行文件名。

—  -c选项表示只要求编译器输出目标代码,而不必要输出可执行文件。

—  -g选项表示要求编译器在编译的时候提供对程序进行调试的信息。

假设有一套程序共由5个文件组成:main.c、too11.h、tool1.c、tool2.h和tool2.c。其中tool1.c和tool2.c是供main.c调用的工具程序,它们所包含的函数分别在tool1.h和tool2.h中定义。则编译的过程如下:

gcc -c main.c #编译产生main.o目标文件

gcc -c tool1.c #编译产生tool1.o目标文件

gcc -c tool2.c #编译产生tool2.o目标文件

gcc -o main main.o tool1.o tool2.o #将三个目标文件连接成可执行文件main

如果源文件数量多的话,在修改调试过程中难免重复执行上述步骤,这样耗时耗力。解决办法是引入Makefile文件。Makefile文件是编译规则的配置文件,它的一般格式是:

目标:依赖文件列表

  规则列表

第1行是声明目标(可能是可执行文件或者目标文件)所依赖的文件(可能是源文件或者目标文件)列表;第2行规定的是第1行中的依赖文件中的任何一个发生变化后需要执行的编译动作。就上面的例子来说,可能的Makefile文件是:

main:main.o tool1.o tool2.o

gcc -o main main.o tool1.o tool2.o

main.o:main.c tool1.h tool2.h

gcc -c main.c

tool1.o:tool1.c tool1.h

gcc -c tool1.c

tool2.o:tool2.c tool2.h

gcc -c tool2.c

当引入Makefile文件之后,即使源文件数量多也不用执行过多的命令行,而只需输入一个命令即可通知gcc依据Makefile文件所规定的依赖关系自动发现需要重新编译的文件:

make

15.2.2.2  简单例程

本节的例程和第15.2.1节的例程复杂程度相当,只是切换到了Linux平台上,目的是为了帮助读者们熟悉Linux下的Java+C开发环境。

作为主调方的Java源程序TestJNI.java如下。

代码清单15-4  在Linux平台上调用C函数的例程——TestJNI.java

1.       public class TestJNI

2.       {

3.         static

4.         {

5.           System.loadLibrary("testjni");//载入静态库,test函数在其中实现

6.         }

7.        

8.         private native void testjni(); //声明本地调用

9.         

10.      public void test()

11.      {

12.        testjni();

13.      }

14.     

15.      public static void main(String args[])

16.      {

17.        TestJNI haha = new TestJNI();

18.        haha.test();

19.      }

20.    }

TestJNI.java声明从libtestjni.so(注意Linux平台的动态链接库文件的扩展名是.so)中调用函数testjni()。

在Linux平台上,遵循JNI规范的动态链接库文件名必须以“lib”开头。例如在上面的Java程序中指定的库文件名为“testjni”,则实际的库文件应该命名为“libtestjni.so”。

 

编译TestJNI.java,并为C程序生成头文件:

java TestJNI.java

javah TestJNI

提供testjni()函数的testjni.c源文件如下。

代码清单15-5  在Linux平台上调用C函数的例程——testjni.c

1.       #include <stdio.h>

2.       #include <TestJNI.h>

3.        

4.       JNIEXPORT void JNICALL Java_TestJNI_testjni(JNIEnv *env, jobject obj)

5.       {

6.         printf("haha---------go into c!!!\n");

7.       }

编写Makefile文件如下,JDK安装的位置请读者自行调整:

libtestjni.so:testjni.o

     gcc -rdynamic -shared -o libtestjni.so testjni.o

testjni.o:testjni.c TestJNI.h

     gcc -c testjni.c -I./ -I/usr/java/jdk1.6.0_00/include -I/usr/java/jdk1.6.0_00/include/linux

在Makefile文件中,我们描述了最终的libtestjin.so依赖于目标文件testjni.o,而testjni.o则依赖于testjni.c源文件和TestJNI.h头文件。请注意,我们在将testjni.o连接成动态链接库文件时使用了“-rdynamic”选项。

执行make命令编译testjni.c。Linux平台和在Windows平台上类似,有3种方法可以让Java程序找到并装载动态链接库文件。

— 将动态链接库文件放置在当前路径下。

— 将动态链接库文件放置在LD_LIBRARY_PATH环境变量所指向的路径下。注意这一点和Windows平台稍有区别,Windows平台参考PATH环境变量。

— 在启动JVM时指定选项“-Djava.library.path”,将动态链接库文件放置在该选项所指向的路径下。

从下一节开始,我们开始接触到在JNI框架内Java调用C程序的一些高级话题,包括如何传递参数、如何传递数组、如何传递对象等。

各种类型数据的传递是跨平台、跨语言互操作的永恒话题,更复杂的操作其实都可以分解为各种基本数据类型的操作。只有掌握了基于各种数据类型的互操作,才能称得上掌握了JNI开发。从下一节开始,环境和步骤不再是阐述的重点,将不再花费专门的篇幅,例程中的关键点将成为我们关注的焦点。

15.2.2.3  传递字符串

到目前为止,我们还没有实现Java程序向C程序传递参数,或者C程序向Java程序传递参数。本例程将由Java程序向C程序传入一个字符串,C程序对该字符串转成大写形式后回传给Java程序。

Java源程序如下。

代码清单15-6  在Linux平台上调用C函数的例程——Sample1

1.       public class Sample1

2.       {

3.         public native String stringMethod(String text);

4.         

5.         public static void main(String[] args)

6.         {

7.           System.loadLibrary("Sample1");

8.           Sample1 sample = new Sample1();

9.           String  text   = sample.stringMethod("Thinking In Java");

10.        System.out.println("stringMethod: " + text);

11.      }

12.    }

Sample1.java以“Thinking In Java”为参数调用libSample1.so中的函数stringMethod(),在得到返回的字符串后打印输出。

Sample1.c的源程序如下。

代码清单15-7  在Linux平台上调用C函数的例程——Sample1.c

1.       #include <Sample1.h>

2.       #include <string.h>

3.        

4.       JNIEXPORT jstring JNICALL Java_Sample1_stringMethod(JNIEnv *env, jobject obj, jstring string)

5.       {

6.           const char *str = (*env)->GetStringUTFChars(env, string, 0);

7.           char cap[128];

8.           strcpy(cap, str);

9.           (*env)->ReleaseStringUTFChars(env, string, str);

10.        int i=0;

11.        for(i=0;i<strlen(cap);i++)

12.          *(cap+i)=(char)toupper(*(cap+i));

13.        return (*env)->NewStringUTF(env, cap);

14.    }

首先请注意函数头部分,函数接收一个jstring类型的输入参数,并输出一个jstring类型的参数。jstring是jni.h中定义的数据类型,是JNI框架内特有的字符串类型,因为jni.h在Sample1.h中被引入,因此在Sample1.c中无须再次引入。

程序的第4行是从JNI调用上下文中获取UTF编码的输入字符,将其放在指针str所指向的一段内存中。第9行是释放这段内存。第13行是将经过大写转换的字符串予以返回,这一句使用了NewStringUTF()函数,将C语言的字符串指针转换为JNI的jstring类型。JNIEnv也是在jni.h中定义的,代表JNI调用的上下文,GetStringUTFChars()、ReleaseStringUTFChars()和NewStringUTF()均是JNIEnv的函数。

15.2.2.4  传递整型数组

本节例程将首次尝试在JNI框架内启用数组:C程序向Java程序返回一个定长的整型数组成的数组,Java程序将该数组打印输出。

Java程序的源代码如下。

代码清单15-8  在Linux平台上调用C函数的例程——Sample2

1.       public class Sample2

2.       {

3.         public native int[] intMethod();

4.         

5.         public static void main(String[] args)

6.         {

7.           System.loadLibrary("Sample2");

8.           Sample2 sample=new Sample2();

9.           int[] nums=sample.intMethod();

10.        for(int i=0;i<nums.length;i++)

11.          System.out.println(nums[i]);

12.      }

13.    }

Sample2.java调用libSample2.so中的函数intMethod()。Sample2.c的源代码如下。

代码清单15-9  在Linux平台上调用C函数的例程——Sample2.c

1.       #include <Sample2.h>

2.        

3.       JNIEXPORT jintArray JNICALL Java_Sample2_intMethod(JNIEnv *env, jobject obj)

4.       {

5.         inti = 1;

6.         jintArray array;//定义数组对象

7.         array = (*env)-> NewIntArray(env, 10);

8.         for(; i<= 10; i++)

9.           (*env)->SetIntArrayRegion(env, array, i-1, 1, &i);

10.     

11.       /* 获取数组对象的元素个数 */

12.      int len = (*env)->GetArrayLength(env, array);

13.       /* 获取数组中的所有元素 */

14.      jint* elems = (*env)-> GetIntArrayElements(env, array, 0);

15.      for(i=0; i<len; i++)

16.        printf("ELEMENT %d IS %d\n", i, elems[i]); 

17.     

18.      return array;

19.    }

Sample2.c涉及了两个jni.h定义的整型数相关的数据类型:jint和jintArray,jint是在JNI框架内特有的整数类型。程序的第7行开辟出一个长度为10 的jint数组。然后依次向该数组中放入元素1-10。第11行至第16行不是程序的必须部分,纯粹是为了向读者们演示GetArrayLength()和GetIntArrayElements()这两个函数的使用方法,前者是获取数组长度,后者则是获取数组的首地址以便于遍历数组。

15.2.2.5  传递字符串数组

本节例程是对上节例程的进一步深化:虽然仍然是传递数组,但是数组的基类换成了字符串这样一种对象数据类型。Java程序将向C程序传入一个包含中文字符的字符串,C程序并没有处理这个字符串,而是开辟出一个新的字符串数组返回给Java程序,其中还包含两个汉字字符串。

Java程序的源代码如下。

代码清单15-10  在Linux平台上调用C函数的例程——Sample3

1.       public class Sample3

2.       {

3.         public native String[] stringMethod(String text);

4.         

5.         public static void main(String[] args) throws java.io.UnsupportedEncodingException

6.         {

7.           System.loadLibrary("Sample3");

8.           Sample3 sample = new Sample3();

9.           String[] texts = sample.stringMethod("java编程思想");

10.        for(int i=0;i<texts.length;i++)

11.        {

12.          texts[i]=new String(texts[i].getBytes("ISO8859-1"),"GBK");

13.          System.out.print( texts[i] );

14.        }

15.        System.out.println();

16.      }

17.    }

Sample3.java调用libSample3.so中的函数stringMethod()。Sample3.c的源代码如下:

代码清单15-11  在Linux平台上调用C函数的例程——Sample3.c

1.       #include <Sample3.h>

2.       #include <string.h>

3.       #include <stdlib.h>

4.        

5.       #define ARRAY_LENGTH 5

6.        

7.       JNIEXPORT jobjectArray JNICALL Java_Sample3_stringMethod(JNIEnv *env, jobject obj, jstring string)

8.       {   

9.           jclass objClass = (*env)->FindClass(env, "java/lang/String");

10.        jobjectArray texts= (*env)->NewObjectArray(env, (jsize)ARRAY_LENGTH, objClass, 0);

11.        

12.        jstring jstr;

13.        char* sa[] = { "Hello,", "world!", "JNI", "很", "好玩" };

14.        int i=0;

15.        for(;i<ARRAY_LENGTH;i++)

16.        {

17.          jstr = (*env)->NewStringUTF( env, sa[i] );

18.          (*env)->SetObjectArrayElement(env, texts, i, jstr);//必须放入jstring

19.        }

20.        

21.        return texts;

22.    }

第9、10行是我们需要特别关注的地方:JNI框架并没有定义专门的字符串数组,而是使用jobjectArray——对象数组,对象数组的基类是jclass,jclass是JNI框架内特有的类型,相当于Java语言中的Class类型。在本例程中,通过FindClass()函数在JNI上下文中获取到java.lang.String的类型(Class),并将其赋予jclass变量。

在例程中我们定义了一个长度为5的对象数组texts,并在程序的第18行向其中循环放入预先定义好的sa数组中的字符串,当然前置条件是使用NewStringUTF()函数将C语言的字符串转换为jstring类型。

本例程的另一个关注点是C程序向Java程序传递的中文字符,在Java程序中能否正常显示的问题。在笔者的试验环境中,Sample3.c是在Linux平台上编辑的,其中的中文字符则是用支持GBK的输入法输入的,而Java程序采用ISO8859_1字符集存放JNI调用的返回字符,因此在“代码清单15-10在Linux平台上调用C函数的例程——Sample3”的第14行中将其转码后输出。

15.2.2.6  传递对象数组

本节例程演示的是C程序向Java程序传递对象数组,而且对象数组中存放的不再是字符串,而是一个在Java中自定义的、含有一个topic属性的MailInfo对象类型。

MailInfo对象定义如下。

代码清单15-12  在Linux平台上调用C函数的例程——MailInfo

1.       public class MailInfo

2.       {

3.         public String topic;

4.         

5.         public String getTopic()

6.         {

7.           return this.topic;

8.         }

9.         

10.      public void setTopic(String topic)

11.      {

12.        this.topic=topic;

13.      }

14.    }

Java程序的源代码如下。

代码清单15-13  在Linux平台上调用C函数的例程——Sample4

1.       public class Sample4

2.       {

3.         public native MailInfo[] objectMethod(String text);

4.         

5.         public static void main(String[] args)

6.         {

7.           System.loadLibrary("Sample4");

8.           Sample4 sample = new Sample4();

9.           MailInfo[] mails = sample.objectMethod("Thinking In Java");

10.        for(int i=0;i<mails.length;i++)

11.          System.out.println(mails[i].topic);

12.      }

13.    }

Sample4.java调用libSample4.so中的objectMethod()函数。Sample4.c的源代码如下。

代码清单15-14  在Linux平台上调用C函数的例程——Sample4.c

1.       #include <Sample4.h>

2.       #include <string.h>

3.       #include <stdlib.h>

4.        

5.       #define ARRAY_LENGTH 5

6.        

7.       JNIEXPORT jobjectArray JNICALL Java_Sample4_objectMethod(JNIEnv *env, jobject obj, jstring string)

8.       {  

9.           jclass objClass = (*env)->FindClass(env, "java/lang/Object");

10.        jobjectArray mails= (*env)->NewObjectArray(env, (jsize)ARRAY_LENGTH, objClass, 0);

11.     

12.        jclass objectClass = (*env)->FindClass(env, "MailInfo");

13.        jfieldID topicFieldId = (*env)->GetFieldID(env, objectClass, "topic", "Ljava/lang/String;");

14.        

15.        int i=0;

16.        for(;i<ARRAY_LENGTH;i++)

17.        {

18.          (*env)->SetObjectField(env, obj, topicFieldId, string);

19.          (*env)->SetObjectArrayElement(env, mails, i, obj);

20.        }

21.        

22.        return mails;

23.    }

程序的第9、10行读者们应该不会陌生,在上一节的例程中已经出现过,不同之处在于这次通过FindClass()函数在JNI上下文中获取的是java.lang.Object的类型(Class),并将其作为基类开辟出一个长度为5的对象数组,准备用来存放MailInfo对象。

程序的第12、13行的目的则是创建一个jfieldID类型的变量,在JNI中,操作对象属性都是通过jfieldID进行的。第12行首先查找得到MailInfo的类型(Class),然后基于这个jclass进一步获取其名为topic的属性,并将其赋予jfieldID变量。

程序的第18、19行的目的是循环向对象数组中放入jobject对象。SetObjectField()函数属于首次使用,该函数的作用是向jobject的属性赋值,而值的内容正是Java程序传入的jstring变量值。请注意在向对象属性赋值和向对象数组中放入对象的过程中,我们使用了在函数头部分定义的jobject类型的环境参数obj作为中介。至此,JNI框架固有的两个环境入参env和obj,我们都有涉及。

http://book.csdn.net/bookfiles/606/10060619592.shtml

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值