前言

在 java 中可以在方法中指定关键字 native 使得java可以调用本地代码中得方法,如c\cpp。

本文描述了如何简单的跑通了在java中调用本地的一个cpp方法的过程。

开始

工具

  1. IntelliJ IDEA 2022.3.2 编写java代码
  2. CLion 2023.2.2 编写cpp代码

java 代码

创建一个 main 文件

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
System.out.println(sum(114,514));
}

public static native int sum(int a,int b);
}

其中使用native 关键字,表示该方法为本地方法

构建该文件, 以下是文件树结构

1
2
3
4
5
6
7
├─out
│ └─production
│ └─javaNative
│ Main.class

└─src
Main.java

生成该本地方法的头文件

在项目的根目录的powershell或者cmd中使用以下命令:

1
javah -classpath out/production/javaNative -d ./jni Main

指令解释:

  1. javah: 表示要运行javah命令。javah命令用于生成Java类的本地方法接口(Native Method Interface,简称JNI)头文件。它可以根据Java类中声明的本地方法生成对应的C/C++头文件,以便在本地代码中实现这些方法。
  2. -classpath out/production/javaNative: 指定编译后的Java类文件所在的类路径。这里编译后的类文件位于out/production/javaNative目录中。
  3. -d ./jni: 指定生成的JNI头文件的输出目录。这里将生成的头文件放在当前目录下的jni目录中。
  4. Main: 指定要生成JNI头文件的Java类名。这里生成的JNI头文件将与名为Main的Java类相关联。

之后就能在 /jni 文件夹下看到 Main.h 的头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Main */

#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Main
* Method: sum
* Signature: (II)I <--(II)表明传入两个Int类型参数,I表明返回一个Int类型结果
*/
JNIEXPORT jint JNICALL Java_Main_sum //<-- 为要求实现的方法
(JNIEnv *, jclass, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

cpp 代码

使用Clion

  1. 创建一个项目

  2. 在项目中的 CMakeList.txt中引入方才java编译使用的jdk的include目录和include/win32文件,例如:

    1
    2
    3
    include_directories("C:/Program Files/Eclipse Adoptium/jdk-8.0.382.5-hotspot/include")

    include_directories("C:/Program Files/Eclipse Adoptium/jdk-8.0.382.5-hotspot/include/win32")
  3. 将 /jni 文件夹下看到 Main.h 的头文件 加入项目的根目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    │  CMakeLists.txt
    │ main.cpp
    │ Main.h <---- 方在与main.cpp 同级别

    ├─.idea
    │ ...

    └─cmake-build-debug
    ...

编写cpp实现头文件中要求实现的方法

  1. 在 main.cpp 文件中写入:

    1
    2
    3
    4
    5
    6
    #include "Main.h"
    JNIEXPORT jint JNICALL Java_Main_sum
    (JNIEnv *, jclass, jint a, jint b){ //仅仅使用了后边的jint

    return a+b;
    }

    解释:

    1. JNIEXPORT jint JNICALL: 这是一个用于指定函数导出的宏定义。它告诉编译器将这个函数作为动态链接库的导出函数。JNIEXPORT是一个宏,用于指定函数的导出类型。jint表示函数的返回类型是int。
    2. Java_Main_sum: 这是一个约定的命名规则,用于表示这个JNI函数与Java类Main中的sum方法相对应。在JNI中,函数名的命名规则是Java_<类名>_<方法名>。
    3. (JNIEnv *, jclass, jint, jint): 这是函数的参数列表。在JNI中,函数的前两个参数分别是JNIEnv和jclass类型的指针,用于与Java虚拟机进行交互。后面的两个参数是jint类型的整数参数。
  2. jni中的类型与Java的基本类型相似,仅仅需要在java的基本类型前加入j;例如Java中Boolean,在jni中则为jboolean;Byte<–>jbyte 。。。

编译生成动态库

在项目的根目录的powershell或者cmd中使用以下命令:

1
gcc main.cpp -I "C:/Program Files/Eclipse Adoptium/jdk-8.0.382.5-hotspot/include" -I "C:/Program Files/Eclipse Adoptium/jdk-8.0.382.5-hotspot/include/win32" -shared -o main.dll -lstdc++

解释:

  1. gcc main.cpp 使用gcc编译main.cpp文件
  2. -I “…” 引入文件
  3. -shared 指定生成动态链接库
  4. -o main.dll 指定生成文件名字
  5. -lstdc++ 链接windows上的cpp标准库

最后就可以在项目的更目录到 main.dll 文件了

在Java中使用该dll文件

  1. 将生成的main.dll文件放在方才的java项目的更目录
  2. 在java Main.java 中加载该.dll文件,使用以下代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Main {

    static {
    // 使用 System.getProperty("user.dir") 获取项目的路径
    System.load(System.getProperty("user.dir") + "/mian.dll");
    }
    public static void main(String[] args) {
    System.out.println(sum(114,514));
    }

    public static native int sum(int a,int b);
    }
  3. 编译运行得到结果:
    1
    2
    3
    628

    进程已结束,退出代码0

后话

尚不清楚 JNIEnv *, jclass 的作用,留有之后探究。