美文网首页
JNI 初识

JNI 初识

作者: 一个追寻者的故事 | 来源:发表于2020-05-04 14:20 被阅读0次

    Java平台有一个和本地C代码进行互操作的API,称为Java本地接口(JNI)

    求助本地代码是有缺陷的。如果应用的某个部分使用其他语言编写的,那么就必须为需要支持的每个平台提供一个单独的本地类库。

    建议在有必要的时候才使用本地代码:

    • 你的应用需要访问系统的各个特性和设备,这些特性和设备通过Java平台是无法访问的。
    • 你已经有了大量的测试过和调试过的用另一种语言编写的代码,并且知道如何将其导出到所有的目标平台上。
    • 通过基准测试,你已经发现所编写的Java代码比用其它语言编写的等价代码要慢得多。

    JNI 并不支持 Java类 和C++类之间的任何映射机制。

    一、Java程序调用C函数

    native 关键字 提醒编译器该方法将在外部定义。

    public class HelloNative {
        // java 调用 native
        public static native void greeting();
    
        static {
    //        System.loadLibrary("HelloNative");
            System.load("/Users/jxf/workspace/Java/project/jni/out/production/jni/libHelloNative.so");
        }
    }
    

    通过工具 javah 生成对应的本地函数名(前提是HelloNative类已经编译,HelloNative.class已生成)

    javah com.test.jni.HelloNative 注意要有类的完整包名。这条命令生成了 com_test_jni_HelloNative.h 文件(实际包目录跟具体包名相关),具体代码:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_test_jni_HelloNative */
    
    #ifndef _Included_com_test_jni_HelloNative
    #define _Included_com_test_jni_HelloNative
    # 你可以使用C++实现代码,然而,那样你必须将实现本地方法的函数声明为 extern  "C"(阻止C++编译器生成C++特有的代码)
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_test_jni_HelloNative
     * Method:    greeting
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_com_test_jni_HelloNative_greeting
      (JNIEnv *, jclass);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    本地代码的函数命名必须遵守Java运行环境预期的那样才行:
    1、使用完整的Java方法名。比如 com.testj.ni.HelloNative.greeting
    2、用 下划线 替换所有的句话,并且加上 Java_ 前缀。比如:Java_com_test_jni_HelloNative_greeting

    如果你重载本地方法,必须在名称后面附加两个下划线,后面再加上已编码的参数类型。例如,如果有一个本地方法greeting 和 另一个本地方法 greeting(int repeat),那么第一个称为 Java_com_test_jni_HelloNative_greeting__,第二个称为Java_com_test_jni_HelloNative_greeting__I

    然后,我们手动创建源代码文件 com_test_jni_HelloNative.c,代码如下:

    #include "com_test_jni_HelloNative.h"
    #include <stdio.h>
    //实现具体方法:
    JNIEXPORT void JNICALL Java_com_test_jni_HelloNative_greeting(JNIEnv *env, jclass cl)
    {
        printf("Hello Native World\n");
    }
    

    之后,将本地C代码编译到一个动态装载库中,具体方法依赖于编译器,Mac 下使用Gnu C 编译器,使用如下命令:
    gcc -fPIC -I /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/ -I /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/darwin -shared -o libHelloNative.so com_test_jni_HelloNative.c

    -fPIC -- Generate position-independent code if possible (large mode)
    -I <dir> Add directory to include search path
    -o <file> Write output to <file>

    /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/
    /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/darwin 这两个路径是生成so库时需要依赖的我本机的路径。

    /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/ 我本机的目录截图:

    我们生成的 .h头文件依赖于 jni.h,jni.h又依赖于jni_md.h

    编译时,如果没有写 include 路径,就会出现如下错误:

    In file included from com_test_jni_HelloNative.c:2:
    In file included from ./com_test_jni_HelloNative.h:2:
    /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/jni.h:45:10: fatal error:
          'jni_md.h' file not found
    #include "jni_md.h"
             ^~~~~~~~~~
    1 error generated.
    

    如果编译过程顺利,会生成 libHelloNative.so

    最后,我们要在程序中添加一个对 System loadloadLibrary方法的调用。为了确保虚拟机在第一次使用该类之前就会装载这个库,需要使用静态初始化代码块。本例中我们使用 load 方法

    load 加载要求传入的路径为 an absolute path name
    loadLibrary 要求传入的路径为 library name。装载指定名字的库,该库位于库搜索路径中,依赖于操作系统。

    使用:

    public class Main {
        public static void main(String[] args) {
            HelloNative.greeting();
        }
    }
    

    结果:

    Hello Native World
    

    二、数值参数 和 返回值

    java:

    public static native int getNumber(int num);
    

    c:

    JNIEXPORT jint JNICALL Java_com_test_jni_HelloNative_getNumber(JNIEnv *env, jclass cl, jint param)
    {
        jint res = param * 2;
        return res;
    }
    

    使用:

    System.out.println("jni method: " + HelloNative.getNumber(10));
    

    结果:

    jni method: 20
    

    当在C 和 Java之间传递数字时,我们应该知道它们 彼此之间的对应类型。例如,尽管C拥有int 和 long 的数据类型,但是它们的实现确实取决于平台的。在一些平台上,int类型是16位的,在另外一些平台是32位的。当然,在Java平台上int类型总是32位的整数。基于这个原因,Java本地接口定义了jint、jlong 等类型。

    java语言 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:

    public static native String getString();
    

    c:

    JNIEXPORT jstring JNICALL Java_com_test_jni_HelloNative_getString(JNIEnv *env, jclass cl)
    {
        jstring jstr;
        char greeting[] = "nativeString\n";
        jstr = (*env)->NewStringUTF(env, greeting);
        return jstr;
    }
    

    使用:

    System.out.println(HelloNative.getString());
    

    结果:

    nativeString
    

    所有针对JNI函数的调用都使用到了env指针,该指针是每一个本地方法的第一个参数,env指针是函数指针表的指针。

    四、native 访问 java对象的属性

    java:

    private int flag = 0;
    // native 访问 java实例的属性
    public native void setFlag(int value);
    public int getFlag() {
         return flag;
    }
    

    c:

    JNIEXPORT void JNICALL Java_com_test_jni_HelloNative_setFlag(JNIEnv *env, jobject this_obj, jint value)
    {
        //get the class
        jclass cls = (*env)->GetObjectClass(env, this_obj);
    
        // get the field ID
        jfieldID id_flag = (*env)->GetFieldID(env, cls, "flag", "I");
    
        //set the field value
        (*env)->SetIntField(env, this_obj, id_flag, value);
    }
    

    使用:

    HelloNative obj = new HelloNative();
    //访问实例域
    obj.setFlag(100);
    System.out.println("flag: " + obj.getFlag());
    

    结果:

    flag: 100
    

    五、native 访问 java对象的静态属性

    java:

        private static float num =  10;
        public native void setNum(float value);
        public float getNum(){
            return num;
        }
    

    c:

    JNIEXPORT void JNICALL Java_com_test_jni_HelloNative_setNum(JNIEnv *env, jobject this_obj, jfloat value)
    {
        jclass cls =  (*env)->GetObjectClass(env, this_obj);
    
        jfieldID id_num = (*env)->GetStaticFieldID(env, cls, "num", "F");
    
        (*env)->SetStaticFloatField(env, cls, id_num, value);
    }
    

    使用:

    obj.setNum(1110);
    System.out.println("static num: " + obj.getNum());
    

    结果:

    static num: 1110.0
    

    六、native 调用 java 方法

    java:

    public native static void callJavaOtherMethod();
    

    c:

    JNIEXPORT void JNICALL Java_com_test_jni_HelloNative_callJavaOtherMethod(JNIEnv *env, jclass cl)
    {
        jmethodID id_greeting = (*env)->GetStaticMethodID(env, cl, "greeting", "()V");
    
        (*env)->CallStaticVoidMethod(env, cl, id_greeting);
    }
    

    调用:

    HelloNative.callJavaOtherMethod();
    

    结果:

    Hello Native World
    

    为了访问实例域和调用Java中定义的方法,必须学习“编入”数据类型的名称和方法签名的规则。

    类型简写 java类型
    B byte
    C char
    D double
    F float
    I int
    J long
    S short
    V void
    Z boolean
    Lclassname; 类的类型

    描述数组用 [ 例如:[Ljava/lang/String; 一个float[][]可描述为:[[F
    要建立一个方法的完整签名,需要把括号内的参数类型都列出来,然后列出返回值类型。例如一个接受两个整形参数并返回一个整数的方法编码为:(II)I

    七、native 抛出一个 Exception

    java:

    public native static void throwAException();
    

    c:

    JNIEXPORT void JNICALL Java_com_test_jni_HelloNative_throwAException(JNIEnv *env, jclass cls)
    {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/NullPointerException"), "a exception from native code");
    
        return;
    }
    

    使用:

    HelloNative.throwAException();
    

    结果:

    Exception in thread "main" java.lang.NullPointerException: a exception from native code
       at com.test.jni.HelloNative.throwAException(Native Method)
       at com.test.Main.main(Main.java:29)
    

    其它:
    https://www.cnblogs.com/chaoren399/p/6232467.html
    https://www.cnblogs.com/EasonJim/p/9445282.html
    https://www.cnblogs.com/ylz8401/p/9605498.html

    相关文章

      网友评论

          本文标题:JNI 初识

          本文链接:https://www.haomeiwen.com/subject/ekzawhtx.html