在密码学领域搞开发,无论是看密码算法的文献资料,还是调用业界著名的密码函数库、大数库、数学库等,都免不了要和C语言打交道(没办法,谁叫C语言是前辈呢)。为了避免重复编程或在核心运算部分提高性能,对于一些基础的算法,我们完全可以利用现有的C函数库来帮忙。这就涉及在Java中调用C函数的问题。幸亏Java给我们提供了JNI(Java Native Interface,Java原生接口)机制,使得在Java中调用C函数易如反掌。
JNI是Java语言的本地编程接口。在Java程序中,我们可以通过JNI实现一些用Java语言不便实现的功能,具体如下:
1)标准的Java类库没有提供应用程序所需要的功能,通常这些功能是与平台相关的(只能由其他语言编写)。
2)希望使用一些已经有的类库或者应用程序,而它们并不是用Java语言编写的。
3)程序的某些部分对速度要求比较苛刻,选择用汇编或者C语言来实现并在Java语言中调用它们。
4)为了应用的安全性,会将一些复杂的逻辑和算法通过本地代码(C或C++)来实现,本地代码比字节码难以破解。
Java可以通过JNI调用C/C++的库,这对于那些对性能要求比较高的Java程序或者Java无法处理的任务无疑是一个很好的方式。Java中的JNI开发流程主要分为以下6步:
步骤01 编写声明native方法的Java类。
步骤02 将Java源代码编译成Class字节码文件。
步骤03 用javah -jni命令生成.h头文件(javah是JDK自带的一个命令,-jni参数表示将Class中用native声明的函数生成JNI规则的函数)。
步骤04 用本地代码实现.h头文件中的函数。
步骤05 将本地代码编译成动态库(Windows下为*.dll,Linux/UNIX下为*.so,Mac OS X下为*.jnilib)。
步骤06 将动态库复制到java.library.path本地库搜索目录下,并运行Java程序。
下面我们通过实例来说明。
1)打开Eclipse,设置工作区路径,注意本例用编号作为工作区文件夹名(这是为了给读者演示多样性,所以本例没有用myws作为工作区名称)。新建一个Java工程,工程名是SimpleHello。在工程中新建一个类,在New Java Class对话框中,在Package框中输入包名com.study.jni.demo.simple,在Name框中输入类名SimpleHello,同时勾选public static void main(String []args]选项,如图2-44所示。
图2-44
这里简单解释一下package(程序包,简称为包),Java提供了程序包机制,用于区别类名的命名空间。程序包机制把功能相似或相关的类或接口组织在同一个包中,以方便类的查找和使用。如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名是不同的,不同包的类名可以相同,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免命名冲突。另外,包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。Java使用包这种机制是为了防止命名冲突,以及对类和接口进行分类,以便更好地组织和维护它们,提高可重用性。
设置好如图2-44所示的各项后,单击Finish按钮以关闭对话框。此时Eclipse会显示出SimpleHello.java的编辑窗口,在SimpleHello.java中输入如下代码:
package com.study.jni.demo.simple; public class SimpleHello { public static native String sayHello(String name); public static void main(String[] args) { String name = "Ljy"; String text = sayHello(name); System.out.println("after native, java shows:" + text); } static { System.loadLibrary("hello"); //hello.dll要放在系统路径下,比如c:\windows\ } }
在main函数中调用sayHello函数。注意sayHello()方法的声明,它有一个关键字native,表明这个方法是使用Java以外的语言实现的。该方法不包括在本程序业务功能的实现中,因为我们要用C/C++语言来实现它。注意System.loadLibrary("hello")这句代码,它是在静态初始化块中定义的,系统用来载入hello库,就是在后文所述要生成的hello.dll。
2)生成.h文件。打开命令行窗口,然后进入SimpleHello.java所在目录,这里是D:\eclipse-workspace\2.5\SimpleHello\src\com\study\jni\demo\simple\,引用了程序包,那么路径就变长了。输入如下命令:
javac SimpleHello.java -h .
注意,-h后面有一个空格,然后有一个黑点。选项-h表示需要生成JNI的头文件,h后面有个空格,然后加了黑点,黑点表示在当前目录下生成头文件,如果需要指定目录,可以把点改成文件夹名称(文件夹会自动新建)。该命令执行后,会在同一目录下生成两个文件:SimpleHello.class和com_study_jni_demo_simple_SimpleHello.h,后者就是我们所需的头文件,这个文件不要去修改,后面的VC工程中要用到。
3)编写本地实现代码。现在我们要用C/C++语言实现Java中定义的方法,其实就是新建一个DLL程序(DLL是指动态链接库)。
打开VC 2017,按Ctrl+Shift+N组合键打开“新建项目”对话框,然后在界面左侧选择“Windows桌面”,在右侧选择“Windows桌面向导”,输入工程名为“hello”,并设置工程存放的位置,如图2-45所示。
图2-45
单击“确定”按钮,随后出现“Windows桌面项目”对话框,选择“应用程序类型”为“动态链接库(.dll)”,并撤选“预编译标头”复选框,如图2-46所示。
图2-46
单击“确定”按钮,此时一个DLL工程就建立起来了。在VC解决方案中双击hello.cpp,然后在编辑框中输入如下代码:
#include "header.h" #include "jni.h" #include "stdio.h" #include "string.h" #include "com_study_jni_demo_simple_SimpleHello.h" JNIEXPORT jstring JNICALL Java_com_study_jni_demo_simple_SimpleHello_ sayHello(JNIEnv *env, jclass cls, jstring j_str) { const char *c_str = NULL; char buff[128] = { 0 }; jboolean isCopy; c_str = env->GetStringUTFChars(j_str, &isCopy); //生成native的char指针 if (c_str == NULL) { printf("out of memory.\n"); return NULL; } printf("From Java String:addr: %x string: %s len:%d isCopy:%d\n", c_str, c_str, strlen(c_str), isCopy); sprintf_s(buff, "hello %s", c_str); env->ReleaseStringUTFChars(j_str, c_str); return env->NewStringUTF(buff); //将C语言字符串转化为java字符串 }
这段代码很简单,主要就是把Java传来的参数打印出来。其中jni.h是JDK自带的文件,我们需要为工程添加JDK所在的路径,在VC菜单栏中选择“项目→属性→配置属性→C/C++”,然后在右边“平台”下选择“x64”(因为我们要生成64位的DLL),并在“附加包含目录”框中输入:%JAVA_HOME%\include; %JAVA_HOME%\include\win32,如图2-47所示。
图2-47
然后单击“确定”按钮关闭对话框,把D:\eclipse-workspace\2.5\SimpleHello\src\com\study\jni\demo\simple\下的com_study_jni_demo_simple_SimpleHello.h复制到VC工程目录下,并在VC中添加该头文件,在VC工具栏上选择解决方案平台为x64,如图2-48所示。
图2-48
按F7键生成解决方案,此时将在hello\x64\Debug下生成hello.dll,把该文件复制到C:\Windows下。至此,VC本地代码开发工作就完成了。
4)重新回到Eclipse中,按Ctrl+F11组合键来运行工程,随后就可以在下方控制台窗口中看到该程序的输出,如图2-49所示。
图2-49
我们看到不但打印了VC程序中的语句,也打印了VC函数的返回值(字符串:hello Ljy)。从这个例子可以验证Java向本地函数传参数,并成功获取了返回值。有人可能不喜欢在C:\Windows下添加文件,没关系,我们可以把hello.dll放到Java工程目录下或者任意一个目录下,只要在Java中指定绝对路径即可。比如我们把hello.dll放到D:盘下,则在Java程序中就要写成绝对路径:
System.load("d:/hello.dll"); //写绝对路径时,后缀名.dll也要写出来,要调用load函数