前言
我们知道Java是一种编译解释型语言,编译后得到.class文件再经由jvm解释执行,尽管jvm现在已经相当高效,但和C/C++这种可以直接编译为可执行文件的语言相比效率还是略逊一筹。为了让Java更加高效也为了跨平台能力,JNI(Java Native Interface)应运而生。
JNI简单来说就是让Java方法中可以调用C函数,C函数也可以调用Java。这样做的好处有很多:在计算密集型场景适当使用jni可以显著提高效率;需要直接操作内存时JNI更加合适,例如Object
类中的clone()
方法;本地链接库不易破解,核心算法和一些加/解密操作可以放在本地方法中。
下面我会写一个小示例,用到的工具和环境如下:
- windows系统
- jdk环境
- IntelliJ IDEA(Java IDE)
- Clion(C IDE,一般来说用Visual Studio的多一些,但是我比较喜欢jetbrains的风格)
- MinGW(Win下的编译器,Clion需要靠这个来编译)
1. 在Java文件定义本地方法并生成头文件
package com.leqi.example;
public class Hello {
public static native String helloFromNative();
public static void main(String[] args) {
System.out.println(helloFromNative());
}
}
IDEA新建一个类Hello,定义一个native方法helloFromNative()
,方法返回一个String
类。
如果你是Kotlin爱好者,可以这么写,新建Koltin File(名字随便起):
@file:JvmName("Hello")
package com.leqi.example
external fun helloFromNative(): String
fun main() {
System.load("C:\\Users\\LG\\Desktop\\libNativeHello.dll")
println(helloFromNative())
}
Kotlin和Java并无本质上的区别,只是要注意Kotlin File编译后文件名会发生改变,只需要指定JvmName用起来就差不多了。
点击IDEA的绿色小锤子编译项目,然后在这个目录下找到编译后的.class文件。
随后打开命令行工具,cd到项目文件夹名\out\production\项目文件夹名
下,输入命令javah com.leqi.example.Hello
,不指定输出目录的话头文件会自动生成在当前目录下。
如果jdk版本高,javah
命令被移除的话可以用javac -h
命令代替。用命令行生成只是为了省事,后期熟练了自己手写头文件也是没关系的。
2. 编译动态链接库
打开Clion新建C++Library项目,Library type选择shared。
第一次新建项目时如果没有配置编译工具的话点击file - setting中,找到Toolchains然后添加MinGW,把MinGW的根目录选中即可。
接下来把上一步得到的头文件copy到项目中,再找到jdk安装目录,在include目录下找到jni.h文件,include/win32目录下找到jni_md.h,这两个文件也copy到项目中。
新建Hello.cpp,代码如下:
#include "com_leqi_example_Hello.h"
#include "string"
extern "C" {
JNIEXPORT jstring
JNICALL Java_com_leqi_example_Hello_helloFromNative
(JNIEnv *env, jclass) {
std::string hello = "Hello World from C++";
return env->NewStringUTF(hello.c_str());
}
}
extern "C"
很重要,它的功能是让编译器以处理 C 语言代码的方式处理C++代码,否则jni调用时会出现问题。
JNI函数命名是有一些规则的,并不是我们想怎么写就怎么写,首先是JNIEXPORT
表明这还是个可被外部调用的函数。
接下来是函数的返回值jstring
,这个东西可以展开讲讲,我们知道Java和C是两套东西,它们的基本数据类型是不可以直接互相调用的,所以jni头文件中就定义了各种各样的映射,我在别人博客盗了个表格:
Java类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型e |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
JNICALL
表明这个函数是个jni函数,jni函数名的命名也是有规则的,Java_Java包名_Java类名_native函数名
,函数都会有两个固定的参数JNIEnv *env, jclass
是由jvm虚拟机传入的,env
指针提供了大量的访问java变量和方法的函数,后面会常用到。
接下来修改CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.17)
project(NativeHello)
set(CMAKE_CXX_STANDARD 14)
add_library(NativeHello SHARED com_leqi_example_Hello.h Hello.cpp)
其实CMakeLists也很容易理解,主要把你要编译的文件名写在add_library
中的SHARED
后面即可。
最后点击绿色的小锤子编译一下项目,然后可以看到libNativeHello.dll文件,这个就是我们要的C函数库,现在把它copy到桌面或者容易找的地方。
3. Java中调用C函数库
回到IDEA,在Hello.java中略作修改:
package com.leqi.example;
public class Hello {
public static native String helloFromNative();
public static void main(String[] args) {
System.load("刚才编译好的.dll文件的绝对路径");
System.out.println(helloFromNative());
}
}
运行看一下结果
OK,大功告成,接下来我会写一篇C和Java之间传递复杂数据类型的文章。