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:

更多参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容

  • JAVA代码: C++代码:wjb.cpp com_apply_aspect_Wjb.h文件如何生成 可以使用ja...
    bboymonk阅读 280评论 1 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,598评论 25 707
  • native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)...
    时待吾阅读 1,110评论 0 1
  • 佩琦下午说她不喜欢佩琦妈妈把人叫到自己家,她认为带也可以,但要得到家庭成员的准许,只要有人不同意,就不允许其他人带...
    麦子飞呀飞阅读 763评论 0 0
  • 题目要求:Given a non negative integer number num. For every n...
    Jarryd阅读 326评论 0 1