本文来源于博客《自己实现一个Native方法的调用》,写下自己的实现过程,实现Java和C++的混编。所用编译器/编辑器为VS2017、VSCode。
1、编写Java代码
//Native.java
import java.io.File;
public class Native{
public native static void Hello();
public native static int echo(int num);
public static void main(String[] args){
System.load("C:" + File.separator + "JavaNativeForC.dll");
Hello();
System.out.println(echo(15));
}
}
上面代码中声明了两个本地化方法:
public native static void Hello();
public native static int echo(int num);
注意它们都使用关键字native
,这说明这两个方法并不是用Java代码实现的,它们来源于本地库的实现,例如,在dll
动态链接库文件中。
使用下面代码导入动态链接库文件C:/JavaNativeForC.dll
,我们的两个本地化方法就在该文件中实现:
System.load("C:" + File.separator + "JavaNativeForC.dll");
后面就是用这两个方法进行操作。
2、编译Java代码
javac Native.java
javah -jni Native
从而生成一个名为Native.h
的头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class Native */
#ifndef _Included_Native
#define _Included_Native
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Native
* Method: Hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Native_Hello
(JNIEnv *, jclass);
/*
* Class: Native
* Method: echo
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_Native_echo
(JNIEnv *, jclass, jint);
#ifdef __cplusplus
}
#endif
#endif
这个头文件中可以看见我们声明的两个Java本地化方法
public native static void Hello();
public native static int echo(int num);
对应C++的声明:
JNIEXPORT void JNICALL Java_Native_Hello(JNIEnv *, jclass);
JNIEXPORT jint JNICALL Java_Native_echo(JNIEnv *, jclass, jint);
我们只要实现这两个方法即可
3、创建DLL项目
在VS2017中,点击:文件 - 新建 - 项目 - 动态链接库(DLL)
4、导入必要头文件
这里需要用到三个头文件:javah
命令生成的Native.h
文件、JDK中的JNI支持头文件jni.h
、jni_md.h
。后面这两个文件分别位于%JAVA_HOME%/include
以及%JAVA_HOME%/include/win32
中,在命令行中输入echo %JAVA_HOME%
就能显示出%JAVA_HOME%
的具体地址,在我的机子上是
C:\Users\Berlin>echo %JAVA_HOME%
C:\Program Files\Java\jdk1.8.0_102
将这三个头文件复制到DLL项目目录下,然后在头文件 - 添加 - 现有项
把目录下的这三个头文件导入进来,并且将
Native.h
开头的
#include
改为
#include "jni.h"
。
5、编写方法
任意创建一个cpp
源文件,将我们的方法写入。这个源文件需要包含Native.h
:
#include "Native.h"
由于VS2017已经为我生成了一个名为JavaNativeForC.cpp
的头文件,我可以直接编写:
// JavaNativeForC.cpp: 定义 DLL 应用程序的导出函数。
//
#include "stdafx.h"
#include
#include "Native.h"
using namespace std;
/*======方法实现,来源于Native.h=============*/
/*
* Class: Native
* Method: Hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Native_Hello
(JNIEnv * env, jclass cls) {
cout << "Hello World" << endl;
}
/*
* Class: Native
* Method: echo
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_Native_echo
(JNIEnv *, jclass, jint n) {
return n * 2;
}
5、生成DLL文件
没有什么问题,右键项目 - 生成
:
请注意,要设置是x86还是x64,要和本机机型匹配:
然后生成成功后,在项目目录下的
x64/debug
可以找到名为
JavaNativeForC.dll
动态链接库文件。我们将该文件复制到目的地址:因为我们在Java代码中写了
System.load("C:" + File.separator + "JavaNativeForC.dll");
,所以要将
JavaNativeForC.dll
放在
C:/
下
6、运行字节码文件
现在,使用
java Native
运行字节码文件,即可成功:
> java Native
Hello World
30
更新:
使用G++来生成动态链接库文件。
【参考:Java Programming Tutorial : Java Native Interface (JNI)】
Java代码:
public class HelloJNI{
static {
System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so (Unixes)
}
// Native method declaration
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNI().sayHello(); // Invoke native method
}
}
关于System.loadLibrary(libname)
方法:
Loads the native library specified by the
libname
argument. Thelibname
argument must not contain any platform specific prefix, file extension or path. If a native library calledlibname
is statically linked with the VM, then theJNI_OnLoad_libname
function exported by the library is invoked. See the JNI Specification for more details. Otherwise, thelibname
argument is loaded from a system library location and mapped to a native library image in an implementation- dependent manner.
也就是使用System.loadLibrary(libname)
导入本地库,则libname
不能有任何平台特用前缀、文件拓展名或者路径。这和System.load
不同,后者需要指明完整路径。
生成的HelloJNI.h
文件如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class Hello */
#ifndef _Included_Hello
#define _Included_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Hello
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Hello_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
注意sayHello
在Java中是一个类方法而不是静态方法,所以头文件的函数签名的第二个参数类型是jobject
而不是jclass
。
接下来只要实现这个方法,为此创建HelloJNI.c
的文件:
#include
#include
#include "HelloJNI.h"
// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj)
{
printf("Hello World!\n");
return;
}
然后将这两个文件HelloJNI.c
、HelloJNI.h
编译为动态链接库。
可以使用下面的分步编译:
$ gcc -c -I"C:/Program Files/Java/jdk1.8.0_102/include" -I"C:/Program Files/Java/jdk1.8.0_102/include/win32" HelloJNI.c HelloJNI.h
路径C:/Program Files/Java/jdk1.8.0_102
实际上是%JAVA_HOME%
,参数-I
指定头文件路径,这样就编译出了HelloJNI.o
文件,然后再将该文件编译为DLL文件:
$ gcc -Wl,--add-stdcall-alias -shared -o hello.dll HelloJNI.o
-shared
表示编译为dll
。
然后我们可以使用命令nm hello.dll | grep say
来查看生成的dll文件内的函数定义:
$ nm hello.dll | grep say
0000000062401430 T Java_HelloJNI_sayHello
这个函数签名是符合头文件中的定义的。如果我们使用g++
来代替上面上的gcc
看看:
$ g++ -c -I"C:/Program Files/Java/jdk1.8.0_102/include" -I"C:/Program Files/Java/jdk1.8.0_102/include/win32" HelloJNI.c HelloJNI.h
$ g++ -Wl,--add-stdcall-alias -shared -o hello.dll HelloJNI.o
然后再次查看:
$ nm hello.dll | grep say
0000000062401430 T _Z22Java_HelloJNI_sayHelloP7JNIEnv_P8_jobject
发现这个函数签名非常奇怪,它将参数类型作为后缀连接到原函数名后了。显然它与头文件中声明的方法签名完全不同。如果这时运行java字节码,就会得出找不到方法,尽管此时dll确实存在:
C:\Users\Berlin\Desktop\akka-scala>java HelloJNI
Exception in thread "main" java.lang.UnsatisfiedLinkError: HelloJNI.sayHello()V
at HelloJNI.sayHello(Native Method)
at HelloJNI.main(HelloJNI.java:13)
这是因为,后缀为.c
的,gcc把它当作是C程序,而g++当作是C++程序;后缀为.cpp
的,两者都会认为是C++程序。在这里,g++将HelloJNI.c
中的方法当做C++来编译了。C++和C有个不同就是,C++支持函数重载,而C不支持。C++的函数重载使得编译后方法签名包含了参数类型,如Java_HelloJNI_sayHello(JNIEnv *, jobject)
这样的方法,编译后它的方法名就变成了_Z22Java_HelloJNI_sayHelloP7JNIEnv_P8_jobject
。
而同时,从javah
生成的头文件可以看到有一个宏判断语句:
#ifdef __cplusplus
extern "C" {
#endif
这就说要求编译器将头文件中的方法按照C来编译,这样编出的方法名就没有乱七八糟的类型后缀,只是单纯的Java_HelloJNI_sayHello
。由于头文件和实现文件的函数签名不同,所以java运行时在dll中就找不到Java_HelloJNI_sayHello
方法的具体实现了。
也可以一次性编译到位:
$ gcc -Wl,--add-stdcall-alias -I"C:/Program Files/Java/jdk1.8.0_102/include" -I"C:/Program Files /Java/jdk1.8.0_102/include/win32" -shared -o hello.dll HelloJNI.c HelloJNI.h
附A:
g++和gcc辨析:
- 两者都可以编译C和C++代码。后缀为
.c
的,gcc把它当作是C程序,而g++当作是C++程序;后缀为.cpp
的,两者都会认为是C++程序。在这里,g++将HelloJNI.c
中的方法当做C++来编译了。 - 编译阶段,g++会调用gcc,对于C++代码,两者是等价的,但是因为gcc命令 不能 自动和C++程序使用的库联接,所以通常用g++来完成链接,为了统一起见,干脆编译/链接统统用g++了,这就给人一种错觉,好像cpp程序只能用G++似的。
- 宏·
__cplusplus
只是标志着编译器将会把代码按C还是C++语法来解释,如上所述,如果后缀为.c
,并且采用gcc编译器,则该宏就是未定义的,否则,就是已定义。 - 编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接,所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价。
- 无论是gcc还是g++ ,用
extern "C"
时,都是以C的命名方式来为符号命名,否则,都以C++方式命名。
附B:
JAVA不同方法的C语言签名:
以下是自定义Java类JNI
中不同native
方法以及其对应的C语言签名:
/*
* Class: JNI
* Method: Method_1
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_JNI_Method_11
(JNIEnv *, jclass, jstring);
public static native int Method_1(String arg);
/*
* Class: JNI
* Method: Method_2
* Signature: ([Ljava/lang/Object;)V
*/
JNIEXPORT void JNICALL Java_JNI_Method_12
(JNIEnv *, jobject, jobjectArray);
private native void Method_2(Object[] arrs);
/*
* Class: JNI
* Method: Method_3
* Signature: ([LJNI;)V
*/
JNIEXPORT void JNICALL Java_JNI_Method_13
(JNIEnv *, jobject, jobjectArray);
protected native void Method_3(JNI ... objs);
/*
* Class: JNI
* Method: otherMethod_1_3_foo_Barz
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNI_otherMethod_11_13_1foo_1Barz
(JNIEnv *, jobject);
native void otherMethod_1_3_foo_Barz();
其规律是:
- 如果是静态方法,则传入参数是
(JNIEnv *, jclass);
,如果是类方法则是(JNIEnv *, jobject);
- 方法名具有格式为
Java_
_ - 如果方法名中有下划线,就会在下划线之前添加
1
前缀作为转义字符
附C:
主题参考:
- Java™ Native Interface
- Java Native Interface Specification—Contents
- Java Native Interface(JNI)从零开始详细教程
- Java中JNI的使用详解第一篇:HelloWorld
- Java中JNI的使用详解第二篇:JNIEnv类型和jobject类型的解释
- Java中JNI的使用详解第三篇:JNIEnv类型中方法的使用
附D:
更多参考:
- C语言dll文件的说明以及生成、使用方法
- 如何编译生成dll
- VS2017动态链接库(.dll)的生成与使用
- 导出函数__declspec(dllexport)
- extern "C"
- C++中为什么有时要使用extern "C"
- lib和dll文件的区别和联系
- MinGW gcc 生成动态链接库 dll 的一些问题汇总