JNI开发:入门篇

前言

我们知道Java是一种编译解释型语言,编译后得到.class文件再经由jvm解释执行,尽管jvm现在已经相当高效,但和C/C++这种可以直接编译为可执行文件的语言相比效率还是略逊一筹。为了让Java更加高效也为了跨平台能力,JNI(Java Native Interface)应运而生。

JNI简单来说就是让Java方法中可以调用C函数,C函数也可以调用Java。这样做的好处有很多:在计算密集型场景适当使用jni可以显著提高效率;需要直接操作内存时JNI更加合适,例如Object类中的clone()方法;本地链接库不易破解,核心算法和一些加/解密操作可以放在本地方法中。

下面我会写一个小示例,用到的工具和环境如下:

  • windows系统
  • jdk环境
  • IntelliJ IDEA(Java IDE)
  • Clion(C IDE,一般来说用Visual Studio的多一些,但是我比较喜欢jetbrains的风格)
  • MinGW(Win下的编译器,Clion需要靠这个来编译)

1. 在Java文件定义本地方法并生成头文件

package com.leqi.example;

public class Hello {

    public static native String helloFromNative();

    public static void main(String[] args) {
        System.out.println(helloFromNative());
    }
}

IDEA新建一个类Hello,定义一个native方法helloFromNative(),方法返回一个String类。

如果你是Kotlin爱好者,可以这么写,新建Koltin File(名字随便起):

@file:JvmName("Hello")
package com.leqi.example

external fun helloFromNative(): String

fun main() {
    System.load("C:\\Users\\LG\\Desktop\\libNativeHello.dll")
    println(helloFromNative())
}

Kotlin和Java并无本质上的区别,只是要注意Kotlin File编译后文件名会发生改变,只需要指定JvmName用起来就差不多了。

点击IDEA的绿色小锤子编译项目,然后在这个目录下找到编译后的.class文件。

随后打开命令行工具,cd到项目文件夹名\out\production\项目文件夹名下,输入命令javah com.leqi.example.Hello,不指定输出目录的话头文件会自动生成在当前目录下。

如果jdk版本高,javah命令被移除的话可以用javac -h命令代替。用命令行生成只是为了省事,后期熟练了自己手写头文件也是没关系的。

2. 编译动态链接库

打开Clion新建C++Library项目,Library type选择shared。

第一次新建项目时如果没有配置编译工具的话点击file - setting中,找到Toolchains然后添加MinGW,把MinGW的根目录选中即可。

接下来把上一步得到的头文件copy到项目中,再找到jdk安装目录,在include目录下找到jni.h文件,include/win32目录下找到jni_md.h,这两个文件也copy到项目中。

新建Hello.cpp,代码如下:

#include "com_leqi_example_Hello.h"
#include "string"

extern "C" {

JNIEXPORT jstring
JNICALL Java_com_leqi_example_Hello_helloFromNative
        (JNIEnv *env, jclass) {
    std::string hello = "Hello World from C++";
    return env->NewStringUTF(hello.c_str());
}
}

extern "C"很重要,它的功能是让编译器以处理 C 语言代码的方式处理C++代码,否则jni调用时会出现问题。

JNI函数命名是有一些规则的,并不是我们想怎么写就怎么写,首先是JNIEXPORT表明这还是个可被外部调用的函数。

接下来是函数的返回值jstring,这个东西可以展开讲讲,我们知道Java和C是两套东西,它们的基本数据类型是不可以直接互相调用的,所以jni头文件中就定义了各种各样的映射,我在别人博客盗了个表格:

Java类型 本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型e
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型
Object jobject 任何Java对象,或者没有对应java类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组

JNICALL表明这个函数是个jni函数,jni函数名的命名也是有规则的,Java_Java包名_Java类名_native函数名,函数都会有两个固定的参数JNIEnv *env, jclass是由jvm虚拟机传入的,env指针提供了大量的访问java变量和方法的函数,后面会常用到。

接下来修改CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.17)
project(NativeHello)

set(CMAKE_CXX_STANDARD 14)

add_library(NativeHello SHARED com_leqi_example_Hello.h Hello.cpp)

其实CMakeLists也很容易理解,主要把你要编译的文件名写在add_library中的SHARED后面即可。

最后点击绿色的小锤子编译一下项目,然后可以看到libNativeHello.dll文件,这个就是我们要的C函数库,现在把它copy到桌面或者容易找的地方。

3. Java中调用C函数库

回到IDEA,在Hello.java中略作修改:

package com.leqi.example;

public class Hello {

    public static native String helloFromNative();

    public static void main(String[] args) {
        System.load("刚才编译好的.dll文件的绝对路径");
        System.out.println(helloFromNative());
    }
}

运行看一下结果


OK,大功告成,接下来我会写一篇C和Java之间传递复杂数据类型的文章。

你可能感兴趣的:(JNI开发:入门篇)