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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容