通过第一篇文章,大家明白了调用 native 方法之前,首先要调用 System.loadLibrary 接口加载一个实现了native 方法的动态库才能正常访问,否则就会抛出 java.lang.UnsatisfiedLinkError 异常,找不到 XX 方法的提示。现在我们想想,在 Java 中调用某个 native 方法时,JVM 是通过什么方式,能正确的找到动态库中 C/C++ 实现的那个 native 函数呢?
JVM 查找 native 方法
JVM 查找 native 方法有两种方式:
- 按照 JNI 规范的命名规则
- 调用 JNI 提供的 RegisterNatives 函数,将本地函数注册到 JVM 中。(后面会详细介绍)
本文通过第一篇文章 HelloWorld 示例中的 Java_com_study_jnilearn_HelloWorld_sayHello 函数来详细介绍第一种方式:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_HelloWorld */
#ifndef _Included_com_study_jnilearn_HelloWorld
#define _Included_com_study_jnilearn_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_study_jnilearn_HelloWorld
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
JNIEXPORT 和 JNICALL 的作用
在上篇文章中,我们在将 HelloWorld.c 编译成动态库的时候,用-I
参数包含了 JDK 安装目录下的两个头文件目录:
gcc -dynamiclib -o /Users/yangxin/Library/Java/Extensions/libHelloWorld.jnilib jni/HelloWorld.c -framework JavaVM -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin
其中第一个目录为jni.h
头文件所在目录,第二个是跨平台头文件目录(Mac os x系统下的目录名为 darwin,在 Windows 下目录名为 win32,linux 下目录名为 linux),用于定义与平台相关的宏,其中用于标识函数用途的两个宏 JNIEXPORT 和 JNICALL,就定义在 darwin 目录下的jni_md.h
头文件中。在 Windows 中编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加__declspec
(dllexport)标识,表示将该函数导出在外部可以调用。在 Linux/Unix 系统中,这两个宏可以省略不加。这两个平台的区别是由于各自的编译器所产生的可执行文件格式不一样。这里有篇文章详细介绍了两个平台编译的动态库区别:http://www.cnblogs.com/chio/archive/2008/11/13/1333119.html。JNICALL 在 Windows 中的值为__stdcall
,用于约束函数入栈顺序和堆栈清理的规则。
Windows 下jni_md.h
头文件内容:
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
#endif
Linux 下jni_md.h
头文件内容:
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT
#define JNIIMPORT
#define JNICALL
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
#endif
从 Linux 下的jni_md.h
头文件可以看出来,JNIEXPORT 和 JNICALL 是一个空定义,所以在 Linux 下 JNI 函数声明可以省略这两个宏。
函数的命名规则:
用 javah 工具生成函数原型的头文件,函数命名规则为:Java_
类全路径_方法名。如Java_com_study_jnilearn_HelloWorld_sayHello
,其中Java_
是函数的前缀,com_study_jnilearn_HelloWorld
是类名,sayHello
是方法名,它们之间用 _
(下划线) 连接。
函数参数:
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello(JNIEnv *, jclass, jstring);
- 第一个参数:
JNIEnv*
是定义任意 native 函数的第一个参数(包括调用 JNI 的 RegisterNatives 函数注册的函数),指向 JVM 函数表的指针,函数表中的每一个入口指向一个 JNI 函数,每个函数用于访问 JVM 中特定的数据结构。 - 第二个参数:调用 Java 中 native 方法的实例或 Class 对象,如果这个 native 方法是实例方法,则该参数是 jobject,如果是静态方法,则是 jclass。
- 第三个参数:Java 对应 JNI 中的数据类型,Java 中 String 类型对应 JNI 的 jstring 类型。(后面会详细介绍 JAVA 与 JNI 数据类型的映射关系)。
函数返回值类型:夹在 JNIEXPORT 和 JNICALL 宏中间的 jstring,表示函数的返回值类型,对应 Java 的String 类型。
总结:当我们熟悉了 JNI 的 native 函数命名规则之后,就可以不用通过javah
命令去生成相应 java native方法的函数原型了,只需要按照函数命名规则编写相应的函数原型和实现即可。
比如com.study.jni.Utils
类中还有一个计算加法的 native 实例方法add
,有两个in
t参数和一个int
返回值:public native int add(int num1, int num2)
,对应 JNI 的函数原型就是:JNIEXPORT jint JNICALL Java_com_study_jni_Utils_add(JNIEnv *, jobject, jint,jint)
。