java 调用 native 方法实例

缘起

最近因为在读《java并发编程实践》,看到第五章,想了解ConcurrentHashMap的源码,但是发现不论是JDK1.7还是JDK1.8, 都要使用一个sun.misc.Unsafe。 而这个是一个工具类(关于它,后面我会继续写文章的). 它里面几乎所有的方法都是native的. 所以不经激起了我的好奇心——native方法~ 嗯,它广泛存在与java中,但是我们平时只是CRUD,没有仔细去了解它~ 现在才发现,已经到了不得不去了解它的原理的地步了. 而且发现,不了解c/c++的java程序员是根本没办法真正理解java的!!!

分析

关于 native方法的详述参见【1】. 这里就不多说了, 本文的目的是看看如何自定义一个native方法, 并且在java程序中调用.

首先java的项目结构如下(JNITest项目对应文件夹为 D:\bone\JNITest)

HelloWorld.java如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.yfs.example;

public class HelloWorld {

public native void displayHelloWorld();

static {
System.loadLibrary("jniTest");
}

public static void main(String[] args) {
new HelloWorld().displayHelloWorld();
}

}

注意,上面的源码中,我们在static块中要求加载项目根路径下面的名为jniTest的dll. 其实这种代码也可以写在main方法中.

所以我们显然现在要去搞这个jniTest.dll.

首先cmd到上述java项目根路径下面, 例如我这里是 D:\bone\JNITest.

执行下面的脚本

1
2
mkdir jni
javah -classpath bin -d jni com.yfs.example.HelloWorld

然后就会在D:\bone\JNITest\jni 目录下生成一份 com_yfs_example_HelloWorld.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 com_yfs_example_HelloWorld */

#ifndef _Included_com_yfs_example_HelloWorld
#define _Included_com_yfs_example_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yfs_example_HelloWorld
* Method: displayHelloWorld
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_yfs_example_HelloWorld_displayHelloWorld
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

对于extern “C”不熟悉的童鞋可以参考 【2】. 简单讲,就是在C++编译环境下(ifdef __cplusplus, __cplusplus是c++的预定义宏,表示当前开发环境是c++),依旧告诉下面的函数Java_com_yfs_example_HelloWorld_displayHelloWorld用c标准进行编译.

有了这份头文件,我们开始着手开发该头文件的实现类. 我们将上面的头文件的第二行改成

1
#include "jni.h"

然后将此份头文件以及 D:\JDK\jdk8\jdk\include\jni.h和D:\JDK\jdk8\jdk\include\win32\jni_md.h 这两份头文件

(其中jni_md.h的内容很简单,也没有引入其他头文件

1
2
3
4
5
6
7
8
9
10
11
12
#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 /* !_JAVASOFT_JNI_MD_H_ */

而jni.h(这份头文件比较大)引入了三份配置文件

1
2
3
#include <stdio.h>
#include <stdarg.h>
#include "jni_md.h"

注意,stdio.h、stdarg.h在我们工程预设的include路径

1
$(VCInstallDir)include;$(VCInstallDir)atlmfc\include;$(WindowsSdkDir)include;$(FrameworkSDKDir)\include;

中是可以找到的.

而jni_md.h 在当前路径下就可以找得到. 所以没毛病.

一共三份头文件拷贝到 D:\algorithm\acm\jniTest\jniTest目录中,其中 D:\algorithm\acm\jniTest 是我们新建的win32项目. 这个项目我们按照下面的图示步骤进行创建.

打开vs2010, File->New->Project 然后出现下图. 按图中进行选择

然后在vs中为我们的项目添加头文件

添加方法是右键上图Header Files, 然后Add, 然后选择 Existing Item, 然后选择D:\algorithm\acm\jniTest\jniTest中我们已经拷贝过去的 com_yfs_example_HelloWorld.h(即这些文件都存在,我们只是将他们添加进入工程中去而已),

最后我们新建一个hello.cpp文件. 方法也是类似的,右键上图中的Source Files->Add->New Item

则最后项目

而hello.cpp的内容就是简单的实现com_yfs_example_HelloWorld.h中定义的接口

1
2
3
4
5
6
7
8
#include "com_yfs_example_HelloWorld.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_com_yfs_example_HelloWorld_displayHelloWorld
(JNIEnv *env, jobject obj)
{
printf("Hello World\n");
}

然后右键项目jniTest–>Build, 则会在 D:\algorithm\acm\jniTest\Debug 中生成dll文件(linux平台下是.so文件)

pdb文件是用于调试的. 关于动态库和静态库详见【3】.

然后将jniTest.dll 放进JNITest这个java项目的根路径下.

最后运行HelloWorld这个main方法. 但是很不幸,碰到了我们最初安装JDK的时候遇到的问题

1
2
3
4
5
6
7
Exception in thread "main" java.lang.UnsatisfiedLinkError: D:\bone\JNITest\jniTest.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1857)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at com.yfs.example.HelloWorld.<clinit>(HelloWorld.java:8)

这其实就是由于你的jdk版本是Windows 64位,而你用VS生成的DLL是32位。因此只需要编译生成一个64位的DLL动态链接库就行。

只需要Build->Configuration Manager

上面选择 x64, 之前默认的是Win32. 则选好之后点击Close. 然后重新右键项目–>Build. 最后项目目录下多出了x64目录.

我们之前用的是Debug(即32位编译DLL)中的jniTest.dll才导致上面的报错. 这次我们选用x64目录中的jniTest.dll.

ps: 注意,D:\algorithm\acm\jniTest\jniTest中也会出现Debug和x64两个目录。

但是这里面都是log日志信息(记录编译信息).

最后将我们的64位dll拷贝到java项目JNITest根路径下面,即像上面一样,重新运行HelloWorld,则得到了我们想要的结果

本文是在vc环境下编译出 64位 dll, 然后供java程序进行调用的. 其实也可以使用gcc编译出64位dll. 但是鉴于我Windows上的mingw是32位的, 就没试了. 详见【4】

前面说的一直都是调用,那么如何调试dll呢? 可以这样, 首先可以让断点停在调用dll的代码之前

然后跑到vs中去 Debug–>Attach to Process 然后选中下图中的 javaw.exe (eclipse使用javaw.exe而不是 java.exe 启动java程序)

则在eclipse中F8放开断点,你立马会进到vs中的断点中

至此,完结.

参考

【1】https://www.cnblogs.com/xingzc/articles/6078768.html

【2】https://www.cnblogs.com/douzujun/p/10619393.html

【3】https://blog.csdn.net/jeryjeryjery/article/details/70893616

【4】https://www.jianshu.com/p/1eb6d859175d