文章为本人编纂,转载请联系作者并注明出处。
在日常项目中,我们可能会遇到需要用Java去命令行执行命令或执行shell脚本的情况,但有时可能又会因为某些环境或者权限等无法排查的原因调用失败,这时候就可以通过一个中间介质C来执行。尤其是在对某些项目代码(已经过广泛测试或需要访问特定设备)进行重写,Java恐怕有些力不从心,而Sun公司定义的JNI规范,规定了Java对本地方法的调用规则,这就大可不必废弃旧有代码。
以下将以一个实际例子展示Java通过JNI调用C打印“Hello World!”主要记录实现的过程和方法,对其中的一些原理和规范不做具体展开。想深入了解的可以参考Oracle的官方文档,贴上地址:
JNI Interface Functions and Pointers
环境介绍
操作系统:Ubuntu Gnome 16.04 LTS
Java:Java 1.8.0_111
C:gcc version 5.4.0
实现步骤
Hello World
1、定义一个Java类——JavaCallC.java
首先定义一个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());
}
}
}
2、生成 Java 本地接口头文件
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 <jni.h>
/* 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
3、创建共享库C文件
在与 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"
#include <stdio.h>
JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello
(JNIEnv * env, jobject obj) {
printf("Hello World! \n");
return;
}
4、编译生成共享库文件
编译文件时,需要告知 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
5、运行Java程序
由于我未配置 $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官方文档,方法大体上是一致的。
1、定义本地方法参数
先在声明的本地方法中定义参数:
public native void SayHello(String strName1, String strName2);
然后在 main
方法中调用它并传递参数:
public static void main(String[] args) {
JavaCallC call = new JavaCallC();
call.SayHello("Info", "Xiaosong");
}
2、编译并生成头文件
生成头文件的方法同上,这时候看一下生成的头文件有何区别。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* 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
参数。
其他数值型参数和数组型参数对照如下:
3、创建共享库C文件
同样地,在编写 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 <stdio.h>
#include <string.h>
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("参数缺失!\n");
}else {
printf("%s : Hello %s \n", info, name);
};
//通知虚拟机本地代码不再需要通过str访问java字符串
(*env)->ReleaseStringUTFChars(env, s1, str);
(*env)->ReleaseStringUTFChars(env, s2, user);
return;
}
4、编译生成共享库文件
方法和操作同上
5、运行Java程序
以下是调用 call.SayHello("Information", "Xiaosong");
执行的结果:
以下是调用 call.SayHello("Information", "");
执行的结果:
至此,Java通过JNI调C的例子全部结束,当中如有什么不足或错误还请指正。