Java native关键字,JNI学习

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)

  1. 编写带有native声明的方法的java类,编写.java文件
  2. 使用javac命令编译所编写的java类,使用-h <dir>的选项,在指定文件夹下生成Java类名生成的C/C++的头文件
    • 如果你使用的Java版本是Java SE 9及以下的版本,你也可以使用javah 命令来生成.h头文件的。javah在 JDK10的时候已经被移除了。所推荐的替代品是javac -h <dir>
  3. 使用C/C++(或者其他编程想语言)实现本地方法,创建.h文件的实现,也就是创建.cpp文件实现.h文件中的方法
  4. 将C/C++编写的文件生成动态连接库,生成.dylib文件
    • 生成的动态连接库在不同平台上的后缀不同。在Java中,我们有两个方法去载入动态连接库:System.load(<absolute path to the dll>) 或者 System.loadLibrary(<Lib Name>)。当你使用System.loadLibrary(<Lib Name>) 头载入动态连接库的时候,不同的平台会把lib_name转化成JNI_LIB_PREFIX + lib_name + JNI_LIB_SUFFIX的文件名,然后在java.library.path下面去搜索相对应的文件名。
      • WindowsJNI_LIB_PREFIX = "", JNI_LIB_SUFFIX = ".dll", "hello" -> "hello.dll"
      • LinuxJNI_LIB_PREFIX = "", JNI_LIB_SUFFIX = ".dylib", "hello" -> "libhello.so"
      • MacJNI_LIB_PREFIX = "", JNI_LIB_SUFFIX = ".so", "hello" -> "libhello.dylib"
  5. 在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 <dir>' to generate native header files
    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 <generate_header_dir>` 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 <jni.h>
/* 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 <stdio.h>

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>jni.h 头文件在{JAVA_HOME}/include/的位置,而在MacOS中的jni.h的头文件会#include <jni_md.h>文件,这个文件在${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中,有两种方法来装载动态连接库:

  1. System.load(<path to dylib>);, 在这种情况下,我们只需要把动态连接库的absolute path传递过去就好了,我们可以选择在app/build/tmp/gccCompileDir文件夹下执行Java程序,这样我们就可以通过System.getProperty("user.dir") + "/libHelloWorldImpl.dylib"来得到动态连接库的绝对位置,从而成功装载。
  2. System.loadLibrary(<dylib name>); 在这种方式下,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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容