摘要:以下将以一个实际例子展示通过调用打印主要记录实现的过程和方法,对其中的一些原理和规范不做具体展开。指向在此代码中实例化的对象的一个句柄,相当于指针。加载本地共享库运行结果如下传递参数接下来看一下如何通过向传递参数。
文章为本人编纂,转载请联系作者并注明出处。
在日常项目中,我们可能会遇到需要用Java去命令行执行命令或执行shell脚本的情况,但有时可能又会因为某些环境或者权限等无法排查的原因调用失败,这时候就可以通过一个中间介质C来执行。尤其是在对某些项目代码(已经过广泛测试或需要访问特定设备)进行重写,Java恐怕有些力不从心,而Sun公司定义的JNI规范,规定了Java对本地方法的调用规则,这就大可不必废弃旧有代码。
以下将以一个实际例子展示Java通过JNI调用C打印“Hello World!”主要记录实现的过程和方法,对其中的一些原理和规范不做具体展开。想深入了解的可以参考Oracle的官方文档,贴上地址:
JNI Interface Functions and Pointers
首先定义一个Java类JavaCallC.java,在类中实现一个SayHello方法,并用关键字native为本地方法编写本地声明;
public native void SayHello();
然后在类中的静态代码块显示地加载本地代码库;
static { System.loadLibrary("hello"); //加载本地共享库 }
再加上main方法和一些必要的异常处理程序,就生成以下源文件(当然,也可以将本地方法放在另外一个多带带的类中)。
package com.jni.c; public class JavaCallC { /** * java通过JNI调用C * @author xiaosong 2017-04-03 */ public static void main(String[] args) { JavaCallC call = new JavaCallC(); call.SayHello(); } /** * 加载共享库的本地方法 */ public native void SayHello(); static { try { System.loadLibrary("hello"); //加载本地共享库 }catch(UnsatisfiedLinkError e) { System.err.println("无法加载共享库:" + e.toString()); } } }
P.S. 如果没有使用IDE的,需先用 javac 将类编译为 .class 文件。
要为以上定义的类生成 Java 本地接口头文件,需使用 javah,Java 编译器的 javah 功能将根据 JavaCallC 类生成必要的声明,此命令将生成一个 .h 后缀的头文件,我们在共享库的代码中要包含它。在工程项目的编译文件 bin 目录(也可能是build)下执行如下命令():
javah -jni [package.class]
执行命令后生成了一个 com_jni_c_JavaCallC.h 头文件,头文件的内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include/* Header for class com_jni_c_JavaCallC */ #ifndef _Included_com_jni_c_JavaCallC #define _Included_com_jni_c_JavaCallC #ifdef __cplusplus extern "C" { #endif /* * Class: com_jni_c_JavaCallC * Method: SayHello * Signature: ()V */ JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
在与 com_jni_c_JavaCallC.h 相同的路径下创建一个 .c 文件 hello.c ,在C文件中引入该头文件 ,并使用和头文件中一致的方法来声明函数。内容如下:
记得要为 JNIEnv * 指针和 jobject 对象定义变量,习惯上将这两个变量定义为 env 和 obj 。
env 指针是任意一个本地方法的第一个参数,它指向一个函数指针表。jobject 指向在此 Java 代码中实例化的 Java 对象 LocalFunction 的一个句柄,相当于 this 指针。
#include "com_jni_c_JavaCallC.h" #includeJNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv * env, jobject obj) { printf("Hello World! "); return; }
编译文件时,需要告知 GCC 编译器在何处查找Java本地方法的支持文件 jni.h 和 jni_md.h ,这两个文件一般是在 ../jdk/include 和 ../jdk/include/linux 两个目录下(AIX在 ../jdk/include/aix 目录;Windows在 ../jdk/include/win32 目录),在我的环境中按如下过程编译。
先生成 hello.o :
gcc -fPIC -I/usr/lib/jdk1.8.0_111/include -I/usr/lib/jdk1.8.0_111/include/linux -c hello.c
再生成 libhello.so :
(共享库 .so 的文件名必须是 lib+文件名)
gcc -shared hello.o -o libhello.so
拷贝 libhello.so 到共享库目录:
(共享库目录一般为 ../jre/lib/amd64/server)
sudo cp libhello.so /usr/lib/jdk1.8.0_111/jre/lib/amd64/server
由于我未配置 $LD_LIBRARY_PATH 环境变量,所以程序无法加载到共享库 hello ,因而我改写成通过全路径的方式来加载共享库。
//System.loadLibrary("hello"); //加载本地共享库 System.load("/usr/lib/jdk1.8.0_111/jre/lib/amd64/server/libhello.so");
运行结果如下:
接下来看一下Java如何通过JNI向C传递参数。本文中仅以 String 字符串为例,其他类型的参数的处理可参考文首提供的Oracle官方文档,方法大体上是一致的。
先在声明的本地方法中定义参数:
public native void SayHello(String strName1, String strName2);
然后在 main 方法中调用它并传递参数:
public static void main(String[] args) { JavaCallC call = new JavaCallC(); call.SayHello("Info", "Xiaosong"); }
生成头文件的方法同上,这时候看一下生成的头文件有何区别。
/* DO NOT EDIT THIS FILE - it is machine generated */ #include/* Header for class com_jni_c_JavaCallC */ #ifndef _Included_com_jni_c_JavaCallC #define _Included_com_jni_c_JavaCallC #ifdef __cplusplus extern "C" { #endif /* * Class: com_jni_c_JavaCallC * Method: SayHello * Signature: (Ljava/lang/String;Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv *, jobject, jstring, jstring); #ifdef __cplusplus } #endif #endif
可以看到函数声明里多了两个 jstring ,这就对应于我们要传递的两个 String 参数。
其他数值型参数和数组型参数对照如下:
同样地,在编写 hello.c 文件时,我们需要为传递的两个参数定义变量;
JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv * env, jobject obj, jstring instring1, jstring instring2)
对于字符串型参数,因为在本地代码中不能直接读取 Java 字符串,而必须将其转换为 C /C++ 字符串或 Unicode。此处C的写法和C++的写法略微不同;
/** * C 写法 */ //从instring字符串取得指向字符串UTF编码的指针; const char *info = (*env)->GetStringUTFChars(env, instring1, 0);
/** * C++ 写法 */ const char *info = env->GetStringUTFChars(instring1, 0);
//通知虚拟机本地代码不再需要通过 info 访问Java字符串;
/** * C 写法 */ (*env)->ReleaseStringUTFChars(env, instring1, info);
/** * C++ 写法 */ env->ReleaseStringUTFChars(instring1, info);
再加上一些简单的异常处理,完整的含参的 hello.c 如下:
#include "com_jni_c_JavaCallC.h" #include#include JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv * env, jobject arg, jstring instring1, jstring instring2) { //从instring字符串取得指向字符串UTF编码的指针 const char *info = (*env)->GetStringUTFChars(env, instring1, 0); const char *name = (*env)->GetStringUTFChars(env, instring2, 0); if (strlen(info)==0 || strlen(name)==0) { printf("参数缺失! "); }else { printf("%s : Hello %s ", info, name); }; //通知虚拟机本地代码不再需要通过str访问java字符串 (*env)->ReleaseStringUTFChars(env, s1, str); (*env)->ReleaseStringUTFChars(env, s2, user); return; }
方法和操作同上
以下是调用 call.SayHello("Information", "Xiaosong"); 执行的结果:
以下是调用 call.SayHello("Information", ""); 执行的结果:
至此,Java通过JNI调C的例子全部结束,当中如有什么不足或错误还请指正。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/66945.html
摘要:我们知道,发起函数调用,需要构造一个栈帧。构造栈帧的具体实现细节的选择,被称为调用惯例。要想完成这个函数调用逻辑,就要运行时构造栈帧,生成参数压栈和清理堆栈的工作。目前,几乎支持全部常见的架构。 原文:http://nullwy.me/2018/01/java...如果觉得我的文章对你有用,请随意赞赏 遇到的问题 前段时间开发的时候,遇到一个问题,就是如何用 Java 实现 chdir...
摘要:拆解虚拟机的基本步聚如下首先,要等待到自身成为唯一一个正在运行的非守护线程时,在整个等待过程中,虚拟机仍旧是可工作的。将相应的事件发送给,禁用,并终止信号线程。 本文简单介绍HotSpot虚拟机运行时子系统,内容来自不同的版本,因此可能会与最新版本之间(当前为JDK12)存在一些误差。 1.命令行参数处理HotSpot虚拟机中有大量的可影响性能的命令行属性,可根据他们的消费者进行简...
摘要:换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。参考文章珠峰架构课墙裂推荐细说异步函数发展历程异步编程谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和,您的肯定是我前进的最大动力。知其然知其所以然,首先了解三个概念: 1.什么是同步? 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了...
摘要:参考文章珠峰架构课墙裂推荐细说异步函数发展历程异步编程谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和,您的肯定是我前进的最大动力。 知其然知其所以然,首先了解三个概念: 1.什么是同步? 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。此调...
阅读 2564·2021-10-11 10:58
阅读 1148·2021-09-29 09:34
阅读 1485·2021-09-26 09:46
阅读 3830·2021-09-22 15:31
阅读 730·2019-08-30 15:54
阅读 1458·2019-08-30 13:20
阅读 1251·2019-08-30 13:13
阅读 1486·2019-08-26 13:52