zoukankan      html  css  js  c++  java
  • 在 Linux 平台下使用 JNI

    简介: 本文简要介绍了 JNI 调用规范,及常用函数。并通过具体示例程序展示了实现一个本地调用的基本步骤。

    本文的标签:  best_practices, jni, 应用开发

    发布日期: 2002 年 10 月 29 日
    级别: 初级
    访问情况 : 8431 次浏览
    评论: 0 (查看 | 添加评论 - 登录)

    平均分 3 星 共 27 个评分 平均分 (27个评分)
    为本文评分

    引言

    Java 的出现给大家开发带来的极大的方便。但是,如果我们有大量原有的经过广泛测试的非 Java 代码,将它们全部用 Java 来重写,恐怕会带来巨大的工作量和长期的测试;如果我们的应用中需要访问到特定的设备,甚至是仅符合公司内部信息交互规范的设备,或某个特定的操作系统才 有的特性,Java 就显得有些力不从心了。面对这些问题,Sun 公司在 JDK1.0 中就定义了 JNI 规范,它规定了 Java 应用程序对本地方法的调用规则。

    实现步骤及相关函数使用

    本文将一步步说明在 Linux 平台下如何实现本地共享库与 Java 协同工作。Hello World 程序是目前标准的入门第一步,那么,我也以类似的应用最为样例。

    第一步,定义一个 Java 类 -- Hello. 它提供 SayHello 方法:

    此时应注意两点:

    1. 为要使用的每个本地方法编写本地方法声明,其声明方式与普通 Java 方法接口没什么不同,只是必须指定 native 关键字,如下所示:

    public native void SayHello(String strName);

    在这个函数中,我们将根据传进的人名,向某人问好。

    2. 必须显式地加载本地代码库。我们需在类的一个静态块中加载这个库:

     static 
        { 
        System.loadLibrary("hello"); 
        } 
    	

    再加上必要的异常处理就生成如下源文件 Hello.java:

     public class Hello 
     { 
    	 static 
    	 { 
    		 try 
    		 { 
     // 此处即为本地方法所在链接库名
    			 System.loadLibrary("hello"); 
    		 } 
    		 catch(UnsatisfiedLinkError e) 
    		 { 
    			 System.err.println( "Cannot load hello library:\n " + 
                                    e.toString() ); 
    		 } 
    	 } 
    	 public Hello() 
    	 { 
    	 } 
     // 声明的本地方法
    		 public native void SayHello(String strName); 
     } 
    

    编译后生成 Hello.class 文件。

    第二步,生成本地链接库。具体过程如下:

    1. 要为以上定义的类生成 Java 本地接口头文件,需使用 javah,Java 编译器的 javah 功能将根据 Hello 类生成必要的声明,此命令将生成 Hello.h 文件,我们在共享库的代码中要包含它,javah 不使默认内部命令,需要指明路径,它在 JDK 的 bin 目录下,在我的 Linux 环境下命令如下:

    /home/jbuilder/jdk1.3.1/bin/javah Hello

    生成的 Hello.h 文件 内容如下所示:

     /* DO NOT EDIT THIS FILE - it is machine generated */ 
     #include <jni.h> 
     /* Header for class Hello */ 
     #ifndef _Included_Hello 
     #define _Included_Hello 
     #ifdef __cplusplus 
     extern "C" { 
     #endif 
     /* 
     * Class:     Hello 
     * Method:    SayHello 
     * Signature: (Ljava/lang/String;)V 
     */ 
     JNIEXPORT void JNICALL Java_Hello_SayHello 
      (JNIEnv *, jobject, jstring); 
     #ifdef __cplusplus 
     } 
     #endif 
     #endif 
    

    2. 在与 Hello.h 相同的路径下创建一个 CPP 文件 Hello.cpp。内容如下:

     #include "Hello.h"
     #include <stdio.h> 
     // 与 Hello.h 中函数声明相同
     JNIEXPORT void JNICALL Java_Hello_SayHello  (JNIEnv * env, jobject arg, jstring instring) 
     { 
       // 从 instring 字符串取得指向字符串 UTF 编码的指针
     const jbyte *str = 
            (const jbyte *)env->GetStringUTFChars( instring, JNI_FALSE ); 
        printf("Hello,%s\n",str); 
    	 // 通知虚拟机本地代码不再需要通过 str 访问 Java 字符串。
        env->ReleaseStringUTFChars( instring, (const char *)str ); 
        return; 
     } 
    

    所有的 JNI 调用都使用了 JNIEnv * 类型的指针,习惯上在 CPP 文件中将这个变量定义为 evn,它是任意一个本地方法的第一个参数。env 指针指向一个函数指针表,在 VC 中可以直接用"->"操作符访问其中的函数。

    jobject 指向在此 Java 代码中实例化的 Java 对象 LocalFunction 的一个句柄,相当于 this 指针。

    后续的参数就是本地调用中有 Java 程序传进的参数,本例中只有一个 String 型参数。 对于字符串型参数,因为在本地代码中不能直接读取 Java 字符串,而必须将其转换为 C /C++ 字符串或 Unicode。以下是三个我们经常会用到的字符串类型处理的函数:

    const char* GetStringUTFChars(jstring string,jboolean* isCopy)

    返回指向字符串 UTF 编码的指针,如果不能创建这个字符数组,返回 null。这个指针在调用 ReleaseStringUTFChar() 函数之前一直有效。

    参数:string 		 Java 字符串对象
     isCopy 		如果进行拷贝,指向以 JNI_TRUE 填充的 jboolean, 否则指向以 JNI_FALSE 填充的 jboolean。
     void ReleaseStringUTFChars(jstring str, const char* chars) 
    通知虚拟机本地代码不再需要通过 chars 访问 Java 字符串。
    参数:string 		 Java 字符串对象
     chars 		由 GetStringChars 返回的指针
     jstring NewStringUTF(const char *utf) 
    返回一个新的 Java 字符串并将 utf 内容拷贝入新串,如果不能创建字符串对象,
    返回 null。通常在反值类型为 string 型时用到。
    参数:utf 		 UTF 编码的字符串指针
    对于数值型参数,在 C/C++ 中可直接使用,其字节宽度如下所示:
    Java C/C++ 字节数
    boolean jboolean 1
    byte jbyte 1
    char jchar 2
    short jshort 2
    int jint 4
    long jlong 8
    float jfloat 4
    double jdouble 8

    对于数组型参数,

    Java C/C++
    boolean[ ] JbooleanArray
    byte[ ] JbyteArray
    char[ ] JcharArray
    short[ ] JshortArray
    int[ ] JintArray
    long[ ] JlongArray
    float[ ] JfloatArray
    double[ ] JdoubleArray

    对于上述类型数组,有一组函数与其对应。以下函数中 Xxx 为对应类型。
    xxx * GetXxxArrayElements(xxxArray array, jboolean *isCopy)
    产生一个指向 Java 数组元素的 C 指针。不再需要时,需将此指针传给 ReleaseXxxArrayElemes。

    参数:array 			数组对象
     isCopy 		如果进行拷贝,指向以 JNI_TRUE 填充的 jboolean, 否则指向以 JNI_FALSE 填充的 jboolean。
    例如: jboolean * GetBooleanArrayElements(jbooleanArray array, jboolean *isCopy) 
     void ReleaseXxxArrayElements(xxxArray array,xxx *elems, jint mode) 
    通知虚拟机不再需要从 GetXxxArrayElements 得到的指针。
    参数:array 			数组对象
     elems 		不再需要的指向数组元素的指针
     mode 		 0 =在更新数组元素后释放 elems 缓冲器
     JNI_COMMIT =在更新数组元素后不释放 elems 缓冲器
     JNI_ABORT =不更新数组元素释放 elems 缓冲器
    例如:void ReleaseBooleanArrayElements(jbooleanArray array,jboolean *elems,     jint mode) 
     xxxArray NewXxxArray(jsize len) 
    产生一个新的数组,通常在反值类型为数组型时用到
    参数:len 		数组中元素的个数。
    例如:jbooleanArray NewBooleanArray(jsize len)

    3 .编译生成共享库。

    使用 GCC 时 , 必须通知编译器在何处查找此 Java 本地方法的支持文件,并且显式通知编译器生成位置无关的代码,在我的环境中按如下过程编译:

    gcc -I/home/jbuilder/jdk1.3.1/include 
        -I/home/jbuilder/jdk1.3.1/include/linux -fPIC -c Hello.c
    

    生成 Hello.o

     gcc -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0 Hello.o 
    

    生成 libhello.so.1.0

    接下来将生成的共享库拷贝为标准文件名

    cp libhello.so.1.0 libhello.so

    最后通知动态链接程序此共享文件的路径。

    export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH

    4 .编写一个简单的 Java 程序来测试我们的本地方法。

    将如下源码存为 ToSay.java:

     import Hello; 
     import java.util.*; 
     public class ToSay 
     { 
    	 public static void main(String argv[]) 
    	 { 
    		 ToSay say = new ToSay(); 
    	 } 
    	 public ToSay() 
    	 { 
    		 Hello h = new Hello(); 
    		 // 调用本地方法向 John 问好
    		 h.SayHello("John"); 			
    	 } 
     } 
    

    用 javac 编译 ToSay.java,生成 ToSay.class
    向执行普通 Java 程序一样使用 java ToSay,我们会看到在屏幕上出现 Hello,John。
    到这里我们就将整个的本地调用编写过程大概浏览了一遍。

    应用中注意事项

    1 . 如果可以通过 TCP/IP 实现 Java 代码与本地 C/C++ 代码的交互工作,那么最好不使用以上提到的 JNI 的方式,因为一次 JNI 调用非常耗时,大概要花 0.5 ~ 1 个毫秒。

    2 . 在一个 Applet 应用中,不要使用 JNI。因为在 applet 中可能引发安全异常。

    3 . 将所有本地方法都封装在单个类中,这个类调用单个 DLL。对于每种目标操作系统,都可以用特定于适当平台的版本替换这个 DLL。这样就可以将本地代码的影响减至最小,并有助于将以后所需的移植问题包含在内。

    4 . 本地方法要简单。尽量将生成的 DLL 对任何第三方运行时 DLL 的依赖减到最小。使本地方法尽量独立,以将加载 DLL 和应用程序所需的开销减到最小。如果必须要运行时 DLL,则应随应用程序一起提供它们。

    5 . 本地代码运行时,没有有效地防数组越界错误、错误指针引用带来的间接错误等。所以必须保证保证本地代码的稳定性,因为,丝毫的错误都可能导致 Java 虚拟机崩溃。

    结束语

    JNI 调用规范给我们复用原有其它语言的代码提供了简单易用的接口,可以节省大量的财力,使我们可以在享受 Java 带来的开发速度的同时,不必放弃旧有资源。

    参考资料

    Cay S.Horstmnn,Gary Cornell,Core Java 2 Volume II:Advanced Fearutes,Prentice Hall,2000.

    Rob Gordon and Alan,Essential JNI, Java Native Interface, Prentice Hall,1998

    http://java.sun.com/products/jdk/1.1/docs/guide/jni/

    关于作者

    张翼翔 :拥有8年的软件开发经验。曾经领导瑞星杀毒软件网络版和I'm上网管理系统的研发工作。现任北京华夏新视科技有限公司研发部经理
    李庆明:从事软件开发已经有5年。有丰富的嵌入式Linux和Java开发经验。现任广东捷远资讯科技有限公司技术总监

  • 相关阅读:
    cf B. Sereja and Suffixes
    cf E. Dima and Magic Guitar
    cf D. Dima and Trap Graph
    cf C. Dima and Salad
    最短路径问题(floyd)
    Drainage Ditches(网络流(EK算法))
    图结构练习—BFSDFS—判断可达性(BFS)
    Sorting It All Out(拓扑排序)
    Power Network(最大流(EK算法))
    Labeling Balls(拓扑)
  • 原文地址:https://www.cnblogs.com/xiaoxiaoboke/p/2349805.html
Copyright © 2011-2022 走看看