Java通过JNI调用C++

声明

本篇内容柔和了两篇blog上的内容,并修正了一些问题,添补了必要的步骤。


JNI是Java Native Interface的英文缩写, 中文翻译为本地调用, 自从Java 1.1开始就成为了Java标准的一部分.

C/C++是系统级的编程语言, 可以用来开发任何和系统相关的程序和类库, 但是Java本身编写底层的应用比较难实现, 使用JNI可以调用现有的本地库, 极大地灵活了Java的开发.

C/C++的效率是目前最好的语言, 可以使用C/C++来实现一些实时性非常高的部分. C/C++和Java本身都是非常流行的编程语言, 一些大型软件中经常使用语言之间的混合编程.

鉴于目前网络上JNI的文章不是特别多, 我将自己的一些总结写在这里. 如有错漏, 欢迎指正!

Java调用C/C++大概有这样几个步骤

  1. 编写带有native方法的Java类, 使用javac工具编译Java类
  2. 使用javah来生成与native方法对应的头文件
  3. 实现相应的头文件, 并编译为动态链接库(windows下是.dll, linux下是.so)

下面就完整的介绍一个简单的Java调用C/C++的例子, 这个例子是来自http://www.ibm.com/developerworks/cn/education/java/j-jni/index.html, 不过其中有一些错误, 这个文章是非常不错的JNI学习资料, 但是非常古老.

扫描二维码关注公众号,回复: 3341257 查看本文章

编写Java类

我们来编写一个Sample1的java类

public class Sample1 {
    public native int intMethod(int n);
    public native boolean booleanMethod(boolean bool);
    public native String stringMethod(String text);
    public native int intArrayMethod(int[] intArray);
     
    public static void main(String[] args) {
        System.loadLibrary("Sample1");
        Sample1 sample = new Sample1();
        int square = sample.intMethod(5);
        boolean bool = sample.booleanMethod(true);
        String text = sample.stringMethod("Java");
        int sum = sample.intArrayMethod(new int[]{1,2,3,4,5,8,13});
         
        System.out.println("intMethod: " + square);
        System.out.println("booleanMethod: " + bool);
        System.out.println("stringMethod: " + text);
        System.out.println("intArrayMethod: " + sum);
    }
}

上面有4个native方法, 分别是4种类型的参数, int, boolean, String, int[].

其中有一句比较重要, 这句话加载了动态类库

System.loadLibrary("Sample1");

在windows下加载的就是Sample1.dll, 在linux下加载的就是Sample1.so.

本文使用的windowws, 所以后面使用Sample1.dll来表示Sample1动态链接库.

注意: 不可以在代码中写上后缀dll或so. 还要保证Sample1.dll在path路径中. 这个Sample1.dll是我们后面需要编译出来的东西.

4个native方法就是我们需要用C来实现的方法.

编译Sample1.java, 使用命令行(windows是cmd, linux下一般是bash)

>javac Sample1.java

可以看到Sample1.class文件

使用javah生成头文件

在命令行中运行

>javah Sample1

可以在目录下看到一个新文件Sample1.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Sample1 */
 
#ifndef _Included_Sample1
#define _Included_Sample1
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Sample1
 * Method:    intMethod
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_Sample1_intMethod
  (JNIEnv *, jobject, jint);
 
/*
 * Class:     Sample1
 * Method:    booleanMethod
 * Signature: (Z)Z
 */
JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
  (JNIEnv *, jobject, jboolean);
 
