本文來源于博客《自己實現一個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 <jni.h>
/* 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 <jni.h>
改為#include "jni.h"
。
5、編寫方法
任意創建一個cpp
源文件,將我們的方法寫入。這個源文件需要包含Native.h
:
#include "Native.h"
由于VS2017已經為我生成了一個名為JavaNativeForC.cpp
的頭文件,我可以直接編寫:
// JavaNativeForC.cpp: 定義 DLL 應用程序的導出函數。
//
#include "stdafx.h"
#include<iostream>
#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 <jni.h>
/* 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 <jni.h>
#include <stdio.h>
#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_<ClassName>_<MethodName>
- 如果方法名中有下劃線,就會在下劃線之前添加
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:
更多參考: