Java

【译】Java Native Interface (JNI)

2021-01-16  本文已影响0人  Tubetrue01

引言

查看关于 JNI 相关资料的时候,不巧碰到了这篇文章,通篇读了一下感觉写的很不错,所以拿过来翻译了一下,由于翻译的比较快,有些细节方面的工作可能做的不是很到位,不过后期会进行相关修正,这里先放上来尝尝鲜。
如果你也对 JNI 比较感兴趣,并且打算深入学习,那么本文将会是一个不错的选择。

1.0 JNI 概述

Tips

JNI 还可以用于本地编写的程序(如:C/C++)调用 Java 代码;
比如 Java 的命令行工具(Java 虚拟机启动 Java 代码)。

2.0 JNI 组件

3.0 JNI 开发 (Java)

  1. 创建一个包含 native 方法的 Java 类
public native void sayHi(String who, int times);
  1. 载入实现该方法的类库
System.loadLibrary("HelloImpl");
  1. Java 调用本地方法

Demo

package com.marakana.jniexamples;

public class Hello {
  public native void sayHi(String who, int times);   // 1
  // 2
  static { 
     System.loadLibrary("HelloImpl"); 
  }       
  public static void main (String[] args) {
    Hello hello = new Hello();
    hello.sayHi(args[0], Integer.parseInt(args[1])); // 3
  }
}

其中:
1、3 C/C++ 将实现 sayHi 方法,并且编译成库文件
2 类库的名称:

注意:

Java 载入的类库叫 HelloImpl

4.0 JNI 开发(C)

com_marakana_jniexamples_Hello.h 文件如下:

...
#include <jni.h>
...
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi
  (JNIEnv *, jobject, jstring, jint);
...

Hello.c 文件如下图:

#include <stdio.h>
#include "com_marakana_jniexamples_Hello.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi(JNIEnv *env, jobject obj, jstring who, jint times) {
  jint i;
  jboolean iscopy;
  const char *name;
  name = (*env) -> GetStringUTFChars(env, who, &iscopy);
  for (i = 0; i < times; i++) {
    printf("Hello %s\n", name);
  }
}

5.0 JNI 开发 (编译)

例如:为了编译类路径中的 com_marakana_jniexamples_Hello.c 文件(前提是你得确保 .h 以及 .c 文件在那)。

Linux

gcc -o libHelloImpl.so -lc -shared \
    -I/usr/local/jdk1.6.0_03/include \
    -I/usr/local/jdk1.6.0_03/include/linux com_marakana_jniexamples_Hello.c

macOS

gcc -o libHelloImpl.jnilib -lc -shared \
    -I/System/Library/Frameworks/JavaVM.framework/Headers com_marakana_jniexamples_Hello.c

设置 LD_LIBRARY_PATH 环境变量。

export LD_LIBRARY_PATH=.

最后,运行你的应用程序。

java com.marakana.jniexamples.Hello Student 5
Hello Student
Hello Student
Hello Student
Hello Student
Hello Student

Tips

比较常见的问题是 java.lang.UnsatisfiedLinkError 错误,而导致该错误的一般问题是共享库名称的错误、类库并没有在指定的搜索路径上或者 Java 代码载入了错误的类库。

6.0 类型转换

Table 3. JNI 数据类型映射

Java 类型 本地类型 描述
boolean jboolean 8 bits, unsigned
byte jbyte 8 bits, signed
char jchar 16 bits, unsigned
double jdouble 64 bits
float jfloat 32 bits
int jint 32 bits, signed
long jlong 64 bits, signed
short jshort 16 bits, signed
void void N/A
  1. 映射对象类型会更加复杂一点。这里我们主要关注字符串以及数组类型。不过不要着急,在我们深入探讨之前,先让我们看看本地方法的参数们。

让我们考虑以下 Java class:

package com.marakana.jniexamples;

public class HelloName {
  public static native void sayHelloName(String name);

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