/*
 * Class:     Sample1
 * Method:    stringMethod
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
  (JNIEnv *, jobject, jstring);
 
/*
 * Class:     Sample1
 * Method:    intArrayMethod
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
  (JNIEnv *, jobject, jintArray);
 
#ifdef __cplusplus
}
#endif
#endif

我们可以看到其中有四个函数声明, Java_完整类名_方法名, 完整类名包括了包名, 例如demo.Sample1是完整类名, 对应的这里就是demo_Sample1.

在注释中我们可以看到这样一个东西 Signature, 这个是方法的签名. 关于Signature, 下面通过一个表格来说明.

java类型 Signature 备注
boolean Z  
byte B  
char C  
short S  
int I  
long L  
float F  
double D  
void V  
object L用/分割的完整类名 例如: Ljava/lang/String表示String类型
Array [签名 例如: [I表示int数组, [Ljava/lang/String表示String数组
Method (参数签名)返回类型签名 例如: ([I)I表示参数类型为int数组, 返回int类型的方法

上面头文件的第一个函数声明

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

注释中的签名是 Signature: (I)I

在每个函数的参数列表中都有JNIEnv *和 jobject两个参数, 这两个参数稍候说明.

实现头文件中的函数

可以使用C语言来实现, 也可以使用C++来实现, 下面先说说C语言的实现.

#include "Sample1.h"
#include <string.h>
 
JNIEXPORT jint JNICALL Java_Sample1_intMethod
  (JNIEnv *env, jobject obj, jint num)
{
    return num * num;
}
 
JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
  (JNIEnv *env, jobject obj, jboolean boolean)
{
    return !boolean;
}
 
JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
  (JNIEnv *env, jobject obj, jstring string)
{
    const char* str = (*env)->GetStringUTFChars(env, string, 0);
    char cap[128];
    strcpy(cap, str);
    (*env)->ReleaseStringUTFChars(env, string, 0);
    return (*env)->NewStringUTF(env, strupr(cap));
}
 
JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
  (JNIEnv *env, jobject obj, jintArray array)
{
    int i, sum = 0;
    jsize len = (*env)->GetArrayLength(env, array);
    jint *body = (*env)->GetIntArrayElements(env, array, 0);
     
    for (i = 0; i < len; ++i)
    {
        sum += body[i];
    }
    (*env)->ReleaseIntArrayElements(env, array, body, 0);
    return sum;
}

(*env)->GetStringUTFChars()这个方法, 是用来在Java和C之间转换字符串的, 因为Java本身都使用了双字节的字符, 而C语言本身都是单字节的字符, 所以需要进行转换.

JNIEnv *是每个函数都有的参数, 它包含了很多有用的方法, 使用起来类似Java的反射, 也提供了这样一个编码转换的函数.

GetStringUTFChars()和NewStringUTF(), 第一个是从UTF8转换为C的编码格式, 第二个是根据C的字符串返回一个UTF8字符串.

ReleaseStringUTFChars()是用来释放对象的, 在Java中有虚拟机进行垃圾回收, 但是在C语言中, 这些对象必须手动回收. 否则可能造成内存泄漏.

函数的名字一眼看到就可以猜出功能, jni.h中的大部分函数名都是这样.

如果是C++的话, 这段代码该怎么写?

下面是C++的代码

#include "Sample1.h"
#include <string.h>
 
JNIEXPORT jint JNICALL Java_Sample1_intMethod
  (JNIEnv *env, jobject obj, jint num)
{
    return num * num;
}
 
JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
  (JNIEnv *env, jobject obj, jboolean boolean)
{
    return !boolean;
}
 
JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
  (JNIEnv *env, jobject obj, jstring string)
{
    const char* str = env->GetStringUTFChars(string, 0);
    char cap[128];
    strcpy(cap, str);
    env->ReleaseStringUTFChars(string, 0);
    return env->NewStringUTF(strupr(cap));
}
 
JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
  (JNIEnv *env, jobject obj, jintArray array)
{
    int i, sum = 0;
    jsize len = env->GetArrayLength(array);
    jint *body = env->GetIntArrayElements(array, 0);
     
    for (i = 0; i < len; ++i)
    {
        sum += body[i];
    }
    env->ReleaseIntArrayElements(array, body, 0);
    return sum;
}

上述两端代码非常相似, 只有一个不同点

C代码: (*env)->GetStringUTFChars(env, string, 0);

C++代码: env->GetStringUTFChars(string, 0);

C语言中使用的是结构体的函数指针, 而在C++中使用的还是struct, 我们知道struct在C++中和class的功能是几乎一样的, struct也可以用来定义类, 所以env在C++中是个类对象的指针.

编译和运行

这里使用的是g++编译器, 编译C++语言版的.so

g++ Sample1.cpp  -I /home/user-name/tkaped/jdk1.7.0_80/include -I /home/user-name/tkaped/jdk1.7.0_80/include/linux  -fPIC -shared -o libSample1.so

运行

>javac Sample1

>java -cp . -Djava.library.path=/home/chris/tkaped/demo Sample1 #执行JNI测试


如果您有这样的错误, 请使用32位的JDK来重新运行.

运行结果如下:

intMethod: 25
booleanMethod: false
stringMethod: JAVA
intArrayMethod: 36

【来自转载】http://www.cnblogs.com/icejoywoo/archive/2012/02/22/2363709.html


从java程序中传过去的String对象在本地方法中对应的是jstring类型,jstring类型和c中的char*不同,所以如果你直接当做char*使用的话,就会出错。因此在使用之前需要将jstring转换成为c/c++中的char*,这里使用JNIEnv的方法转换.

下面是访问String的一些方法:
GetStringUTFChars将jstring转换成为UTF-8格式的char*
GetStringChars将jstring转换成为Unicode格式的char*
ReleaseStringUTFChars释放指向UTF-8格式的char*的指针
ReleaseStringChars释放指向Unicode格式的char*的指针
NewStringUTF创建一个UTF-8格式的String对象
NewString创建一个Unicode格式的String对象
GetStringUTFLengt获取 UTF-8格式的char*的长度 
GetStringLength获取Unicode格式的char*的长 度


JNI 支持字符串在 Unicode 和 UTF-8 两种编码之间转换。

JNIEXPORT jstring JNICALL  Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) 
 { 
     char buf[128]; 
     const jbyte *str; 
     str = (*env)->GetStringUTFChars(env, prompt, NULL); 
     if (str == NULL) { //不要忘记检测,否则分配内存失败会抛出异常
         return NULL; /* OutOfMemoryError already thrown */ 
     } 
     printf("%s", str); 
     (*env)->ReleaseStringUTFChars(env, prompt, str); 
     
     /* We assume here that the user does not type more than  * 127 characters */ 
     scanf("%s", buf); 
     return (*env)->NewStringUTF(env, inbuf); 
}

