JNI

最近项目中需要添加JNI接口,顺便学习了一下JNI。JNI简单来说就是Java和C++交互的一个中间层。Java可以通过JNI来调用C++代码,反之亦可。

HelloJNI

JNI在Java中的体现需要用native关键字来修饰,而需要使用库则使用System.loadLibrary("hello")来加载库。先来个最基本的HelloJNI。

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloJNI {
static {
System.loadLibrary("hello");
}

private native void sayHello();

public static void main(String[] args) {
HelloJNI hello = new HelloJNI();
hello.sayHello();
}
}

使用Javac编译这个文件

1
javac -h . HelloJNI.java

之后会生成HelloJNI.class和HelloJNI.h两个文件。生成头文件中定义了对应Native的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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

接下来就是实现这个Native的方法,需要导入jni.h和HelloJNI.h。

1
2
3
4
5
6
7
8
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
#include <iostream>
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thiz)
{
std::cout << "Hello JNI" << std::endl;
}

然后开始编译lib库,需要注意的是Java中System.loadLibrary("hello")这个lib的名称在编译库的时候需要加上lib,也就是libhello

1
2
3
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin HelloJNI.cpp -o HelloJNI.o

g++ -dynamiclib -o libhello.dylib HelloJNI.o -lc

最后执行下,cp就是classpath,是指定类运行所依赖其他类的路径。-Djava.library.path指定加载库的路径。

1
java -cp . -Djava.library.path=. HelloJNI

传递参数

通常调用lib库的函数需要传递一些参数,这就是需要JNI的帮忙了。

如果是基础类型直接使用就可以。在JNI都有和JAVA一一对应的基础类型,例如jint - int、jboolean - boolean。

JNI的基础类型也可以直接传入给C++的函数中。

复杂一点的是传递一个对象,需要从对象中拿到成员变量值。

首先需要搞清楚jobject和jclass的概念,其实和Java中的Object和Class的概念是一样的。Class可以理解为是一种模板,而Object则是具体的一个对象。下面代码中User就是一个类,而star就是一个具体的对象也就是Object

1
User star = new User();

假设有这样一个类User,有两个成员变量,name和password

1
2
3
4
5
6
7
8
class User {
String name;
String password;
User(String name, String password) {
this.name = name;
this.password = password;
}
}

定义了一个native方法来传递User的参数

1
private native void passUserObject(User user);

对应的JNI函数为

1
JNIEXPORT void JNICALL Java_HelloJNI_passUserObject(JNIEnv *env, jobject thiz, jobject user){}

在JNI中是不能直接使用user.name来获取的,必须提供给JNI一个成员变量的名称和类型他才能看得懂。而这两者都是Class的概念,因此需要先拿到user的class,这两种方法都可以拿到

1
2
jclass userClass = env->FindClass("packagename/UserData");
jclass cls = env->GetObjectClass(user);

接下来就是去拿成员变量的id,第二个参数是名称,第三个参数是类型。如果是自定义的类型则是"L+PackageName/Type;",包名依然是用斜杠分开。

1
jfieldID name_id = env->GetFieldID(cls, "name", "Ljava/lang/String;");

之后就是根据这个id在传进来的对象user中取出name

1
jstring name = (jstring)env->GetObjectField(user , name_id);

如果是int类型的参数可以直接用env->GetIntField(user, id)

枚举

如果是枚举类型则稍微复杂一点。枚举类型都有个方法ordinal是返回枚举值。

1
2
3
4
5
enum class VideoStreamType {
NotVideo, //VideoStreamType.NotVideo.ordinal return 0
Video,
Presentation
}

利用这个方法我们可以将枚举转化成int类型,然后再map成C++的枚举类型。

首先将枚举类型变量取出 - vsTypeJni_

1
2
jfieldID vsType_id = env->GetFieldID(eccVideoParasClass, "vsType","Lcom/star/VideoStreamType;");
jobject vsTypeJni_ = env->GetObjectField(videoParas, vsType_id);

然后调用ordinal方法,返回vsTypeJni_对应的int值,这个vsTypeJint就是我们想要的。

1
2
3
jclass vsTypeClass = env->GetObjectClass(vsTypeJni_);
jmethodID ordinal = env->GetMethodID(vsTypeClass, "ordinal", "()I");
jint vsTypeJint = env->CallIntMethod(vsTypeJni_, ordinal);

如果想返回枚举的话则需要先用Java枚举的values方法获取到所有存储枚举类型的数组,然后根据index得到对应的Java枚举类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
inline jobject GetEnumObjectRingerType(RingerType type) {

jobject result = nullptr;

bool bAttached = false;
JNIEnv *env = JniBase::AttachEnv(JniBase::ms_jvm, bAttached);

if (env) {

jclass enumClass = JniBase::FindClass(env,
"com/star/RingerType");
jmethodID methodId = env->GetStaticMethodID(enumClass, "values",
("()[Lcom/star/RingerType;"));
jobjectArray valueArray = static_cast<jobjectArray>(env->CallStaticObjectMethod(
enumClass, methodId));
int index = GetEnumRingerTypeAtIndex(type);
result = env->GetObjectArrayElement(valueArray, index);

env->DeleteLocalRef(valueArray);
env->DeleteLocalRef(enumClass);
}

JniBase::DetachEnv(JniBase::ms_jvm, bAttached);

return result;
}

inline int GetEnumRingerTypeAtIndex(RingerType type) {
int result = 0;
switch (type) {
case RingerType::Outgoing:
result = 1;
break;
case RingerType::BusyTone:
result = 2;
break;
case RingerType::Reconnect:
result = 3;
break;
}
return result;
}

调用Java方法

在处理枚举类型的时候调用了Java方法,和获取变量一样,需要先获取方法id,第二个参数是方法名,第三个参数中括号中的是入参类型,括号后的是返回值,例如env->GetMethodID(class, "calculate", "(III)I",jint a, jint b, jin c)是这样的方法。

1
int calculate(int a, int b, int ) {}

然后就可以调用Java方法了

1
env->CallObjectMethod(object, methodId);

总结

对JNI来说,它只是提供了一个Java和Native代码之间的桥梁,因此没有太多复杂的东西,jclass供我们获取方法或者变量的id,一旦id拿到我们就可以处理对象了。

然而JNI也有缺点,它丢失了Java语言最大的特点:write once, run anywhere。并且由于多了一层导致程序更复杂并且在通信时有额外的消耗。