java dylib,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)

编写带有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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值