  public static void main (String[] args) {
    HelloName hello = new HelloName();
    String name = "John";
    hello.sayHelloName(name);
  }
}
...
#include <jni.h>
...
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName
  (JNIEnv *, jclass, jstring);
...
#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name){
    printf("Hello %s", name);
}

7.0 本地方法参数

8.0 字符串转换

Tips

详细的内容可以参考:http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html

#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name) {
    printf("Hello %s", name); // 1
}

因为 jstring 类型代表的是 Java 虚拟机中的字符串类型,而跟 C 中的字符串类型 (char *) 是不同的,所以这个例子不会按照预期运行。。

#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name){
    const jbyte *str;
    str = (*env)->GetStringUTFChars(env, name, NULL); // 1
    printf("Hello %s\n", str);
    (*env)->ReleaseStringUTFChars(env, name, str); // 2
}

1 它返回一个指向代表 UTF-8 编码的字符串字节数组的指针(并没有产生内存复制)。
2 当我们并没有发生字符串复制的时候,调用 ReleaseStringUTFChars 函数可以防止字符串使用的内存区域保持固定状态。如果数据被复制,我们需要调用 ReleaseStringUTFChars 去释放那些不再使用的内存。

#include <stdio.h>
#include "com_marakana_jniexamples_GetName.h"

JNIEXPORT jstring JNICALL Java_com_marakana_jniexamples_ReturnName_GetName(JNIEnv *env, jclass class) {
    char buffer[20];
    scanf("%s", buffer);
    return (*env)->NewStringUTF(env, buffer);
}
package com.marakana.jniexamples;

public class ArrayReader {
    private static native int sumArray(int[] arr); // 1
    public static void main(String[] args) {
        // Array declaration
        int arr[] = new int[10];
        // Fill the array
        for (int i = 0; i < 10; i++) {
            arr[i] = i;
        }
        ArrayReader reader = new ArrayReader();
        // Call native method
        int result = reader.sumArray(arr); // 2
        System.out.println("The sum of every element in the array is " + Integer.toString(result));
    }
    static {
        System.loadLibrary("arrayreader");
    }
}

1 2 这个方法将返回数组中元素的总和。

#include <stdio.h>
#include "com_marakana_jniexamples_ArrayReader.h"

JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_ArrayReader_sumArray(JNIEnv *env, jclass class, jintArray array) {
    jint *native_array;
    jint i, result = 0;
    native_array = (*env) -> GetIntArrayElements(env, array, NULL);  // 1
    if (native_array == NULL) {
        return 0;
    }
    for (i = 0; i < 10; i++) {
        result += native_array[i];
    }
    (*env) -> ReleaseIntArrayElements(env, array, native_array, 0);
    return result;
}

1 由于我们恰恰知道数组的大小,所以我们也可以使用 GetIntArrayRegion 函数

10 在本地世界中抛出异常

void ThrowExceptionByClassName(JNIEnv *env, const char *name, const char *message) {
  jclass class = (*env) -> FindClass(env, name); // 1
  if (class != NULL) {
      (*env) -> ThrowNew(env, class, message); // 2
  }
  (*env) -> DeleteLocalRef(env, class); // 3
}

1 通过名字找到该异常类
2 使用我们之前获得的类引用和异常信息抛出异常
3 删除异常类的本地引用

ThrowExceptionByClassName(env,"java/lang/IllegalArgumentException","This exception is thrown from C code");

11 从本地代码访问属性和方法

package com.marakana.jniexamples;

public class InstanceAccess {
    public String name; // 1

    public void setName(String name) { // 2
        this.name = name;
    }

    // Native method
    public native void propertyAccess(); // 3
    public native void methodAccess(); // 4

