文章目录
1 - Java native关键字
尽管使用Java代码编程已经比较方便,但是在某些特定情况下,比如我们希望程序能够直接与底层进行交互,或者是进一步提升程序的性能,又或是对于系统的一些功能JVM并没有进行封装,那么这个关键字的作用就来了
一个native方法能够让Java去调用非Java代码书写的接口,JVM将这一部分又封装,让我们在使用Java代码的时候不用去关注底层实现的细节转而更多的关注Java本身代码逻辑的书写。
参考文章:
Java安全详谈-JNI 底层分析
深入理解JNI
2 - native方法的注册方式
native方法的注册方式主要是静态方式和动态方式两种,前者会将Java代码中书写的函数具体的实现(位于DLL源文件当中)与函数声明(位于基于Java代码生成的头文件中)进行绑定,从而在Java代码调用native方法的时候,JVM能够具体地找到对应的方法实现。
相较于静态方式进行注册,动态方式的注册则具有更高的灵活度,动态方式不需要显式地将函数声明和实现绑定,其对应的函数命名在DLL源文件中可以是任意的,更加灵活,并且拥有更高的查找效率,两种方式都是通过Java System类提供的loadLibrary方法将项目中的动态库文件加载到JVM中以供调用,下面分别介绍两种注册方式
JDK版本:jdk-17.0.5
开发工具:Intellij Idea, Visual Studio 2022
2.1 - 静态方式的注册
2.1.1 - 头文件生成
通过Java代码调用c语言实现的求和计算来演示原生方法静态方式的注册过程。项目结构:
在测试类当中书写原生方法,使用静态代码块的形式,在类被加载的时候将对应的动态链接库加载进入虚拟机当中,在主方法中测试原生方法是否生效:
package club.pineclone.jni.demo;
/**
* 静态方式注册native方法
*/
public class IntSum {
//原生方法声明
private native int sum(int a, int b);
//将含有原生方法实现的DLL文件加载到Java虚拟机
static {
System.loadLibrary("DLL_NAME");
}
//在这里测试原生方法
public static void main(String[] args) {
System.out.println(new IntSum().sum(5, 6));
}
}
注意loadLibrary里面的参数,后面要回来改的
可以使用Java bin目录中的javac命令,来生成Java类中原生方法对应的头文件(曾经是javah,高版本jdk改用javac),可以打开idea的控制台进入src/main/java
目录下,然后键入如下命令:
javac -h . club\pineclone\jni\demo\IntSum.java
如果因为中文乱码报错的可以用下面这一条:
javac -encoding UTF-8 -h . club\pineclone\jni\demo\IntSum.java
执行完毕之后在当前目录下就可以看到根据类中的原生方法生成的c头文件了
在头文件中可以看到java中的原生方法在c语言头文件中的声明,头文件中有几个点需要稍加注意:
- JNIEXPORT jint JNICALL Java_club_pineclone_jni_demo_IntSum_sum(JNIEnv *, jobject, jint, jint)
生成c头文件之后对应方法的函数名遵循JNI的命名规范,即Java_<PackageName>_<ClassName>_<MethodName>
,如果方法名本身就带有下划线,那么将会以_数字
的形式进行替换,相较于原先方法的声明,c头文件中的声明多了JNIEnv指针
以及jobject
两个参数,这两个参数在方法被调用的时候由JVM传入,JNIEXPORT
以及JNICALL
两个宏定义在后面会介绍到。 - #include <jni.h>
引入名为jni.h
的头文件,这个头文件中声明了所有JNI相关的接口规范,位于$JAVA_HOME/include
目录下,其中包含了JNI所有的宏定义,结构体以及函数定义 - extern “C”
这一句主要是告诉编译器按照C的方式去编译C函数,使用C语言的规则进行编译和连接,而不是C++。由于C和C++在函数编译上的差异,前者不支持函数重载,在编译前后函数名不变,而C++在编译之后,无论函数是否重名,编译之后函数会被命名为函数名+参数类型
的特殊命名格式,
使用如此编译产生的DLL文件在项目中执行时会导致抛出Exception in thread "main" java.lang.UnsatisfiedLinkError
异常
因此采用C的方式进行编译连接。
2.1.2 - 为头文件书写对应的实现
对应原生方法的c头文件已经创建完成了,下一步就是为头文件中的方法填充对应的实现了,使用Visual Studio创建一个DLL项目:
项目框架:
在头文件中新建项
DLL_JNI_01.h
将上面生成的头文件中的内容复制进去即可:
项目没有检测到引入的头文件,
#include <jni.h>
报红了,需要手动给项目引入一下JNI开发的目录:
右击项目进入属性 -> C/C++ -> 常规
找到包含附加目录一栏,手动添加$JAVA_HOME/include
以及%JAVA_HOME/include/system32
两个目录
添加完成之后可以看到报红已经正常了
头文件对方法声明的注释可以留意一下:
/*
* Class: club_pineclone_jni_demo_IntSum
* Method: sum
* Signature: (II)I
*/
从上到下依次是:Java层完整类名,Java层方法名以及Java层方法的签名
$JAVA_HOME/include/win32
目录中我们可以看到名为jni_md.h
,其中就包括了JNIEXPORT,JNICALL,JNIIMPORT的宏定义:
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
上面是windows操作系统下JDK中对JNIEXPORT
、JNIIMPORT
、JNICALL
三者的宏定义,需要注意的是Linux操作系统下的JDK对三者的宏定义有一些差别,下面是Linux JDK中的宏定义:
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL
- JNIEXPORT和JNIIMPORT的作用
对于所生成的动态链接库中的方法,JNIEXPORT负责将这个方法导出,这种情况下函数对于外部调用DLL的程序而言是可见的,从而可以被成功调用,JNIEXPORT的作用类似于Java当中的public,如果一个方法不被JNIEXPORT修饰,对于外部程序而言它就是不可见的。在Windows操作系统下,它被定义成__declspec(dllexport)
,而在Linux操作系统中它被定义为__attribute__((visibility("default")))
- JNICALL的作用
在Windows中JNICALL被宏定义为__stdcall
,是一种函数调用的约定,由于在Windows当中,函数调用时参数以栈的形式保存,栈是一种后进先出的结构,__stdcall
规定参数从右到左进行保存,从而维持传入参数的顺序正确。而在Linux当中似乎没有这个特性,因此Linux中的JDK的JNICALL是置空的,在Linux当中是否将函数修饰为JNICALL
没有区别。
创建好头文件之后,我们就可以为头文件书写对应的实现了,一个DLL动态库项目中已经包含了一个cpp源文件,可以将上面生成的函数声明复制到源文件当中书写其实现:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "framework.h"
//注意引入一下头文件
#include "DLL_JNI_01.h"
//书写对应的实现
JNIEXPORT jint JNICALL Java_club_pineclone_jni_demo_IntSum_sum(JNIEnv* env, jobject obj, jint a, jint b) {
return a + b;
};
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
2.1.3 - 生成动态库DLL文件
找到生成 -> 生成解决方案
即可在项目当中生成DLL文件
来到
DLL-JNI-01/x64/Debug
下可以看到DLL文件
把文件复制到前面Idea的项目根目录下即可:
修改原先的代码,执行main方法就可以看到效果了:
注意改一下加载的DLL文件名字,控制台成功输出结果:
2.1.4 - JNIEnv*和jobject
回顾上面生成在头文件中的函数,除开我们原先就在方法形式参数表中写上的两个int参数a和b,还多出了一个JNIEnv*
和jobject
,这两个参数在函数被调用的时候由虚拟机传入,两者的实现可以在头文件引入的#include <jni.h>
中找到,实际上jni.h
包含了JNI编程需要的所有宏,结构体以及函数定义:
/*
* JNI Native Method Interface.
*/
struct JNINativeInterface_;
struct JNIEnv_;
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
在C++环境下,JNIEnv
被定义成结构体JNIEnv_
,而在C环境下,JNIEnv
则被定义成JNINativeInterface*
,这里我们关注C++中的实现,找到结构体JNIEnv_
的定义:
/*
* We use inlined functions for C++ so that programmers can write:
*
* env->FindClass("java/lang/String")
*
* in C++ rather than:
*
* (*env)->FindClass(env, "java/lang/String")
*
* in C.
*/
struct JNIEnv_ {
const struct JNINativeInterface_ *functions;
#ifdef __cplusplus
jint GetVersion() {
return functions->GetVersion(this);
}
jclass DefineClass(const char *name, jobject loader, const jbyte *buf,
jsize len) {
return functions->DefineClass(this, name, loader, buf, len);
}
jclass FindClass(const char *name) {
return functions->FindClass(this, name);
......
JNIEnv_
结构体中的functions
是一个JNI提供的函数表,囊括了JVM提供的能力,供C++代码进行调用从而实现JAVA代码的功能,通过JNINativeInterface函数表,C++代码也可以做到例如创建JAVA对象,调用JAVA方法,获取VM指针等等
函数表存在的作用就是在原生代码执行的时候,C++能够对Java代码进行操作,函数表为JVM和C/C++之间搭建起了沟通的桥梁,使原生方法能够顺利执行,JNINativeInterface结构体的定义在jni.h中也可以找到:
struct JNINativeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
void *reserved3;
jint (JNICALL *GetVersion)(JNIEnv *env);
jclass (JNICALL *DefineClass)
(JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
jsize len);
jclass (JNICALL *FindClass)
(JNIEnv *env, const char *name);
jmethodID (JNICALL *FromReflectedMethod)
(JNIEnv *env, jobject method);
jfieldID (JNICALL *FromReflectedField)
(JNIEnv *env, jobject field);
jobject (JNICALL *ToReflectedMethod)
(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);
......
函数形式参数表中的另一个参数jobect则是调用函数对应的,Java方法的对象本身,
2.1.5 - Java,JNI,C/C++中的数据类型映射
Java,JNI,C/C++各自保持着自己拥有的数据类型,Java代码中的一个数据类型到原生方法执行的时候一共通过两次映射,第一次是由Java到JNI的映射,第二次是JNI到C/C++的映射,JNI会接受Java传入的参数,并且JNI保持自己使用的一套数据类型,这是为独立性考虑的,JNI的数据类型不管是在Windows还是Linux平台上都是一致的,从而适配发生变动的C/C++中的数据类型。
JNI对基础数据类型的定义:
// jni_md.h
// 'long' is always 32 bit on windows so this matches what jdk expects
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
// jni.h
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
包括Java中的数组和对象,JNI针对C和C++环境也为它们进行了适配:
// jni.h
// c++
#ifdef __cplusplus
class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};
typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
typedef _jbooleanArray *jbooleanArray;
typedef _jbyteArray *jbyteArray;
typedef _jcharArray *jcharArray;
typedef _jshortArray *jshortArray;
typedef _jintArray *jintArray;
typedef _jlongArray *jlongArray;
typedef _jfloatArray *jfloatArray;
typedef _jdoubleArray *jdoubleArray;
typedef _jobjectArray *jobjectArray;
// c
#else
struct _jobject;
typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
#endif
有意思的是JNI在C++的环境下也保持了原先的继承体系,就像是Java当中所有类都继承自Object类,不过如果C的编译器进行的编译没有对象集成的概念的话,那么所有类型都都被定义为jobject
- 基本数据类型关系表:
Java类型 | JNI类型 | C/C++类型 | 本地类型 |
---|---|---|---|
int | jsize/jint | int | int32_t |
short | jshort | short | int16_t |
long | jlong | long/__int64 | int64_t |
byte | jbyte | signed char | char |
boolean | jboolean | unsigned char | uint8_t |
char | jchar | unsigned short | uint16_t |
float | jfloat | float | float |
double | jdouble | double | double |
- 引用数据类型关系表
Java类型 | JNI类型 | C类型 | C++类型 |
---|---|---|---|
类实例化对象 | jobject | _jobject* | _jobject* |
类 | jclass | _jobject* | _jclass* |
java.lang.String | jstring | _jobject* | _jstring* |
java.lang.Throwable | jthrowable | _jobject* | _jthrowable* |
int[] | jintArray | _jobject* | _jintArray* |
当一个原生方法被调用之后,Java当中的参数会按照Java -> JNI -> C/C++
的映射顺序进行转换
2.1.6 - 生成头文件中的方法签名
回看生成头文件时函数的注释部分:
/*
* Class: club_pineclone_jni_demo_IntSum
* Method: sum
* Signature: (II)I
*/
前面两项都比较好理解,那么所谓方法的签名(Signature)是什么
在Java中能够被native关键字修饰的部分有字段和函数两种情况,签名的生成规则如下:
- 字段: 标识字段类型的描述符
- 函数: 表示函数结构的描述符,即:(参数描述符) + 返回值类型描述符
- Java数据类型描述符关系表
Java类型 | 字段描述符(签名) | 备注 |
---|---|---|
int | I | int首字母大写 |
float | F | float首字母大写 |
double | D | double首字母大写 |
short | S | short首字母大写 |
long | L | long首字母大写 |
char | C | char首字母大写 |
byte | B | byte首字母大写 |
boolean | Z | Z(B已经被byte使用) |
object | L + /分割完整类名 | java.lang.String -> Ljava/lang/String |
array | [ + 描述符类型 | int[] -> [I |
- Java函数结构描述符关系表
Java函数 | 函数描述符(签名) | 备注 |
---|---|---|
void | V | 无返回值类型 |
Method | (每个参数字段描述符) | double sum(int a, int b) -> (II)D |
2.2 - 动态方式的注册
相较于静态注册,动态注册提供了例如安全性更高,原生方法声明简洁等优点,注册动态原生方法的核心步骤在于通过JNIEnv提供的核心方法RegisterNatives
进行动态注册,我们不需要再像动态注册那样实现一个例如
JNIEXPORT jint JNICALL Java_club_pineclone_jni_demo_IntSum_sum(JNIEnv* env, jobject obj, jint a, jint b) {
return a + b;
};
样式的代码,冗长的代码会让我们的代码可读性和可维护性下降,通过动态注册,我们只需要像正常的C/C++编程那样编写一个方法,然后通过JNIEnv的RegisterNatives
方法进行动态绑定即可,那么如何进行这种绑定呢,和上面一样,这次采用原生方法实现一个乘法运算来演示动态注册native方法。
2.2.1 - 动静态结合,进行动态注册
在club\pineclone\jni\demo
下创建第二个类IntMultiple.java
,代码结构如下:
package club.pineclone.jni.demo;
/**
* 动态方式注册native方法
*/
public class IntMultiple {
//动态注册核心方法,主要目的是获取JNIEnv对象从而进行原生方法注册
private static native void registerNatives();
//原生方法,将来和DLL动态库中的实现进行动态绑定
private static native int multiple(int a, int b);
//通过静态代码块的形式,在类被JVM加载的时候进行native的动态绑定
static {
System.loadLibrary("DLL-JNI-02");
registerNatives();
}
public static void main(String[] args) {
System.out.println(multiple(5 , 6));
}
}
和静态注册一样,进入项目的src\main\java
目录下,键入如下命令生成头文件
javac -encoding UTF-8 -h . club\pineclone\jni\demo\IntMultiple.java
观察生成的头文件,包含两个原生方法声明
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class club_pineclone_jni_demo_IntMultiple */
#ifndef _Included_club_pineclone_jni_demo_IntMultiple
#define _Included_club_pineclone_jni_demo_IntMultiple
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: club_pineclone_jni_demo_IntMultiple
* Method: registerNatives
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_club_pineclone_jni_demo_IntMultiple_registerNatives
(JNIEnv *, jclass);
/*
* Class: club_pineclone_jni_demo_IntMultiple
* Method: multiple
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_club_pineclone_jni_demo_IntMultiple_multiple
(JNIEnv *, jclass, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
只需要实现registerNatives
方法,multiple方法需要记住注释当中的Method
以及Signature
两项对应的值即可,它们是将来进行动态绑定的依据。和前面一样,创建一个新的DLL项目,命名DLL-JNI-02,将头文件迁移到项目当中,然后前往dllmain.cpp
书写对应的实现,如果项目找不到jni.h
的话记得给项目的属性中的C/C++ -> 附加包含目录
项手动添加一些$JAVA_HOME/include
以及$JAVA_HOME/include/win32
,具体的过程在静态native注册中有提及。
复制过来的头文件只需要保留registerNatives函数声明即可,这一个函数采用静态方式进行注册,从而使得其他动态方式注册的native方法得到绑定,有一种化静为动的感觉
来到dllmain.cpp
给声明的方法写好实现
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "framework.h"
// 引入生成的头文件
#include "DLL_JNI_02.h"
//写一个multiple方法实现以下native,命名任意
jint multiple(JNIEnv* env, jclass cls, jint a, jint b) {
return a * b;
}
//native方法列表以JNINativeMethod数组的形式编写:
static JNINativeMethod methods[] = {
//三个参数分别是注释中的Method,Signature以及dllmain.cpp中实现的函数
{(char*)"multiple", (char*)"(II)I", (void*)multiple},
};
//以静态方式注册registerNatives方法,然后在方法中通过JNIEnv参数进行动态native方法的绑定
JNIEXPORT void JNICALL Java_club_pineclone_jni_demo_IntMultiple_registerNatives(JNIEnv* env, jclass cls) {
//动态注册时需要cls,动态方法列表,方法个数三个参数
env->RegisterNatives(cls, methods, sizeof(methods) / sizeof(methods[0]));
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
写好之后还是一样的生成 -> 生成解决方案
,将生成好的DLL文件拷贝到Java项目当中即可,完整项目结构:
代码执行结果:
2.2.2 - 重写JNI_OnLoad()函数进行动态native注册
分析上面的动态native方式注册,注册的入口点为一个静态的native方法registerNatives,那么是否可以有一种方法可以直接进行动态注册,而不需要额外添置一个registerNatives方法作为入口点。
加载DLL文件的方法System.loadLibrary()
,会调用一个名为JNI_OnLoad()的函数,也就是说这个函数在我们加载动态库的时候是一定会被调用的,把它当作钩子,我们通过重写JNI_OnLoad的形式,在函数当中执行native的动态注册逻辑,也可以完成native注册,相较于上面的registerNatives作动态注册入口点,这种方式要更加方便一些。
通过一个减法运算,来实现这种形式下的native动态注册,club\pineclone\jni\deno
创建类IntSub.java
,代码如下:
package club.pineclone.jni.demo;
/**
* 动态方式注册native方法,重写JNI_OnLoad形式
*/
public class IntSub {
//native方法声明
private static native int sub(int a, int b);
//加载DLL动态库
static {
System.loadLibrary("DLL-JNI-02");
}
public static void main(String[] args) {
System.out.println(sub(5 , 6));
}
}
JNI_OnLoad在jni.h
中的声明:
/* Defined by native libraries. */
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved);
依照前面,生成对应头文件,创建一个DLL项目DLL-JNI-03,将$JAVA_HOME/include
和$JAVA_HOME/include/win32
加入到项目作为依赖项,添加头文件,书写头文件对应的实现如下:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "framework.h"
#include "jni.h"
//native方法实现
jint sub(JNIEnv* env, jclass cls, jint a, jint b) {
return a - b;
}
static JNINativeMethod methods[] = {
{(char*)"sub", (char*)"(II)I", (void*)sub},
};
//重写JNI_OnLoad方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
//获取JNIEnv
if (vm -> GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
//获取Java Native方法注册的class
jclass jClassName = env->FindClass("club/pineclone/jni/demo/IntSub");
//动态注册native方法
jint ret = env->RegisterNatives(jClassName, methods, sizeof(methods) / sizeof(methods[0]));
return JNI_VERSION_1_6;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
在这种方式下的cpp源文件不需要像之前引入生成的头文件,直接引入jni.h
,将生成头文件中的方法注释信息写入函数表,然后进行动态注册即可,Java代码中在System.loadLibrary
的过程中,就会执行JNI_OnLoad方法,从而执行JNIEnv的RegisterNatives方法将动态native方法进行注册,和上面化静为动实际上是一样的,不过减少了Java中书写静态入口registerNatives这一步。