tl;dr 使用Gradle来自动生成JNI的cpp header文件。并使用 ./gradlew run 来执行用JNI实现的hello world方法。
前言
最近在学习Java的 ConcurrentHashMap, 看到 Java 1.8 的 ConcurrentHashMap 的实现会使用 Compare And Swap 来实现并发安全性。在看 Java AtomicInteger (使用CAS来实现并发)中使用的 Unsafe 中,就发现了 native 方法。于是学习一下有关native的东西。
什么是 Java native
一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。
用户可以自己声明public或者private的native方法,并自己写出他们的C/C++实现,并在你的程序中调用他。例如:
public native void helloWorldPublic();
private native void helloWorldPrivate();
关于native方法的详解,请参考这篇知乎。
JNI简介
关于什么是JNI,可以参考一下这篇文章。写的十分详细,在此我就不赘述了。本文算是对上文的一个补充,记录一下上文没有说清的点,并实现一个自动生成,以及调用JNI的 hello world 程序。
简单的说,JNI就是允许你的Java程序去调用一段非Java代码写成的程序(一般是C/C++),本文会以C++为例。
JNI生成步骤 (MacOS Catalina 10.15.7 + Java11 + Gradle 6.8)
编写带有native声明的方法的java类,编写.java文件
使用javac命令编译所编写的java类,使用-h
如果你使用的Java版本是Java SE 9及以下的版本,你也可以使用javah 命令来生成.h头文件的。javah在 JDK10的时候已经被移除了。所推荐的替代品是javac -h
使用C/C++(或者其他编程想语言)实现本地方法,创建.h文件的实现,也就是创建.cpp文件实现.h文件中的方法
将C/C++编写的文件生成动态连接库,生成.dylib文件
生成的动态连接库在不同平台上的后缀不同。在Java中,我们有两个方法去载入动态连接库:System.load() 或者 System.loadLibrary()。当你使用System.loadLibrary() 头载入动态连接库的时候,不同的平台会把lib_name转化成JNI_LIB_PREFIX + lib_name + JNI_LIB_SUFFIX的文件名,然后在java.library.path下面去搜索相对应的文件名。
Windows:JNI_LIB_PREFIX = "", JNI_LIB_SUFFIX = ".dll", "hello" -> "hello.dll"
Linux:JNI_LIB_PREFIX = "", JNI_LIB_SUFFIX = ".dylib", "hello" -> "libhello.so"
Mac:JNI_LIB_PREFIX = "", JNI_LIB_SUFFIX = ".so", "hello" -> "libhello.dylib"
在java文件中load生成的library,然后执行java程序
JNI实践
样例代码:https://github.com/attix-zhang/jni-hello-world
在本文的例子中,将使用Gradle 6.8来自动化JNI的实现。直接使用gradle init命令来初始化一个project:
➜ jni-hello-world git:(main) gradle init
Select type of project to generate:
1: basic
2: application
3: library
4: Gradle plugin
Enter selection (default: basic) [1..4] 2
Select implementation language:
1: C++
2: Groovy
3: Java
4: Kotlin
5: Scala
6: Swift
Enter selection (default: Java) [1..6] 3
Split functionality across multiple subprojects?:
1: no - only one application project
2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1
Select build script DSL:
1: Groovy
2: Kotlin
Enter selection (default: Groovy) [1..2] 1
Select test framework:
1: JUnit 4
2: TestNG
3: Spock
4: JUnit Jupiter
Enter selection (default: JUnit 4) [1..4] 1
Project name (default: jni-hello-world):
Source package (default: jni.hello.world):
> Task :init
Get more help with your project: https://docs.gradle.org/6.8/samples/sample_building_java_applications.html
BUILD SUCCESSFUL in 36s
2 actionable tasks: 2 executed
在初始化之后的项目中,我们不需要额外的dependencies已经repository,所以,我们可以把app/build.gradle中相对应的代码删除。删除之后的build.gradle 文件:
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java application project to get you started.
* For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
* User Manual available at https://docs.gradle.org/6.8/userguide/building_java_projects.html
*/
plugins {
// Apply the application plugin to add support for building a CLI application in Java.
id 'application'
}
application {
// Define the main class for the application.
mainClass = 'jni.hello.world.App'
}
编写带有native声明的方法的java类,编写.java文件
编辑app/src/main/java/jni/hello/world/App.java 文件,在其中声明native的方法:
/*
* This Java source file was generated by the Gradle 'init' task.
*/
package jni.hello.world;
public class App {
public native void helloWorldPublic();
private native void helloWorldPrivate();
static{
System.loadLibrary("HelloWorldImpl");
//System.load(System.getProperty("user.dir") + "/libHelloWorldImpl.dylib");
}
public static void main(String[] args){
System.out.println(System.getProperty("java.library.path"));
final App helloWorld = new App();
helloWorld.helloWorldPublic();
helloWorld.helloWorldPrivate();
}
}
在这里,我们声明了两种native的方法,一个public,以及一个private。并且在static的block中,调用System.loadLibrary/System.load将未来会生成的动态连接库装载进来。在main方法中,我们调用了两种native方法来验证我们生成的JNI library可以正常工作。在这里,还有一行代码是打印 System property java.library.path的,为什么多此一举,我们下面再解释。
生成JNI方法的C/C++的头文件
使用javac -h方法
如果要使用命令行的话,我们需要执行以下CLI command在compile Java classes的同时,生成.h头文件:
➜ jni-hello-world git:(main) javac app/src/main/java/jni/hello/world/App.java -d app/build/classes -h app/build/tmp/generateJniHeaders
那么用Gradle的方法,就是在app/build.gradle文件中添加下列代码:
def generateJniWorkingDir = file("${buildDir}/tmp/generateJniHeaders")
compileJava {
// Using 'javac -h
options.compilerArgs << "-h" << "${generateJniWorkingDir}"
}
使用javah方法
javah需要的Java版本是Java SE 9及以下。如果要使用javah的话,我们需要先试用javac来生成对应的class文件,然后使用javah来读取class文件去生成头文件:
➜ jni-hello-world git:(main) mkdir -p app/build/classes
➜ jni-hello-world git:(main) JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home javac app/src/main/java/jni/hello/world/App.java -d app/build/classes
➜ jni-hello-world git:(main) mkdir -p app/build/tmp/generateJniHeaders
➜ jni-hello-world git:(main) JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home javah -classpath app/build/classes -d app/build/tmp/generateJniHeaders -jni jni.hello.world.App
如果要使用Gradle来自动化这个过程就是:
def generateJniWorkingDir = file("${buildDir}/tmp/generateJniHeaders")
// Using javah to generate header file. In Java 11, we need to use `javac -h ` to generate header file
task generateJniHeaders(type: Exec) {
generateJniWorkingDir.mkdirs()
workingDir generateJniWorkingDir
def classpath = sourceSets.main.java.classesDirectory
commandLine "javah", "-classpath", classpath.get(), "-jni", "jni.hello.world.App"
dependsOn compileJava
}
我们需要定义一个新的task:generateJniHeaders. 这个task需要依赖于compileJavatask。
当我们通过上述方法去自动化生成动态连接库的头文件之后,我们可以看到在app/build/tmp/generateJniHeaders/jni_hello_world_App.h的位置生成了下面这个文件:
➜ jni-hello-world git:(main) cat app/build/tmp/generateJniHeaders/jni_hello_world_App.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class jni_hello_world_App */
#ifndef _Included_jni_hello_world_App
#define _Included_jni_hello_world_App
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jni_hello_world_App
* Method: helloWorldPublic
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_jni_hello_world_App_helloWorldPublic
(JNIEnv *, jobject);
/*
* Class: jni_hello_world_App
* Method: helloWorldPrivate
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_jni_hello_world_App_helloWorldPrivate
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
使用C/C++实现本地方法,创建.h文件的实现,也就是创建.cpp文件实现.h文件中的方法
我们创建app/jni_hello_world_App.cpp文件:
➜ jni-hello-world git:(main) cat app/jni_hello_world_App.cpp
#include "jni_hello_world_App.h"
#include
JNIEXPORT void JNICALL Java_jni_hello_world_App_helloWorldPublic(JNIEnv *env,jobject obj) {
printf("[Public]: Hello World!\n");
return;
}
JNIEXPORT void JNICALL Java_jni_hello_world_App_helloWorldPrivate(JNIEnv *env,jobject obj) {
printf("[Private]: Hello World!\n");
return;
}
将C/C++编写的文件生成动态连接库,生成.dylib文件
接下来,我们需要把自动生成的.h文件,以及我们稍后创建的.cpp文件放到同一个文件夹下,使用gcc命令来生成动态连接库:
➜ jni-hello-world git:(main) mkdir -p app/build/tmp/gccCompileDir
➜ jni-hello-world git:(main) cd app/build/tmp/gccCompileDir
➜ gccCompileDir git:(main) cp ../../../jni_hello_world_App.cpp .
➜ gccCompileDir git:(main) cp ../../tmp/generateJniHeaders/jni_hello_world_App.h .
➜ gccCompileDir git:(main) ls
jni_hello_world_App.cpp jni_hello_world_App.h
➜ gccCompileDir git:(main) gcc -I"${JAVA_HOME}/include/" -I"${JAVA_HOME}/include/darwin/" -dynamiclib jni_hello_world_App.cpp -o libHelloWorldImpl.dylib
➜ gccCompileDir git:(main) ls
jni_hello_world_App.cpp jni_hello_world_App.h libHelloWorldImpl.dylib
因为在我们生成的.h头文件中会#include , jni.h 头文件在{JAVA_HOME}/include/的位置,而在MacOS中的jni.h的头文件会#include 文件,这个文件在${JAVA_HOME}/include/darwin/. 所以我们需要在gcc的命令中把这两个文件夹加入到搜索位置中。
如果要使用Gradle来自动化这个过程就是:
def gccCompileDir = file("${buildDir}/tmp/gccCompileDir");
task copyHeaderAndCppFiles(type: Copy) {
from "${generateJniWorkingDir}/jni_hello_world_App.h", "${projectDir}/jni_hello_world_App.cpp"
into gccCompileDir
dependsOn compileJava
}
task generateJniLib(type: Exec) {
workingDir gccCompileDir
def javaHome = "${System.env.JAVA_HOME}"
commandLine "gcc",
"-I${javaHome}/include/", "-I${javaHome}/include/darwin/",
"-dynamiclib", "jni_hello_world_App.cpp",
"-o", "libHelloWorldImpl.dylib"
dependsOn copyHeaderAndCppFiles
}
执行Java程序
现在我们已经在app/build/tmp/gccCompileDir/libHelloWorldImpl.dylib处生成了动态连接库,接下来就是让我们的Java程序可以找到并装载动态连接库。
在Java中,有两种方法来装载动态连接库:
System.load();, 在这种情况下,我们只需要把动态连接库的absolute path传递过去就好了,我们可以选择在app/build/tmp/gccCompileDir文件夹下执行Java程序,这样我们就可以通过System.getProperty("user.dir") + "/libHelloWorldImpl.dylib"来得到动态连接库的绝对位置,从而成功装载。
System.loadLibrary(); 在这种方式下,Java会在java.library.path中搜索所需的动态连接库,于是,我之前在程序开始打印出了这个System Property: /Users/zhangzhen/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
可以看到,最后一个搜索位置就是当前文件夹,那么如果我们在app/build/tmp/gccCompileDir文件夹下执行Java程序,就可以直接装载这个动态连接库。
那么以Gradle的方式来执行Java程序就是:
run {
workingDir gccCompileDir
dependsOn generateJniLib
}
最终的执行结果就是:
➜ jni-hello-world git:(main) ./gradlew run
> Task :app:run
/Users/zhangzhen/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
[Public]: Hello World!
[Private]: Hello World!
BUILD SUCCESSFUL in 1s
4 actionable tasks: 2 executed, 2 up-to-date