    public static void main(String args[]) {
        InstanceAccess instanceAccessor = new InstanceAccess();
        // Set the initial value of the name property
        instanceAccessor.setName("Jack");
        System.out.println("Java: value of name = \""+ instanceAccessor.name +"\"");
        // Call the propetyAccess() method
        System.out.println("Java: calling propertyAccess() method...");
        instanceAccessor.propertyAccess(); // 5
        // Value of name after calling the propertyAccess() method
        System.out.println("Java: value of name after calling propertyAccess() = \""+ instanceAccessor.name +"\"");
        // Call the methodAccess() method
        System.out.println("Java: calling methodAccess() method...");
        instanceAccessor.methodAccess(); // 6
        System.out.println("Java: value of name after calling methodAccess() = \""+ instanceAccessor.name +"\"");
    }

    // Load library
    static {
        System.loadLibrary("instanceaccess");
    }
}

1 name 属性会在代码执行的时候被修改
2 该方法在本地代码修改 name 属性的时候被调用
3 5 本地方法通过直接访问 name 属性的方式对其进行修改
4 6 本地方法通过调用 Java setName() 方法对 name 属性进行修改

#include <stdio.h>
#include "com_marakana_jniexamples_InstanceAccess.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_propertyAccess(JNIEnv *env, jobject object){
    jfieldID fieldId;
    jstring jstr;
    const char *cString;

    /* Getting a reference to object class */
    jclass class = (*env) -> GetObjectClass(env, object); /* 1 */

    /* Getting the field id in the class */
    fieldId = (*env) -> GetFieldID(env, class, "name", "Ljava/lang/String;"); /* 2 */
    if (fieldId == NULL) {
        return; /* Error while getting field id */
    }

    /* Getting a jstring */
    jstr = (*env) -> GetObjectField(env, object, fieldId); /* 3 */

    /* From that jstring we are getting a C string: char* */
    cString = (*env) -> GetStringUTFChars(env, jstr, NULL); /* 4 */
    if (cString == NULL) {
        return;  /* Out of memory */
    }
    printf("C: value of name before property modification = \"%s\"\n", cString);
    (*env) -> ReleaseStringUTFChars(env, jstr, cString);

    /* Creating a new string containing the new name */
    jstr = (*env) -> NewStringUTF(env, "Brian"); /* 5 */
    if (jstr == NULL) {
        return;  /* Out of memory */
    }
    /* Overwrite the value of the name property */
    (*env) -> SetObjectField(env, object, fieldId, jstr); /* 6 */
}

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_methodAccess(JNIEnv *env, jobject object){
    jclass class = (*env) -> GetObjectClass(env, object); /* 7 */
    jmethodID methodId = (*env) -> GetMethodID(env, class, "setName", "(Ljava/lang/String;)V"); /* 8 */
    jstring jstr;
    if (methodId == NULL) {
        return; /* method not found */
    }
    /* Creating a new string containing the new name */
    jstr = (*env) -> NewStringUTF(env, "Nick"); /* 9 */
    (*env) -> CallVoidMethod(env, object, methodId, jstr); /* 10 */
}

1 7 获取 class 对象的引用
2 从 class 对象中获取字段 Id,以及指定要获取的属性以及内部类型。可以从以下链接中获取关于 jni 类型的信息:http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/types.html
3 这里将会返回本地类型中的属性值 jstring
4 我们需要将 jstring 类型转换为 C 中的字符串
5 这里会创建出一个新的 java.lang.String 类型用以修改属性的值
6 将新的值设置给该属性
8 从先前获取到的 class 对象中通过方法的名称以及签名获取方法 id 。这里有一个用来获取方法签名的实用工具:javap -s -p ClassName for instance javap -s -p InstanceAccess
9 创建出一个新的 java.lang.String 对象作为从本地代码调用 java 方法的参数。
10 由于 Java 方法返回值类型为 void,所以调用 CallVoidMethod 方法,并且将先前创建出的 jstring 作为参数传递给它


参考

上一篇下一篇

猜你喜欢

热点阅读