JNI学习笔记
1. 使用Java程序调用C++函数步骤
-
创建包含本地方法的Java类:
package org.example; public class HelloWorld { static { System.loadLibrary("HelloWorld"); } public native void print(); public static void main(String[] args) { new HelloWorld().print(); } }
-
使用javac编译生成HelloWorld.class文件:
javac HelloWorld
-
使用2生成的HelloWorld.class,通过javah在src/main/java目录下生成C++头文件,详见以下2,3节:
javah org.example.HelloWorld
此时生成了org_example_HelloWorld.h头文件。
-
编写本地方法实现,创建org_example_HelloWorld.cpp,在其中实现HelloWorld.print方法:
#include <jni.h> #include <stdio.h> #include <org_example_HelloWorld.h> JNIEXPORT void JNICALL Java_org_example_HelloWorld_print(JNIEnv *env, jobject obj){ printf("Hello World!\n"); return ; }
-
编译C++源码并生成一个本地库,用于Java代码中的
System.loadLibrary("HelloWorld");
,这里我使用g++/gcc:g++ -fno-pie -fPIC -no-pie -shared -I ${JAVA_HOME}/include -I ${JAVA_HOME}/include/linux -I . -o libHelloWorld.so org_example_HelloWorld.cpp -L . -l decodeplugin
这里 -I后面的是指头文件所在目录,-o指明导出的本地库位置,-shared说明导出动态库。
-
在java目录下运行,命令如下:
java -Djava.library.path=./org/example/ org.example.HelloWorld
2. 踩坑
2.1 javah无法生成头文件
需要到src/main/java目录下使用javah,命令是:javah java类的完整名(包名+类名)
例如:javah org.example.HelloWorld
2.2 g++ 报错
报错:/usr/bin/ld: /tmp/ccEtEkwZ.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
原因:需要在编译命令里加入 -fPIC。
2.3 运行报错
报错:找不到HelloWorld库
这里比较坑,原因有两个,一个是我编译本地库时名字是HelloWorld.so,而正确的名字应该是libHelloWorld.so;第二个问题是我运行的是org.example.HelloWorld,所以library需要设置为libHelloWorld.so所在的目录,即./org/example/(当前目录是src/main/java)
报错:Exception in thread "main" java.lang.UnsatisfiedLinkError: /home/jni/lib/libVapServiceProxy.so: libGieBuildAndInfer.so: cannot open shared object file: No such file or directory
这里是因为生成的so文件依赖其他so文件,而其他so文件找不到,这里有可能有三个原因:
- 在使用g++生成命令的时候,-l 后面没有加依赖的so文件;
- 使用ldd so文件名 查看下依赖的so库,有可能有so显示未找到;
- LD_LIBRARY_PATH地址没有填依赖的so库地址,需要使用
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/jni/lib/
补上地址。
3. javah生成的.h文件解析
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_example_HelloWorld */
#ifndef _Included_org_example_HelloWorld
#define _Included_org_example_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_example_HelloWorld
* Method: print
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_org_example_HelloWorld_print
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
#include:引用头文件
#ifndef:宏命令,后接标识符 意为:如果不包含标识符
#define:宏命令,后接标识符 意为:定义标识符
#ifdef:宏命令,后接标识符 意为:如果包含标识符;此文件中,后接的__cplusplus
用于识别编译器,即将当前代码编译的时候,是否将代码作为C++进行编译,如果是,则定义了__cplusplus
extern "C"{:实现C++和C以及其他语言的混合编程。
3.1 extern "C"{ 说明
举例如下:
- C++引用C函数:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
cExample.h 是C语言的头文件,在C++中使用extern "C"
包含,这样C++在链接C库时,采用C的方式进行链接(即寻找_add
而不是_add_int_int
)。
- C引用C++函数:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
此时,C不能直接使用#include "cppExample.h"
,因为C不支持extern "C"
,这里C++文件使用extern "C"
目的是让C++编译时生成C形式的符号,将其添加到C++实现库中,以便C能找到。
总结:
-
extern "C" 只是 C++ 的关键字,不是 C 的;
如果在 C 程序中引入了 extern "C" 会导致编译错误。
-
被 extern "C" 修饰的目标一般是对一个全局C或者 C++ 函数的声明
从源码上看 extern "C" 一般对头文件中函数声明进行修饰。
C
和cpp
中头文件函数声明的形式都是一样的(因为两者语法基本一样),对应声明的实现却可能由于语言特性而不同了(C
库和C++
库里面当然会不同)。 -
extern "C" 这个关键字声明的真实目的,就是实现 C++ 与C及其它语言的混合编程
一旦被 extern "C" 修饰之后,它便以
C
的方式工作(编译阶段:以C的方式编译,链接阶段:寻找C方式编译生成的符号),C
中引用C++
库的函数,或C++
中引用C
库的函数,都可以通过这个方式(即在C++文件中用extern "C" 声明,实现兼容。
3.2 本地方法
生成的本地方法C实现接受两个参数,尽管Java中没有接受任何参数。第一个参数事JNIEnv的接口指针,第二个参数是HelloWorld对象本身,类似C++中的this指针。
4.数据类型映射
4.1 String类型使用和转换
转换
Java中的String在本地方法中变为jstring,而jstring和C++中的字符串char*并不等价,不能替换使用,此时需要使用JNI方法GetStringUTFChars:
const char* model_Dir = env->GetStringUTFChars(modelDir, NULL);
不要忘记检查 GetStringUTFChars 的返回值,这是因为 Java 虚拟机的实现决定内部需要申请内存来容纳 UTF-8 字符串,内存的申请是有可能会失败的。如果内存申请失败,那么 GetStringUTFChars 将会返回 NULL 并且会抛出 OutOfMemoryError 异常。
释放
在使用完之后需要释放本地字符串:
env->ReleaseStringUTFChars(modelDir, model_Dir);
创建
通过调用JNI函数NewStringUTF,可以在本地代码中创建一个新的Java.lang.String 实例。同样,需要检查返回值是否为NULL。
其他
以Unicode格式获取和释放字符串:GetStringChars和ReleaseStringChars
统计字符串长度:GetStringUTFLength/GetStringLength
以 Unicode 格式将字符串的内容复制到预分配的 C 缓冲器到或从预分配的 C 缓冲区中复制:GetStringRegion\SetStringRegion.
4.2 数组
同String一样,数组在JNI中转换为jarray及其子类型,例如jintArray,这些类型也不是C/C++数据类型,不能直接访问,需要通过JNI函数间接访问。使用Get/ReleaseIntArrayElements获取/释放数组:
jint *carr;
carr = (*env)->GetIntArrayElements(env, arr, NULL);
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
对象数组获取方式:
JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size)
{
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env, "[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
for (i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough! */
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}