JNI系列教程二——数据结构

JNI和java相互调用,一个不可避免的问题就是两者的数据结构要相互转换。这一节正是要讲这个重头戏。

本文源地址http://blog.whyun.com/posts/jni/2-struct-transform/ 转载请注明出处。

2.1 基本类型

基本数据类型大都是数字类型表2.1.1中给出了java和jni的对应关系。

Java 类型 本地类型 说明
boolean jboolean 无符号,8 位
byte jbyte 无符号,8 位
char jchar 无符号,16 位
short jshort 有符号,16 位
int jint 有符号,32 位
long jlong 有符号,64 位
float jfloat 32 位
double jdouble 64 位

表2.1.1 基本数据类型对照表

2.2 对象类型

对象类型的对照关系牵扯到的知识点比较多,所以下面决定通过具体例子来让大家更好的学习它。 首先在java代码有这么两行行声明

public native int getSum(int a, int b);
public native int getSum(byte [] array);

第一个函数是一个基本数据类型,第二个函数是一个数组,虽然两个函数在java中是同名的,但是生成的c文件却不能使用同名文件,

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    getSum
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_whyun_jni_chapter2_StructDemo_getSum__II
    (JNIEnv *env, jobject obj, jint a, jint b) {
        return a+b;
};

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    getSum
 * Signature: ([B)I
 */
JNIEXPORT jint JNICALL Java_com_whyun_jni_chapter2_StructDemo_getSum___3B
  (JNIEnv *env, jobject obj, jbyteArray arr) {
      int sum = 0;
      char *data = (char*)(*env)->GetByteArrayElements(env,arr,NULL);
      int dataLen = (int)(*env)->GetArrayLength(env,arr);
      int i = 0;
      for (i=0;i<dataLen;i++) {
          sum += data[i];
      }
      (*env)->ReleaseByteArrayElements(env,arr,data,0);
      return (jint)sum;
}

我们通过javah生成头文件时,就会发现生成的getSum的函数后缀是不一样的。这里重点看JNI中对数组的操作,在实际编程中我们常常要处理二进制数据,那么自己数组便是经常要使用的一个数据结构了。java中的byte[]最终在JNI中被转化为jbyteArray,但是jbyteArray要想在C语言中使用,还必须得到一个C语言中可识别的char类型指针的形式,这就是函数GetByteArrayElements的作用。要想知道当前数组的长度,可以使用函数GetArrayLength。我们可以推断出GetByteArrayElements内部申请了一块内存,也就是说变量data是通过类似于malloc之类的函数申请得到的,所以最终在使用完成之后是需要释放的,所以才有了函数最后调用ReleaseByteArrayElements函数的代码。

下面看一个复杂的例子通过JNI来调用java函数,直接上JNI的代码:

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    getUserList
 * Signature: (I)Ljava/util/ArrayList;
 */
JNIEXPORT jobject JNICALL Java_com_whyun_jni_chapter2_StructDemo_getUserList
  (JNIEnv *env, jobject obj, jint num) {
    int count = (int)num,i=0;
    jclass clsUserBean = (*env)->FindClass(env,"com/whyun/jni/bean/UserBean");
    jclass clsArrayList = (*env)->FindClass(env,"java/util/ArrayList");
    jmethodID userBeanConstructor = (*env)->GetMethodID(env,clsUserBean,"<init>","()V");
    jmethodID userBeanSetAge = (*env)->GetMethodID(env,clsUserBean,"setAge","(I)V");
    jmethodID userBeanSetName = (*env)->GetMethodID(env,clsUserBean,"setName","(Ljava/lang/String;)V");
    jmethodID arrayListContructor = (*env)->GetMethodID(env,clsArrayList,"<init>","(I)V");
    jmethodID arrayListAdd = (*env)->GetMethodID(env,clsArrayList,"add","(ILjava/lang/Object;)V");

    jobject arrayList = (*env)->NewObject(env,clsArrayList,arrayListContructor,num);

    char nameStr[5] = {0};
    int index = 0;
    jstring name;

    for(i=0;i<count;i++) {
        jobject userBean = (*env)->NewObject(env,clsUserBean,userBeanConstructor);

        for (index=0;index<4;index++) {
            nameStr[index] = randStr[(i+7)%5];
        }
        name = (*env)->NewStringUTF(env,(const char *)nameStr);
        (*env)->CallVoidMethod(env,userBean,userBeanSetAge,(jint)(20+i));
        (*env)->CallVoidMethod(env,userBean,userBeanSetName,name);

        (*env)->CallVoidMethod(env,arrayList,arrayListAdd,(jint)i,userBean);
    }
    return arrayList;
}

如果你稍加揣测的话,这段代码翻译成java代码应该是这么写的:

public ArrayList<UserBean> getUserList(int count) {
    ArrayList<UserBean> list = new ArrayList<UserBean>(count);
    for(int i=0;i<count;i++) {
        UserBean bean = new UserBean();
        bean.setAge(i+20);
        bean.setName("name"+i);
        list.add(i,bean);
    }
    return list;
}

看上去java代码要简单许多,在JNI中包括类、成员函数、对象之类的数据都需要先创建再使用。在java中创建对象用new UserBean()就够了,但是在JNI中,你首先要通过FindClass函数来找到类定义,然后通过类定义用函数GetMethodID来找到构造函数,然后根据类定义和构造函数通过函数NewObject来获取一个对象,下面分别对这三个函数进行讲解。
FindClass函数的第二个参数是要加载的类的类名全称,在java中我们应该写作com.whyun.jni.bean.UserBean,在JNI中就是把.换成了/而已。
GetMethodID函数的第二个参数是FindClass函数得到的类变量clsUserBean,第二个参数是函数名,一般来说函数名直接写函数名称就行了,比如说你再往下看一行代码获取UserBean的setAge函数的时候就直接写的函数名,但是构造函数就不同了,所有类的构造函数在JNI中统一叫<init>。最后一个参数很重要,它是java函数的签名,java中每个函数和属性都有一个它的标识,这个标识用来指出当前函数的参数、返回值类型或者属性的类名,可能有些人第一听说这个概念,其实获取这个标识有一个很简单的方法,就是命令javap,下面先做个小实验,运行javap -s java.lang.String,会输出如下内容:

Compiled from "String.java"
public final class java.lang.String implements java.io.Serializable, java.lang.Comparable<java.lang.String>, java.lang.CharSequence {
  public static final java.util.Comparator<java.lang.String> CASE_INSENSITIVE_ORDER;
    Signature: Ljava/util/Comparator;
  public java.lang.String();
    Signature: ()V

  public java.lang.String(java.lang.String);
    Signature: (Ljava/lang/String;)V

  public java.lang.String(char[]);
    Signature: ([C)V

  public java.lang.String(char[], int, int);
    Signature: ([CII)V

  public java.lang.String(int[], int, int);
    Signature: ([III)V
由于文件内容比较多,所以省略掉下面内容

由于java是支持重载的,一个函数可能会拥有多种实现方式,比如String类的构造函数就有N多个,那么你在调用其函数的时候,就必须得依靠参数和返回值类型来区分不同的函数了,而签名正提供了一种简介的方式来表示一个函数的参数和返回值。通过刚才javap命令的输出,我们可以得到对于没有参数的String构造函数,其签名为()V;对于参数为字符数组的构造函数签名为([C)V。 接着讲函数NewObject,前面经过FindClassGetMethodID一顿折腾,我们拿到了两个变量类变量clsUserBean和函数变量userBeanConstructor,将其传到NewObject中就能得到一个对象。在我们的代码中还有一个对于ArrayList类型的对象的构造,他调用NewObject的时候比UserBean多了一个参数,那是由于我们使用的构造函数为ArrayList(int i),故需要传递一个ArrayList的长度参数,这里需要声明的是NewObject函数的参数个数是可变的,调用的构造函数有参数,就依次追加到后面即可。 终于讲到真正调用java函数这一步了,就是代码中的CallVoidMethod,和NewObject一样,它也是可变参数,参数形式也一样。除了CallVoidMethod,JNI中还有各种Call[Type]Method,这个Type就代表了java函数的返回值,它可以是Object Boolean Byte Char Short Int Long Float Double

2.3 使用异常

c语言中没有异常这个概念,使用c代码的时候多是通过返回码来判断是否调用成功,然而对于java程序来说判断有没有成功,往往是看有没有异常抛出,所以说编写一个java友好的JNI程序,我们需要将错误码转成java异常。直接上例子:

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    showException
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_whyun_jni_chapter2_StructDemo_showException
  (JNIEnv *env, jobject obj) {
      jclass exception = (*env)->FindClass(env,"java/lang/Exception");
      (*env)->ThrowNew(env,exception,"This is a exception.");
}

这里ThrowNew转成java的话,就是throw new Exception("This is a exception.");其它的我就不多说了。

Comments

JNI系列教程一——入门

1.1 背景

和很多语言类似,java也提供了调用原生代码的功能,这门技术叫做JNI。有了JNI,可以在付出更小的代价的前提下,复用大量已经写好的C/C++库,当然一般用JNI的目的还是由于java在处理计算密集型(比如说非对称运算)的操作时有时会力不从心。
从结构上来看JNI是一个中间层,具体的调用步骤是这个样子的:java->JNI->C/C++。

本文源地址http://blog.whyun.com/posts/jni 转载请注明出处

1.2 准备活动

1.2.1 编写java代码

package com.whyun.jni.chapter1;

/**
 * User: sunny
 * Date: 15-10-28
 * Time: 下午12:29
 */
public class FirstDemo {
    static{
        System.loadLibrary("firstdemo");
    }
    public native int getNum();
    public native String getString();
    public static void main(String []args) {
        FirstDemo demo = new FirstDemo();
        System.out.println("num:"+demo.getNum()+",string:"+demo.getString());
    }
}

代码 1.2.1.1 注意到我们在类FirstDemo中定义了两个成员函数都没有函数体,且都加了关键字native声明,如果函数写成这样,则代表当前函数需要调用底层C/C++代码。注意System.loadLibrary这句话,java中使用这个函数来加载动态库,windows平台下运行此段代码要保证firstdemo.dll存在环境变量%path%中,linux平台下要保证libfirstdemo.so存在环境变量$LD_LIBRARY_PATH中。

其实java在loadLibrary的时候,是读取的系统变量java.library.path来搜寻动态库位置的,你可以用System.getProperty("java.library.path")来输出这个变量的内容。只不过在windows中会把环境变量%path%的内容加入到这个变量中,在linux中会把环境变量$LD_LIBRARY_PATH加入到这个变量中。在我的一台linux上打印java.library.path,会输入如下内容: /usr/jdk1.6.0_45/jre/lib/i386/server:/usr/jdk1.6.0_45/jre/lib/i386: /usr/jdk1.6.0_45/jre/../lib/i386: /home/username/lib::/usr/java/packages/lib/i386 :/lib:/usr/lib 其中/home/username/lib:是从环境变量$LD_LIBRARY_PATH读取的。不推荐将生成的动态库放置到系统目录中,首先是不一定有管理员权限,其次会导致系统库目录下的文件过多,不易管理。

1.2.2 生成头文件

本文用到的项目源码在文后给出,项目的目录结构如下:
项目目录结构
图1.2.2 项目目录结构
其中目录out/production为我们的class文件生成的目录,在命令行下进入该目录,运行如下命令javah com.whyun.jni.chapter1.FirstDemo,运行成功之后则在运行命令行的目录下生成文件com_whyun_jni_chapter1_FirstDemo.h,用文本编辑器打开这个头文件,会显示如下内容:

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

#ifndef _Included_com_whyun_jni_chapter1_FirstDemo
#define _Included_com_whyun_jni_chapter1_FirstDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_whyun_jni_chapter1_FirstDemo
 * Method:    getNum
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_whyun_jni_chapter1_FirstDemo_getNum
  (JNIEnv *, jobject);

/*
 * Class:     com_whyun_jni_chapter1_FirstDemo
 * Method:    getString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_whyun_jni_chapter1_FirstDemo_getString
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

代码 1.2.1

1.3 编译运行

为了简化在windows和linux下配置编译步骤,我们先在操作系统中配置两个环境变量,在windows上将JAVA_HOME配置为JDK的安装路径,很多情况下这个环境变量在配置JDK编译环境的时候已经配置过,可以通过echo %JAVA_HOME%的输出来判断之前是否已经配置过;同理我们在linux上也配置环境变量JAVA_HOME(通过运行echo $JAVA_HOME来检测是否存在)。同时我们在windows上将目录d:\lib加入环境变量PATH中,在linux上将/opt/lib加入环境变量LD_LIBRARY_PATH中。

1.3.1 windows编译环境配置

windows下需要安装visual studio(简称vs)环境来完成C/C++编译,vs 有professional、ultimate和Express三个版本,前两者收费,我们使用免费的Express就够用了。我电脑上装的是vs express for desktop版本。
打开vs,新建项目,选择Visual C++,然后选择Empty Project,输入项目名firstdemo,点击确定。
接着设置项目属性,右击项目,然后选择properties,在Configuration Properties->General->Project Defaults->Configuration Type中,选择Dynamic Library (.dll)。然后在Configuration Properties->VC++ Directories->General->Include Directories中添加两个路径:$(JAVA_HOME)\include$(JAVA_HOME)\include\win32
最后编写c代码,在vs中新建源代码的时候,默认是cpp后缀,我们这里建一个c后缀的文件,因为我的编写习惯是c语法。

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

/*
 * Class:     com_whyun_jni_chapter1_FirstDemo
 * Method:    getNum
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_whyun_jni_chapter1_FirstDemo_getNum
  (JNIEnv *env, jobject obj) {
      return (jint)1;
}

/*
 * Class:     com_whyun_jni_chapter1_FirstDemo
 * Method:    getString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_whyun_jni_chapter1_FirstDemo_getString
  (JNIEnv *env, jobject ob) {
      jstring jinfo            = (*env)->NewStringUTF(env,"the first demo.");
      return jinfo;
}

代码1.3.1 firstdemo.c
注意我们引用了之前的com_whyun_jni_chapter1_FirstDemo.h,我们把它放到firstdemo.c同一级目录下了。
接着运行编译,产生dll文件,默认情况下会在项目目录Debug文件夹下产生firstdeom.dll,你可以把这个文件夹添加到环境变量PATH中去,也可以写脚本在编译完成之后将dll拷贝到电脑的任何一个PATH路径下,在Configuration Properties->Build Events->Post-Build Event->Command Line中写入如下命令:copy "$(TargetDir)$(TargetName).dll" d:\lib\$(TargetName).dll

1.3.2 linux编译

在linux下使用命令行GCC即可,将com_whyun_jni_chapter1_FirstDemo.hfirstdemo.c放到同一个目录下,然后运行脚本

saveDir=/opt/lib
gcc -g -Wall -I $JAVA_HOME/include/ -I $JAVA_HOME/include/linux -fPIC -shared -o $saveDir/libfirstdemo.so firstdemo.c

1.3.3 运行

运行java代码,最终输出

num:1,string:the first demo.

本文用到的源代码可以从http://git.oschina.net/yunnysunny/jni 获得到

Comments