Step By Step_Java通过JNI调C程序执行

文章为本人编纂,转载请联系作者并注明出处。

在日常项目中,我们可能会遇到需要用Java去命令行执行命令或执行shell脚本的情况,但有时可能又会因为某些环境或者权限等无法排查的原因调用失败,这时候就可以通过一个中间介质C来执行。尤其是在对某些项目代码(已经过广泛测试或需要访问特定设备)进行重写,Java恐怕有些力不从心,而Sun公司定义的JNI规范,规定了Java对本地方法的调用规则,这就大可不必废弃旧有代码。

以下将以一个实际例子展示Java通过JNI调用C打印“Hello World!”主要记录实现的过程和方法,对其中的一些原理和规范不做具体展开。想深入了解的可以参考Oracle的官方文档,贴上地址:
JNI Interface Functions and Pointers


环境介绍

操作系统:Ubuntu Gnome 16.04 LTS
Ubuntu Gnome 16.04 LTS
Java:Java 1.8.0_111
Java version
C:gcc version 5.4.0
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 对象定义变量,习惯上将这两个变量定义为 envobj
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.hjni_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
hello.o
  • 再生成 libhello.so
    (共享库 .so 的文件名必须是 lib+文件名
gcc -shared hello.o -o libhello.so
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的例子全部结束,当中如有什么不足或错误还请指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,388评论 25 707
  • 近大半年来房价上涨得厉害,很多刚需都在想,这房价突然一下涨了30%,现在在买是不是当了接盘侠?是不是划得来之类的,...
    弹吉他的诗人阅读 418评论 2 1
  • (一) 少年翩翩起舞 酒醉后放肆的摇摆 你是看透了生活的无奈 还是否定自己存在 无妨 醒来后你就回归正常 茫茫人海...
    三月十九阅读 191评论 0 1
  • 午时饭后,闲坐无事,手头也无可以翻阅的报刊杂志,百无聊赖,偶然想起最近六年数次翻起的《黄金时代》,很多唏嘘,不胜感...
    赵等于召阅读 264评论 1 2