Java native方法

本文來源于博客《自己實現一個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.hjni_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. The libname argument must not contain any platform specific prefix, file extension or path. If a native library called libname is statically linked with the VM, then the JNI_OnLoad_libname function exported by the library is invoked. See the JNI Specification for more details. Otherwise, the libname 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.cHelloJNI.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:

主題參考


附D:

更多參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • JAVA代碼: C++代碼:wjb.cpp com_apply_aspect_Wjb.h文件如何生成 可以使用ja...
    bboymonk閱讀 318評論 1 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,820評論 25 708
  • native關鍵字說明其修飾的方法是一個原生態方法,方法對應的實現不是在當前文件,而是在用其他語言(如C和C++)...
    時待吾閱讀 1,158評論 0 1
  • 佩琦下午說她不喜歡佩琦媽媽把人叫到自己家,她認為帶也可以,但要得到家庭成員的準許,只要有人不同意,就不允許其他人帶...
    麥子飛呀飛閱讀 789評論 0 0
  • 題目要求:Given a non negative integer number num. For every n...
    Jarryd閱讀 342評論 0 1