首页 > Coding > 多语言编程之 C and Java:JNI 30 分钟入门

多语言编程之 C and Java:JNI 30 分钟入门

2016年6月29日 发表评论 阅读评论

1 引言

1.1 简介

在实际项目中,很多时候是以多种语言混合开发的。常用的模式是使用 C/C++ 做底层支撑,然后上层辅之以高级语言。 JNI可以使 native 代码与 Java 对象交互,而不会像 Java 代码中的功能那样受到诸多限制, 往往充作 Java 与 C/C++/Assembly 语言的粘合剂。

JNI 明确分开了 Java 代码与 native 代码的执行,为两者的通信定义了一套清晰的API.避免 native 代码对 JVM 的直接内存引用。

2 JNI 入门

2.1 JNI 调用 C

2.1.1  java 中的 native 方法声明

HelloJNI.java

public class HelloJNI{
    static{
        System.loadLibrary("hello");  //运行时加载本地库 hello.dll(Windows)或libhello.so(Unixes) 或 libhello.dylib(MacOX)
    }

    private native void sayHello();  //声明 native 方法

    public static void main(String[] args ){
        new HelloJNI().sayHello();  //调用本地方法
    }
}

当HelloJNI类加载的时候,静态初始化调用 System.loadLibrary() 来加载本地库 hello.dll(或libhello.so)。如果不能加载 hello 库,会抛出 UnsatisfiedLinkError 异常。
然后我们使用 native 关键字声明了 native 方法 sayHello() ,这表明该方法是使用其它语言实现的。
最后 main() 方法实例化了 HelloJNI 并调用了 sayHello().
然后我们编译这个类:

> javac HelloJNI.java

2.1.2  生成 C/C++ 头文件

使用 javah 创建头文件:

> javah HelloJNI

会得到如下 .h 文件 HelloJNI.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

这份自动生成的头文件里声明一个 C 函数 Java_HelloJNI_sayHello:

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

这个方法名是由javah转换来的,转换规则为:

Java_{package_and_classname}_{function_name}(JNI arguments)

包名中的 点 会被转换为 下划线
函数中带有两个参数(虽然我们在本例中并没有使用这两个参数)

  • JNIEnv*: 指向 JNI environment ,提供对 JNI 方法的访问
  • jobject: 指向 this Java 对象

2.1.3 C 代码的实现

HelloJNI.c

#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv * env, jobject obj)
{
    printf("Hello World!");
}

这个实现将在控制台打印万能的 “Hello World!”
文件中包含了 jni.h 头文件。这个头文件由 java 提供,位于 %JAVA_HOME%/include 路径下(Windows下还需要引用 %JAVA_HOME%/include/win32).

2.1.4 编译 C 库

接下来,编译这个 c 文件:

#Windows gcc
> gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloJNI.c

#Windows VS cl.exe link.exe
> cl /c /I%JAVA_HOME%/include /I%JAVA_HOME%/include/win32 HelloJNI.c
> link HelloJNI.obj /DLL /OUT:hello.dll

2.1.5 检查成果

> java HelloJNI
#or
> java -Djava.library.path=. HelloJNI

3 在 Java 和 Native 间传递参数

3.1 传递原始类型参数

Java 原始参数可以在 Java 和 Native 单直接传递,只不过这些类型在JNI中的名称不一样。JNI 定义了 jxxx 类型来对应相应的原始类型。如 jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean 分别对应 Java 中的 int, byte, short, long, float, double, char, boolean. 下例中,将演示如何传递 Java 原始参数。这一次,我们编辑的顺序将从 native 到 Java。

3.1.1 编辑 C 头文件

打开 HelloJNI.h,在文件中添加函数的声明:

JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv*, jobject, jint, jint);

这里用到了 jint 。我们可以在 jni.h 和 jni_md.h 中找到各原始类型的定义:

// In "jni_md.h"
typedef long            jint;
typedef __int64         jlong;
typedef signed char     jbyte;

// In "jni.h"
typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;
typedef jint            jsize;

3.1.2 实现函数

在 HelloJNI.c 中实现上一步声明的函数:

JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv* env, jobject obj, jint a , jint b)
{
    return (a + b);
}

3.1.3 编辑 Java 文件

在Java中声明 native 方法并调用:

public class HelloJNI{
    static{
        System.loadLibrary("hello");
    }

    private native void sayHello();

    private native int add(int a, int b);

    public static void main(String[] args ){
        HelloJNI hj = new HelloJNI();
        hj.sayHello();
        System.out.println("the result of 3 add 4 is: " + hj.add(3,4));
    }
}

3.1.4 编译并执行

像上一章那样,编译 HelloJNI.c 和 HelloJNI.java 并执行 Java 程序,结果如下:

Hello World!
the result of 3 add 4 is: 7

3.2 传递 String 参数

3.2.1 示例

Java 代码

private native String sayHi(String name);

C 函数声明

JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv *, jobject, jstring);

JNI 使用 jstring 来表示 Java String. 最后一个参数 jstring 是 Java 传递到 native 的 String 参数

C 函数实现

JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv * env, jobject obj , jstring str)
{
    // jstring 转换为 C-string
    const char* pcsz = (*env)->GetStringUTFChars(env, str, NULL);
    if (NULL == pcsz)
        return NULL;

    char szMsg[256];
    sprintf(szMsg,"say hi to %s from C native",pcsz);

    // 释放内存
    (*env)->ReleaseStringUTFChars(env, str, pcsz);

    // C-string 转换为 jstring
    return (*env)->NewStringUTF(env, szMsg);    
}

一般使用 GetStringUTFChars() 将接收到的 jstring 转换为 C-string ; 使用 NewStringUTF 将 C-string 转换为 jstring 返回给 Java

Java String 和 C-string 不同,是一个复杂的引用类型,在传递过程中需要将字符串在这两种类型间转换。JNIEnv*(JNI Enviroment) 提供了转换的方法:

// jstring to C-string
const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)

// C-string to jstring
jstring NewStringUTF(JNIEnv*, char*)

GetStringUTFChars() 函数将从第二个参数 jstring 参数中返回 C-string(char*)。如果有内存分配错误,则返回NULL. 函数的第三个参数 isCopy 可以设置为 JNI_TRUE 或 JNI_FALSE . 如果是 JNI_TRUE, 则返回Java String 的副本的 C-string. 如果是 JNI_FALSE,则直接返回 Java String 参数的 C-string(char*) 指针,此时不要改变返回值的内容。
ReleaseStringUTFChars() 函数将用来释放 GetStringUTFChars() 的返回值。只要 GetStringUTFChars() 的返回值不再被使用时,它应该被立即释放以便垃圾回收器介入处理。
NewStringUTF() 用来从 C-string 创建 jstring.

3.2.2 JNI String 操作函数

// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
// Can be mapped to null-terminated char-array C-string
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
   // Informs the VM that the native code no longer needs access to utf.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
   // Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
   // Returns the length in bytes of the modified UTF-8 representation of a string.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
   // Translates len number of Unicode characters beginning at offset start into modified UTF-8 encoding 
   // and place the result in the given buffer buf.

// Unicode Strings (16-bit character)
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to the array of Unicode characters
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
   // Informs the VM that the native code no longer needs access to chars.
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
   // Constructs a new java.lang.String object from an array of Unicode characters.
jsize GetStringLength(JNIEnv *env, jstring string);
   // Returns the length (the count of Unicode characters) of a Java string.
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);
   // Copies len number of Unicode characters beginning at offset start to the given buffer buf

更多的 JNI 函数可参考这里

3.3 传递原始类型数组

3.3.1 示例

Java 代码

private native double[] sumAndAvg(int[] nums);

JNI C 函数声明

JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *, jobject, jintArray);

JNI C 函数实现

JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *env, jobject obj, jintArray nums)
{
    //获取 C 数组
    jint* pcArray = (*env)->GetIntArrayElements(env,nums,NULL);
    if(pcArray == NULL)
            return NULL;

    //数组长度
    jsize len = (*env)->GetArrayLength(env,nums);

    jint sum = 0;
    for(int i = 0; i < len; i++)
    {
        sum += pcArray[i];
    }

    jdouble avg = (jdouble)sum / len;

    //释放内存
    (*env)->ReleaseIntArrayElements(env,nums, pcArray, 0);

    jdouble outArray[] = {sum,avg};

    //C 数组转为 Java 数组
    jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2);
    if(outArray == NULL)
        return NULL;
    (*env)->SetDoubleArrayRegion(env, outJNIArray, 0, 2, outArray);

    return outJNIArray;
}

3.3.2 JNI 原始类型数组操作函数

JNI 原始数据类型的数组(jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray and jbooleanArray)操作函数如下:

// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
  • Get<PrimitiveType>ArrayElements() 从 Java 数组创建 C 数组
  • Get|Set<PrimitiveType>ArrayRegion()将从 Java 数组和 C 数组中来回拷贝数据
  • New<PrimitiveType>Array() 分配新的 Java 数组内存
  • GetPrimitiveArrayCritical() 得到原始数据类型内容的指针,可能使得垃圾回收器不回收此内存,直到 ReleasePrimitiveArrayCritical() 调用

4 附录

4.1 本文源码

  • HelloJNI.java
public class HelloJNI{
    static{
        System.loadLibrary("hello");
    }

    private native void sayHello();

    private native int add(int a, int b);

    private native String sayHi(String name);

    private native double[] sumAndAvg(int[] nums);

    public static void main(String[] args ){
        HelloJNI hj = new HelloJNI();
        hj.sayHello();
        System.out.println("the result of 3 add 4 is: " + hj.add(3,4));
        System.out.println("say hi to wandoer: " + hj.sayHi("wandoer.com"));

        int[] nums = new int[]{3,4};
        double[] dArr = hj.sumAndAvg(nums);
        System.out.println("the sum and avg of 3 and 4 is :" + dArr[0] + " and " + dArr[1]); 
    }
}
  • HelloJNI.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

/*
 * Class:     HelloJNI
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv *, jobject, jint, jint);

/*
 * Class:     HelloJNI
 * Method:    sayHi
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv *, jobject, jstring);

/*
 * Class:     HelloJNI
 * Method:    sunAndAvg
 * Signature: ([I)[D
 */
JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *, jobject, jintArray);

#ifdef __cplusplus
}
#endif
#endif
  • HelloJNI.c
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv * env, jobject obj)
{
    printf("Hello World!\n");
}

JNIEXPORT jint JNICALL Java_HelloJNI_add(JNIEnv* env, jobject obj, jint a , jint b)
{
    return (a + b);
}

JNIEXPORT jstring JNICALL Java_HelloJNI_sayHi(JNIEnv * env, jobject obj , jstring str)
{
    // jstring 转换为 C-string
    const char* pcsz = (*env)->GetStringUTFChars(env, str, NULL);
    if (NULL == pcsz)
        return NULL;

    char szMsg[256];
    sprintf(szMsg,"say hi to %s from C native",pcsz);

    // 释放内存
    (*env)->ReleaseStringUTFChars(env, str, pcsz);

    // C-string 转换为 jstring
    return (*env)->NewStringUTF(env, szMsg);    
}

JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAvg(JNIEnv *env, jobject obj, jintArray nums)
{
    //获取 C 数组
    jint* pcArray = (*env)->GetIntArrayElements(env,nums,NULL);
    if(pcArray == NULL)
            return NULL;

    //数组长度
    jsize len = (*env)->GetArrayLength(env,nums);

    jint sum = 0;
    for(int i = 0; i < len; i++)
    {
        sum += pcArray[i];
    }

    jdouble avg = (jdouble)sum / len;

    //释放内存
    (*env)->ReleaseIntArrayElements(env,nums, pcArray, 0);

    jdouble outArray[] = {sum,avg};

    //C 数组转为 Java 数组
    jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2);
    if(outArray == NULL)
        return NULL;
    (*env)->SetDoubleArrayRegion(env, outJNIArray, 0, 2, outArray);

    return outJNIArray;
}

4.2 参考