`

【转】JNI编程 —— 让C++和Java相互调用

    博客分类:
  • java
 
阅读更多
JNI其实是Java Native Interface的简称,也就是java本地接口。它提供了若干的API实现了和Java和其他语言的通信(主要是C&C++)。也许不少人觉 得Java已经足够强大,为什么要需要JNI这种东西呢?我们知道Java是一种平台无关性的语言,平台对于上层的java代码来说是透明的,所以在多数 时间我们是不需要JNI的,但是假如你遇到了如下的三种情况之一呢?

你的Java代码,需要得到一个文件的属性。但是你找遍了JDK帮助文档也找不到相关的API。
在本地还有一个别的系统,不过他不是Java语言实现的,这个时候你的老板要求你把两套系统整合到一起。
你的Java代码中需要用到某种算法,不过算法是用C实现并封装在动态链接库文件(DLL)当中的。
对于上述的三种情况,如果没有JNI的话,那就会变得异常棘手了。就算找到解决方案了,也是费时费力。其实说到底还是会增加开发和维护的成本。

说了那么多一通废话,现在进入正题。看过JDK源代码的人肯定会注意到在源码里有很多标记成native的方法。这些个方法只有方法签名但是没有方 法体。其实这些naive方法就是我们说的 java native interface。他提供了一个调用(invoke)的接口,然后用C或者C++去实现。我们首先来编写这个“桥梁”.我自己的开发环境是 j2sdk1.4.2_15 + eclipse 3.2 + VC++ 6.0,先在eclipse里建立一个HelloFore的Java工程,然后编写下面的代码。
Java代码
package com.chnic.jni; 
 
public class SayHellotoCPP { 
     
    public SayHellotoCPP(){ 
    } 
    public native void sayHello(String name); 


一般的第一个程序总是HelloWorld。今天换换口味,把world换成一个名字。我的native本地方法有一个String的参数。会传 递一个name到后台去。本地方法已经完成,现在来介绍下javah这个方法,接下来就要用javah方法来生成一个相对应的.h头文件。

javah是一个专门为JNI生成头文件的一个命令。CMD打开控制台之后输入javah回车就能看到javah的一些参数。在这里就不多介绍 我们要用的是 -jni这个参数,这个参数也是默认的参数,他会生成一个JNI式的.h头文件。在控制台进入到工程的根目录,也就是HelloFore这个目录,然后输 入命令。
Java代码
javah -jni com.chnic.jni.SayHellotoCPP 

命令执行完之后在工程的根目录就会发现com_chnic_jni_SayHellotoCPP.h 这个头文件。在这里有必要多句嘴,在执行javah的时候,要输入完整的包名+类名。否则在以后的测试调用过程中会发生java.lang.UnsatisfiedLinkError这个异常。

到这里java部分算是基本完成了,接下来我们来编写后端的C++代码。(用C也可以,只不过cout比printf用起来更快些,所以这里俺偷下 懒用C++)打开VC++首先新建一个Win32 Dynamic-Link library工程,之后选择An empty DLL project空工程。在这里我C++的工程是HelloEnd,把刚刚生成的那个头文件拷贝到这个工程的根目录里。随便用什么文本编辑器打开这个头文 件,发现有一个如下的方法签名。
Cpp代码
/*
* Class:     com_chnic_jni_SayHellotoCPP
* Method:    sayHello
* Signature: (Ljava/lang/String;)V
*/ 
JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello 
  (JNIEnv *, jobject, jstring); 

仔细观察一下这个方法,在注释上标注类名、方法名、签名(Signature),至于这个签名是做什么用的,我们以后再说。在这里最重要的是 Java_com_chnic_jni_SayHellotoCPP_sayHello这个方法签名。在Java端我们执行 sayHello(String name)这个方法之后,JVM就会帮我们唤醒在DLL里的Java_com_chnic_jni_SayHellotoCPP_sayHello这个方 法。因此我们新建一个C++ source file来实现这个方法。
Cpp代码
#include <iostream.h> 
#include "com_chnic_jni_SayHellotoCPP.h" 
 
 
JNIEXPORT void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello  
  (JNIEnv* env, jobject obj, jstring name) 

    const char* pname = env->GetStringUTFChars(name, NULL); 
    cout << "Hello, " << pname << endl; 


因为我们生成的那个头文件是在C++工程的根目录不是在环境目录,所以我们要把尖括号改成单引号,至于VC++的环境目录可以在 Tools->Options->Directories里设置。F7编译工程发现缺少jni.h这个头文件。这个头文件可以 在%JAVA_HOME%\include目录下找到。把这个文件拷贝到C++工程目录,继续编译发现还是找不到。原来是因为在我们刚刚生成的那个头文件 里,jni.h这个文件是被 #include <jni.h>引用进来的,因此我们把尖括号改成双引号#include "jni.h",继续编译发现少了jni_md.h文件,接着在%JAVA_HOME%\include\win32下面找到那个头文件,放入到工程根目 录,F7编译成功。在Debug目录里会发现生成了HelloEnd.dll这个文件。

这个时候后端的C++代码也已经完成,接下来的任务就是怎么把他们连接在一起了,要让前端的java程序“认识并找到”这个动态链接库,就必须把这个DLL放在windows path环境变量下面。有两种方法可以做到:

把这个DLL放到windows下面的sysytem32文件夹下面,这个是windows默认的path
复制你工程的Debug目录,我这里是C:\Program Files\Microsoft Visual Studio\MyProjects\HelloEnd\Debug这个目录,把这个目录配置到User variable的Path下面。重启eclipse,让eclipse在启动的时候重新读取这个path变量。

比较起来,第二种方法比较灵活,在开发的时候不用来回copy dll文件了,节省了很多工作量,所以在开发的时候推荐用第二种方法。在这里我们使用的也是第二种,eclipse重启之后打开 SayHellotoCPP这个类。其实我们上面做的那些是不是是让JVM能找到那些DLL文件,接下来我们要让我们自己的java代码“认识”这个动态 链接库。加入System.loadLibrary("HelloEnd");这句到静态初始化块里。

Java代码
package com.chnic.jni; 
 
public class SayHellotoCPP { 
     
    static{ 
        System.loadLibrary("HelloEnd"); 
    } 
    public SayHellotoCPP(){ 
    } 
    public native void sayHello(String name); 
     


这样我们的代码就能认识并加载这个动态链接库文件了。万事俱备,只欠测试代码了,接下来编写测试代码。
Java代码
SayHellotoCPP shp = new SayHellotoCPP(); 
shp.sayHello("World"); 

我们不让他直接Hello,World。我们把World传进去,执行代码。发现控制台打印出来Hello, World这句话。就此一个最简单的JNI程序已经开发完成。也许有朋友会对CPP代码里的
Cpp代码
const char* pname = env->GetStringUTFChars(name, NULL); 

这句有疑问,这个GetStringUTFChars就是JNI给developer提供的API,我们以后再讲。在这里不得不多句嘴。
因为JNI有一个Native这个特点,一点有项目用了JNI,也就说明这个项目基本不能跨平台了。
JNI调用是相当慢的,在实际使用的之前一定要先想明白是否有这个必要。
因为C++和C这样的语言非常灵活,一不小心就容易出错,比如我刚刚的代码就没有写析构字符串释放内存,对于java developer来说因为有了GC 垃圾回收机制,所以大多数人没有写析构函数这样的概念。所以JNI也会增加程序中的风险,增大程序的不稳定性。
 

其实在Java代码中,除了对本地方法标注native关键字和加上要加载动态链接库之外,JNI基本上是对上层coder透明的,上层coder调用那些本地方法的时候并不知道这个方法的方法体究竟是在哪里,这个道理就像我们用JDK所提供的API一样。所以在Java中使用JNI还是很简单的,相比之下在C++中调用java,就比前者要复杂的多了。

现在来介绍下JNI里的数据类型。在C++里,编译器会很据所处的平台来为一些基本的 数据类型来分配长度,因此也就造成了平台不一致性,而这个问题在Java中则不存在,因为有JVM的缘故,所以Java中的基本数据类型在所有平台下得到 的都是相同的长度,比如int的宽度永远都是32位。基于这方面的原因,java和c++的基本数据类型就需要实现一些mapping,保持一致性。下面 的表可以概括:

    Java类型                 本地类型                  JNI中定义的别名   
int long jint
long _int64 jlong
byte signed char jbyte
boolean unsigned char jboolean
char unsigned short jchar
short short jshort
float float jfloat
double double jdouble
Object _jobject* jobject

上面的表格是我在网上搜的,放上来给大家对比一下。对于每一种映射的数据类型,JNI的设计者其实已经帮我们取好了相应的别名以方便记忆。如果想了解一些更加细致的信息,可以去看一些jni.h这个头文件,各种数据类型的定义以及别名就被定义在这个文件中。

了解了JNI中的数据类型,下面就来看这次的例子。这次我们用Java来实现一个前端的market(以下就用Foreground代替)用CPP来实现一个后端factory(以下用backend代替)。我们首先还是来编写包含本地方法的java类。

Java代码
package com.chnic.service; 
 
import com.chnic.bean.Order; 
 
public class Business { 
    static{ 
        System.loadLibrary("FruitFactory"); 
    } 
     
    public Business(){ 
         
    } 
     
    public native double getPrice(String name); 
    public native Order getOrder(String name, int amount); 
    public native Order getRamdomOrder(); 
    public native void analyzeOrder(Order order); 
     
    public void notification(){ 
        System.out.println("Got a notification."); 
    } 
     
    public static void notificationByStatic(){ 
        System.out.println("Got a notification in a static method."); 
    } 


这个类里面包含4个本地方法,一个静态初始化块加载将要生成的dll文件。剩下的方法都是很普通的java方法,等会在backend中回调这些方法。这个类需要一个名为Order的JavaBean。

Java代码
package com.chnic.bean; 
 
public class Order { 
     
    private String name = "Fruit"; 
    private double price; 
    private int amount = 30; 
     
    public Order(){ 
         
    } 
 
    public int getAmount() { 
        return amount; 
    } 
  
    public void setAmount(int amount) { 
        this.amount = amount; 
    } 
 
    public String getName() { 
        return name; 
    } 
 
    public void setName(String name) { 
        this.name = name; 
    } 
 
    public double getPrice() { 
        return price; 
    } 
 
    public void setPrice(double price) { 
        this.price = price; 
    } 


JavaBean中,我们为两个私有属性赋值,方便后面的例子演示。到此为止除了测试代码之外的Java端的代码就全部高调了,接下来进行生成.h 头文件、建立C++工程的工作,在这里就一笔带过,不熟悉的朋友请回头看第一篇。在工程里我们新建一个名为Foctory的C++ source file 文件,去实现那些native方法。具体的代码如下。

Cpp代码
#include <iostream.h> 
#include <string.h> 
#include "com_chnic_service_Business.h" 
 
jobject getInstance(JNIEnv* env, jclass obj_class); 
 
JNIEXPORT jdouble JNICALL Java_com_chnic_service_Business_getPrice(JNIEnv* env,  
                                                                   jobject obj,  
                                                                   jstring name) 

    const char* pname = env->GetStringUTFChars(name, NULL); 
    cout << "Before release: "  << pname << endl; 
 
    if (strcmp(pname, "Apple") == 0) 
    { 
        env->ReleaseStringUTFChars(name, pname); 
        cout << "After release: " << pname << endl; 
        return 1.2; 
    }  
    else 
    { 
        env->ReleaseStringUTFChars(name, pname); 
        cout << "After release: " << pname << endl; 
        return 2.1; 
    }    

 
 
JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getOrder(JNIEnv* env,  
                                                                   jobject obj,  
                                                                   jstring name,  
                                                                   jint amount) 

    jclass order_class = env->FindClass("com/chnic/bean/Order"); 
    jobject order = getInstance(env, order_class); 
     
    jmethodID setName_method = env->GetMethodID(order_class, "setName", "(Ljava/lang/String;)V"); 
    env->CallVoidMethod(order, setName_method, name); 
 
    jmethodID setAmount_method = env->GetMethodID(order_class, "setAmount", "(I)V"); 
    env->CallVoidMethod(order, setAmount_method, amount); 
 
    return order; 

 
JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getRamdomOrder(JNIEnv* env,  
                                                                         jobject obj) 

    jclass business_class = env->GetObjectClass(obj); 
    jobject business_obj = getInstance(env, business_class); 
 
    jmethodID notification_method = env->GetMethodID(business_class, "notification", "()V"); 
    env->CallVoidMethod(obj, notification_method); 
 
    jclass order_class = env->FindClass("com/chnic/bean/Order"); 
    jobject order = getInstance(env, order_class); 
    jfieldID amount_field = env->GetFieldID(order_class, "amount", "I"); 
    jint amount = env->GetIntField(order, amount_field); 
    cout << "amount: " << amount << endl; 
    return order; 

 
 
JNIEXPORT void JNICALL Java_com_chnic_service_Business_analyzeOrder (JNIEnv* env,  
                                                                     jclass cls,  
                                                                     jobject obj) 

    jclass order_class = env->GetObjectClass(obj); 
    jmethodID getName_method = env->GetMethodID(order_class, "getName", "()Ljava/lang/String;"); 
    jstring name_str = static_cast<jstring>(env->CallObjectMethod(obj, getName_method)); 
    const char* pname = env->GetStringUTFChars(name_str, NULL); 
 
    cout << "Name in Java_com_chnic_service_Business_analyzeOrder: " << pname << endl; 
    jmethodID notification_method_static = env->GetStaticMethodID(cls, "notificationByStatic", "()V"); 
    env->CallStaticVoidMethod(cls, notification_method_static); 
 

 
jobject getInstance(JNIEnv* env, jclass obj_class) 

    jmethodID construction_id = env->GetMethodID(obj_class, "<init>", "()V"); 
    jobject obj = env->NewObject(obj_class, construction_id); 
    return obj; 


可以看到,在我Java中的四个本地方法在这里全部被实现,接下来针对这四个方法来解释下,一些JNI相关的API的使用方法。先从第一个方法讲起吧:

1.getPrice(String name)

这个方法是从foreground传递一个类型为string的参数到backend,然后backend判断返回相应的价格。在cpp的代码中, 我们用GetStringUTFChars这个方法来把传来的jstring变成一个UTF-8编码的char型字符串。因为jstring的实际类型是 jobject,所以无法直接比较。
GetStringUTFChars方法包含两个参数,第一参数是你要处理的jstring对象,第二个参数是否需要在内存中生成一个副本对象。将 jstring转换成为了一个const char*了之后,我们用string.h中带strcmp函数来比较这两个字符串,如果传来的字符串是“Apple”的话我们返回1.2。反之返回 2.1。在这里还要多说一下ReleaseStringUTFChars这个函数,这个函数从字面上不难理解,就是释放内存用的。有点像cpp里的析构函 数,只不过Sun帮我们已经封装好了。由于在JVM中有GC这个东东,所以多数java coder并没有写析构的习惯,不过在JNI里是必须的了,否则容易造成内存泄露。我们在这里在release之前和之后分别打出这个字符串来看一下效果。

粗略的解释完一些API之后,我们编写测试代码。

Java代码
Business b = new Business();         
System.out.println(b.getPrice("Apple")); 

运行这段测试代码,控制台上打出

Before release: Apple
After release:
分享到:
评论

相关推荐

    JNI编程(二) —— 让C++和Java相互调用(2)

    Java代码 博文链接:https://chnic.iteye.com/blog/236248

    JNI技术手册 c/c++调用java

    II. java c/cpp互相调用实例(姊妹篇之一)——java调用c/cpp 4 一 先制作一个系统中有的DLL文件(cpp给出的sdk接口) 4 二 JNI 7 1、 编写java文件 7 2、 生成.h头文件 8 3、 用c/cpp实现这个头文件 9 三 测试 10 ...

    h_JAVA 2应用编程150例.rar

    第12章 JNI编程 493 实例131 简单的JNI调用 494 实例132 调用Windows API 495 实例133 通过JNI运行OpenGL动画 496 实例134 JNI与COM之间的调用 500 实例135 隐藏Java控制台 502 实例136 监控内存 505 实例137 获取本...

    JAVA上百实例源码以及开源项目源代码

     Java局域网通信——飞鸽传书源代码,大家都知道VB版、VC版还有Delphi版的飞鸽传书软件,但是Java版的确实不多,因此这个Java文件传输实例不可错过,Java网络编程技能的提升很有帮助。 Java聊天程序,包括服务端和...

    JAVA上百实例源码以及开源项目

     Java局域网通信——飞鸽传书源代码,大家都知道VB版、VC版还有Delphi版的飞鸽传书软件,但是Java版的确实不多,因此这个Java文件传输实例不可错过,Java网络编程技能的提升很有帮助。 Java聊天程序,包括服务端和...

    java应用软件程序设计

    490 第12章 JNI编程 493 实例131 简单的JNI调用 494 实例132 调用Windows API 495 实例133 通过JNI运行OpenGL动画 496 实例134 JNI与COM之间的调用 500 实例135 隐藏Java控制台 502 实例136 ...

    新版Android开发教程.rar

    程序可以采用 JAVA 开发,但是因为它的虚拟机 (Virtual Machine) Dalvik ,是将 JAVA 的 bytecode 转成 自 己的格式,回避掉需要付给 SUN 有关 JAVA 的授权费用。 对手机制造者的影响 � Android 是款开源的移动计算...

Global site tag (gtag.js) - Google Analytics