一、环境检查
在linux下打包.so文件,首先需要确认是否有安装java环境,可通过在终端中输入指令java的方式来进行查看。如下图所示,则为已安装java环境。
若当前未安装java环境,则可通过在终端中输入如下指令进行安装,我这里使用的java环境为1.8.0版本。
sudo apt-get install openjdk-8-jdk |
如果需要安装不同版本则,修改openjdk-8-jdk即可,如:需要安装openJDK11,则使用入下指令:
sudo apt-get install openjdk-11-jdk |
安装完成后可输入如下指令查看java版本。
Java -version |
我这里查询出来java版本为1.8.0,说明java已安装成功。
二、Java环境变量配置
在进行环境变量配置之前,首先需要找到java的安装目录,通常情况下默认安装路径为:/usr/lib/jvm/
打开文件/etc/profile
sudo vim /etc/profile |
添加如下内容:
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 export PATH=$PATH:$JAVA_HOME/bin |
添加完成后重启系统完成配置,可在终端中输入如下命令来查询JAVA_HOME是否配置成功。
echo $JAVA_HOME |
三、新建.java类
新建一个名为helloword.java的类。
使用System.loadLibrary来导入库,并将需要生成头文件的C语言接口通过public native进行声明。
四、生成C语言头文件
使用如下指令生成.class文件。
javac helloworld.java |
使用如下指令生成.h文件
javah helloworld |
打开头文件可以看到生成的C程序接口声明。
五、新建.c文件并实现它
新建helloworld.c文件,并实现其内容。
六、生成.so文件
在终端中输入如下命令
gcc -shared -fPIC -o libhelloworld.so helloworld.c -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux |
其中${JAVA_HOME}/include和${JAVA_HOME}/include/linux分别为jni.h和jni_md.h所在路径,完成命令输入后即可在文件目录下看到名为libhelloworld.so的库文件。
七、调用运行
在终端输入指令
java -Djava.library.path=. helloworld |
其中” . ”表示的是.so文件所在路径,如果存放在其他位置,则此处也应做出对应修改。完成指令输入后可看到输出结果。
八、注意事项
8.1 .so文件打包
在实际的工作中,我们所导出的.so库会需要导入到如idea这样的集成IDE中使用,而上述流程中的所打包出来的库在一定情况下可能不能满足实际要求而导致无法调用。
在我的工作中就遇到了这样的一个案例:
开发环境使用的是集成IDE idea,java版本为1.8.0,整个系统的执行代码被存放在名为“com.sunward.nettyTcp.iot”的软件包中,通过JNI打包的.so文件是通过在函数名前添加包名的方式以保证外部能够对函数名进行顺利访问,如下所示
而在使用上述7个步骤进行打包时,我发现无论是通过将文件添加到文件目录“com/sunward/nettyTcp/iot”的方式还是在javah指令中输入指定路径的方式都无法成功实现在.h的函数名前添加包名。后来我发现了一个方法去解决它,即在.java类名前手动添加上我们需要的包名,如下:
通过指令生成.h文件,打开.h文件后发现了一个问题,即在包名中会存在一个“_1”。
这个“_1”经过查询后可知是用于替换包名中的“ . ”的,查询的原文描述如下:
但是这里经过实际测试,带有“_1”的.so库,无法在idea中顺利调用,猜测原因是因为idea的文件管理所导致的,因此在导出成供idea调用的库时,需要将.h文件中的“ _1 ”修改为“ _ ”,并在.c文件中对其函数进行代码实现。
导出为.so库后,复制到idea工作目录下,并对JVM进行路径配置,方法如下:
完成如上操作后即可顺利调用.so库内容。
8.2 .c文件实现
在进行C代码实现时需要注意java与C之间的数据类型转换,如以下示例:
新建iotSystemDedicatedFunctionForJava.c文件,并将需要进行调用的C语言函数添加至此文件中。
这里C语言接口FunSea48ByteEncode的函数实现如下:
void FunSea48ByteEncode(unsigned char PlainText[], unsigned char CipherText[]) { unsigned int i = 0;
for (i = 0; i < 48; i++) { CipherText[i] = PlainText[i]; } } |
将JNIEXPORT void JNICALL Java_iotSystemDedicatedFunctionForJava_FunSea48ByteEncode (JNIEnv *, jobject, jintArray, jintArray)声明复制到.c文件中进行实现,如下:
JNIEXPORT void JNICALL Java_iotSystemDedicatedFunctionForJava_FunSea48ByteEncode (JNIEnv *env, jobject obj, jintArray plainText, jintArray cipherText) { // 获取数组长度和指针 jsize plainTextLength = (*env)->GetArrayLength(env, plainText); jsize cipherTextLength = (*env)->GetArrayLength(env, cipherText);
// 检查数组长度是否为48 if (plainTextLength < 48 || cipherTextLength < 48) { // 处理错误情况,例如抛出Java异常 return; }
unsigned int *cPlainText = (unsigned int *)(*env)->GetIntArrayElements(env, plainText, NULL); unsigned int *cCipherText = (unsigned int *)(*env)->GetIntArrayElements(env, cipherText, NULL);
if (cPlainText == NULL || cCipherText == NULL) { // 处理无法获取数组元素的情况 return; }
unsigned char tPlainText[48]; unsigned char tCipherText[48];
for (int i = 0; i < 48; i++) { tPlainText[i] = cPlainText[i] & 0xFF; } // 调用C函数 FunSea48ByteEncode(tPlainText, tCipherText);
for (int i = 0; i < 48; i++) { cCipherText[i] = tCipherText[i]; } // 释放资源并更新Java数组 (*env)->ReleaseIntArrayElements(env, plainText, (jint *)cPlainText, 0); (*env)->ReleaseIntArrayElements(env, cipherText, (jint *)cCipherText, 0); }
|
这里值得注意的是,C语言接口的参数传参是unsigned char类型,而在java中并没有提供与之相同的数据类型,也并未提供unsigned关键字,因此在java中只能通过大的数据类型来代替unsigned char防止数据溢出,这里使用int来代替unsigned char。
在实际的使用当中发现,在java中将int转换为C语言的char不同于C语言中的隐式转换,在java中如果将int强转成C语言的char,会将int拆分为4个char元素,如下图所示。
因此上述代码如果直接将48大小的int类型数组强转为unsigned char,则会得到一个有192字节的数组。
在调用C语言函数之前通过如下操作将int类型的数组转换成unsigned char类型数组,确保转换后每一个int元素对应1个unsigned char,即每一个int丢弃掉前三个字节。
for (int i = 0; i < 48; i++) { tPlainText[i] = cPlainText[i] & 0xFF; } |
并在C语言接口FunSea48ByteEncode(tPlainText, tCipherText);调用完成后,通过如下方式将unsigned char转换回int,即补全unsigned char每个元素前缺少的三个字节。
for (int i = 0; i < 48; i++) { cCipherText[i] = tCipherText[i]; } |