(1)GetStringUTFChars可以把一个 jstring指针(指向JVM内部的Unicode字符序列)转化成一个UTF-8格式的C 字符串。
(2)从GetStringUTFChars 中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars 告诉 JVM 这个 UTF-8 字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。
(3)JNI 函数 NewStringUTF 在本地方法中创建一个新的java.lang.String字符串对象.这个新创建的字符串对象拥有一个与给定的
UTF-8编码的C类型字符串内容相同的 Unicode 编码字符串

UTF-8 字符串以’\0’结尾,而 Unicode 字符串不是。
如果一个jstring指向一个 UTF-8编码的字符串,为了得到这个字符串的字节长度,可以调用标准 C 函数 strlen,当然也可以用GetStringUTFLength

GetStringChars 和 GetStringUTFChars 函数中的第三个参数需要更进一步的解释:
const jchar * GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

当从 JNI 函数 GetStringChars 中返回得到字符串B时,如果B是原始字符串java.lang.String 的拷贝,则isCopy被赋值为 JNI_TRUE。
如果B和原始字符串指向的是JVM中的同一份数据,则 isCopy被赋值为 JNI_FALSE。
当 isCopy值为JNI_FALSE时,本地代码决不能修改字符串的内容,否则JVM中的原始字符串也会被修改,这会打破 JAVA语言中字符串不可变的规则。
通常,因为你不必关心 JVM 是否会返回原始字符串的拷贝,你只需要为 isCopy传递NULL作为参数。

【来自转载】http://www.cnblogs.com/lijunamneg/archive/2012/12/22/2828891.html



猜你喜欢

转载自blog.csdn.net/gwzz1228/article/details/71425643