使用JNI技术实现Java和C/C++互调(Java调C/C++)

    Java作为一个技术生态平台在业界得到了广泛的应用,许许多多的大型项目都是搭载在其之上。Java语言本身功能也十分强大,能实现大量的业务逻辑算法。那么,我们为什么要让Java和C/C++互调呢,直接用Java写不就完了吗?这是初学者一个很常见的疑问,下面我们来看看为什么需要这样做。

    用Java调用底层的代码(不一定指是C/C++写的,虽然大部分是)主要原因有四个:

    1.运行效率:对于很多的科学计算,信号处理,多媒体计算,计算的数据量非常大,计算的次数非常多,属于计算密集型程序,对于这类程序,在JVM上通过字节码运行,效率远远不及直接以二进制码在CPU上运行,因为很多编译优化在JVM这一层无法做出,JVM的栈式体系结构也不适合做这类优化,再者,一般这类计算会较多涉及浮点数,所以二进制码可以直接在硬件层优化。

    2.系统接口:JVM提供了大量访问操作系统或者硬件的接口,然而,它也不是十全十美的,毕竟每台计算机之间系统差异巨大,不同的操作系统直接很难做到完全同意,更甚者,如果在系统中新加入了一个设备和其驱动程序,当我们想在Java中调用它时,用纯Java肯定是不可能的。

    3.代码复用:也需当我们要实现某些功能时发现已经有别人写好的库提供类似的功能,有或者我们在其他平台上开发了一套系统或者引擎,要把他放到Java平台上,无奈他们是以库的形式或者C/C++项目的形式存在,这事就只能用JNI来调用,不然对于前者,我们要再实现一份相同的Java版代码,对于后者,我们不仅要在实现,日后还要面临同时维护两个版本的库的窘境。

   4.程序安全:总所周知,Java代码因为编译后包含大量源代码信息,所以反编译十分容易,相对的,C/C++代码因为是直接编译成机器二进制码,只能精确到反汇编,加上各种优化,反编译更是难上加难,所以C/C++代码更不容易被破解。

    当然,不是说只有以上问题,只是这四个问题是最主要的,对于现在Android开发盛行的时代尤为如此。适度学习JNI还是有必要的。在Android中,可能更倾向于使用NDK,NDK相当于一个集成环境,能够通过各种配置自动帮助用户编译链接动态库,自动生成代码,打包进apk,并且能够像编写Makefile一样编写脚本管理项目。在此不做赘述。我们还是关注JNI的使用,当我们熟悉JNI的使用时,NDK自然也能够很快掌握。

    首先我们来看Java调用C/C++:

    1.编写Java代码,在代码中使用native关键字标明该方法是调用本地库,不需要实现。

    2.使用javah工具,生成对应的头文件,此步骤主要是规范化本地代码,总所周知,Java是平台无关的,但是C/C++是平台相关的,所以需要规范本地代码,使其表示的值与Java中的值意义相同。

    3.实现头文件,并把其编译链接成动态链接库  。

    4.在Java代码中添加加载动态链接库的代码,并将其编译为class文件。

    5.运行并测试效果。

    下面我们来看一个示例,首先是Java代码:

public class Hello {

	static {
		System.loadLibrary("hello");
	}

	public native static void hello(String s);
	
	public static void main(String[] args) {
		hello("hello");
		for(String s : args) hello(s);
	}
}

    这段代码按照上述所说,声明了一个native方法,并在加载类时加载了一个名为hello的动态链接库。接下来执行javah命令,生成了如下的头文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class Hello */

#ifndef _Included_Hello
#define _Included_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Hello
 * Method:    hello
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_Hello_hello
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif
  
    这里需要注意一下,最上面的预处理命令include ,这条明会导致编译器取默认的目录去寻找头文件,但是一般头文件jni.h是放在java对应的include中,有很多方法可以解决这个问题,最简单粗暴的办法是把对应javac的jni.h和jni_md.h拷贝到当前文件夹,然后把头文件那一行改成#include "jni.h"。但是这个方法问题也很大,一是这样做很麻烦,对每个头文件都要改,而是依赖于当前编译环境,如果未来Java编译器或者操作系统升级了这个头文件可能会失效。第二个方法是把jni.h和jni_md.h这两个文件放到/usr/include或者/usr/local/include中,这样就不用修改生成的头文件,但是第二个问题还没解决。所以建议的方法是在Makefile中加入关于头文件的路径信息(为了可扩展性,这个路径可以是另一个环境变量)并在编译命令中以-I来指定路径。

    接下来我们来实现以下这个头文件的函数:

#include 
#include"Hello.h"
#include
using namespace std;
JNIEXPORT void JNICALL Java_Hello_hello (JNIEnv* env, jclass jc, jstring s)
{
	const char* str = env->GetStringUTFChars(s, 0);
	cout << str << endl;
	env->ReleaseStringUTFChars(s, str);
}
    可以看出,这个函数就是打印出Java传进来的字符串,关于这块我们等下次在C/C++调Java中说明。

    接下来我们就可以编译这个文件了,在Linux下用-fPIC -shared来编译动态链接库,关于这点我有一篇博客是写关于编译参数的。在Windows中有很多集成环境能够生产高质量的动态链接库,在此不多说。

    还有一点要注意,Linux下编译后的名字格式为lib[库名].so,这是linux加载器处理库名的格式,也算是一种约定。像之前我们在Java里写的hello这个库在这里就应该编译成libhello.so。

    写下来用javac来编译Java文件,最后调用就可以看到输出了hello和命令行传入的参数。至此,Java调用本地库的过程就结束,可以看出,重点是在于C/C++代码的编写,Java只是负责调用,限于篇幅,下次我们再来看C/C++是怎么获取Java中的对象的信息以及怎么调用Java的方法。

你可能感兴趣的:(C++,Java)