Java与C/C++的连动
---在Java与C/C++的方法的相互调用
-----------徐兆元
FLxyzsby@163.com
前言关于我的JAVA
一. Java与C的接口
二. Java与C连动的实现
三. Java与C连动时的数据类型分析及C端函数处理
四. Java与C连动参数的传递
五.关于异常(*)
六.C调用Java(*)
(由于最近工作太忙,*号的标题的内容我会在这个至急得项目完成之后再写出来与大家分享)
关于我的JAVA
各种关于Java强大的功能以及美好的前途的说法我已经听了太多太多.而我还是一直在开发着各种平台C/C++程序.对Java的向往已久,在大学里开发的Java程序只能叫鸡毛蒜皮.如今,我终于有机会开发实用的Java程序.这让我真正感受了Java.
天哪!在开发的实际过程中,我经常感叹!我心爱的指针没有了,也没有显式的引用,没有默认参数(你只能重载,不过JDK1.5已经支持).更可恨的是为了带回一个需要利用的值,你必须去new 一个对象,而这个对象必须经过你的转换才是你需要的值,而这个别人在阅读你的代码的时候很难看出来.或许我的实现是一个C/C++风格的,所以看起来比较撇足.但是我真的找不到那种灵活的实现方法,一切都需要那么多资源,真是让我心疼.
发了这么多的牢骚,只能说明我不是一个Java高手.我知道它与C++有很大的区别(有的人将Java称为C++--).不过它的简单易学及强大的安全性(这里指的是程序不易崩溃性,不是网络的安全)真的令我们着迷.
在你阅读这篇文章的时候,如果你真正打算写这样的程序.那么请你准备下列的条件.
操作系统:Windows/Unix/Linux/Solaris中的一个
JAVA环境:建议JDK1.4以上的版本
C/C++环境: C/C++编译器.
为什么我要进行连个语言的联动.?
你知道C/C++的优点,你需要利用它的高效写出符合你程序要求的代码,而这个Java无法实现,或者你拥有了一个处理的C/C++代码,而你如果用Java来重写将付出巨大的代价.那么你凭什么不重用这些代码呢?原因等等.
一. Java与C的接口Jini
在JDK中,你会发现有这个目录:%JAVA_HOME%include,里面包含了这些文件
%JAVA_HOME% include
│ jawt.h
│ jni.h
│ jvmdi.h
│ jvmpi.h
│
└─win32
jawt_md.h
jni_md.h
很明显这些是为了C/C++准备的.
其中
jni.h:定义与C/C++接口函数
jni_md.h:定义基本数据类型接口
include/jawt.h和include/win32/jawt_md.h: 提供标准化方法访问本地绘图功能开发.
jvmdi.h和jvmpi.h::提供Java虚拟机与外部的接口.
在lib文件夹下,有它们的库文件.
二. Java与C连动的实现
在认识了这些接口文件之后,我们来明确一下开发Java与C/C++联动的基本步骤:
● 在Java中声明需要调用的C函数名称,即为函数生成一个C/C++存根,以便在Java平台与实际的C函数之间进行转换.其实就是利用Java生成一个C/C++的头文件.当它们在进行连接时,虚拟机将从栈中提取信息,
● 利用前一步生成的存根即头文件在C/C++环境中生成共享库文件.
● 使用Java的专用方法System.loadLibrary将第二个步骤生成的共享库加载进来.
剩下的就是你在他们之间不断地调试了,直到程序满足你的要求.
下面我们来具体分析每一个步骤并举例.(这一处为了说明方法,我们先写简单的例子)
假设我们的需求是这样的:
在Java调用C中的打印信息函数,并且返回是否成功的操作.
● Java中头文件的生成
javah 工具用来为 Java 类的本地函数生成 C/C++ 头文件,javac 工具用来编译 Java 源文件。
我们写这样一个类
package java2c.testdll;
public class javatest {
public static void main(String args[]){
int rtn = execute();
System.out.println("DLL 函数返回值为"+rtn);
}
// 加载库
static {
System.out.println(System.getProperty("java.library.path"));
System.loadLibrary("java_c_dll");
//在这里加载的是C/C++生成的DLL文件名,这里是java_c_dll.dll
System.out.println("------------------DLL Started--------------------------");
}
public native static int execute();
}
这是一个简单的不能的例子了,但它已经能说明核心了.
System.loadLibrary可以加载库,这里不用写出后缀,系统会自动检测它是dll格式还是SO等格式.
public native static int execute()就是我们声明要在C中实现的函数
这里将起声明为本地方法.
接着我们应该根据这个类写一个C/C++的头文件,它的书写规则是这样的:
(1) 使用完整的Java方法名.如HelloBaby.sayHello,如果类在一个包里,那么应该在Java方法的名称前加包名称.如com.interface.dll.common.HelloBaby.sayHello.
(2) 用下划线取代圆点,在加上前缀Java_.如Java_com_interface _dll_common_HelloBaby_sayHello.
(3) 如果类名中的字符包含不是ASCII码或者数字的字符,如’_’,’$’或者Unicode字符,那么用_0xxxx代替他们,其中xxxx是该字符的Unicode值的4个十六进制数字序列.
注:如果你有重载方法,那么你需要在后面加上双下划线.比如你有一个sayHello的重载.那么第一个叫Java_com_interface _dll_common_HelloBaby_sayHello._ _ 第二个则叫Java_com_interface _dll_common_HelloBaby_sayHello._ _I
其实我们是没有必要去手动建立这个头文件的的.因为我们有javah实用程序
它是使用见附录一.(如果你的JDK版本是1.1,请你加上-jni 参数)
先编译文件(此时你应该在../ java2c/testdll目录下)
javac javatest.java
利用javah生成的头文件(此时候你应该在../目录下)
javah java2c.testdll. javatest
生成的头文件为java2c_testdll_javatest.h
他的内容是这样的
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class java2c_testdll_javatest */
#ifndef _Included_java2c_testdll_javatest
#define _Included_java2c_testdll_javatest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: java2c_testdll_javatest
* Method: execute
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_java2c_testdll_javatest_execute
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
我们看到了它将Java的函数翻译成了C/C++语言函数声明,其中的参数也做了相应的转换声明.
还有我们看到了所有的函数被强制变成了C的函数名字生成方式.
JNIEXPORT jint JNICALL Java_java2c_testdll_javatest_execute
(JNIEnv *, jclass);
JNIEnv 为环境参数
Jclass 为类参数,这些都是系统自动加载上去的,他们的作用很大,后面我们再介绍.
下面我们利用这个函数声明写出响应的C/C++代码
注意,在编写过程中,请保证在上面介绍的接口头文件能被C编译系统找到.
java2c_testdll_javatest.cpp
#include "stdio.h"
#include "jni_md.h"
#include "java2c_testdll_javatest.h"
/*
* Class: java2c_testdll_javatest
* Method: execute
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_java2c_testdll_javatest_execute
(JNIEnv* env, jclass cls){
printf("你的DLL文件已经调用成功!");
return 0;
};
将以上的代码进行编译,并将其纳入动态加载库.
如果你在Linux下的Gnu C编译器,你可以使用下面的命令:
Gcc –c –fPIC –I/usr/local/jdk/include/ -I/jdk/include/linux java2c_testdll_javatest.cpp
Gcc –shared -o lib java2c_testdll_javatest.so java2c_testdll_javatest.o
如果是Solaris的Sun编译器,可以使用:
Cc –G –I/usr/local/jdk/include –I/usr./local/jdk/include/solaris java2c_testdll_javatest.cpp –o lib java2c_testdll_javatest.so
如果是Windows下Microsoft C++编译器,你可以使用下面的命令:
cl –IC:/jdk/include –Ic:/jdk/include/win32 –LD java2c_testdll_javatest.cpp - java2c_testdll_javatest.dll
此时你可以使用这个DLL文件了
请将你的DLL文件放在java.library.path的路径上.
以上是调用的结果.
在写DLL文件的时候你可以
实现下面两个函数来实现对DLL的初始化和收尾工作
/* Defined by native libraries. */
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *vm, void *reserved);
三. Java与C连动时的数据类型分析
刚才我们只是举了一个简单的例子来说明连动的基本方法
我们在编写真正实用的程序的时候,我们必然需要传递参数.Java 与C数据类型的对应关系是这样的
-----------------------------------------------------------------------------------------------------------------------
Java数据类型 C数据类型 字节数
-----------------------------------------------------------------------------------------------------------------------
boolean jboolean 1
byte jbyte 1
char jchar 2
short jshort 2
int jint 4
long jlong 8
float jfloat 4
double jdouble 8
-----------------------------------------------------------------------------------------------------------------------
Java所有的数组都拥有相应的C数组类型
-----------------------------------------------------------------------------------------------------------------------
Java数据类型 C数据类型
-----------------------------------------------------------------------------------------------------------------------
Boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
object[] jobjectArray
-----------------------------------------------------------------------------------------------------------------------
还有一个类型jarray,它是一个通用数组
从jini.h可以看出
在C中,所以的数组数据类型都是jobject的同义类型,但是在C++中是有继承关系的.
继承关系为
Jobject < ―― jarray < ――jobjectArray, jbooleanArray, jbyteArray, jcharArray, jshortArray, jintArray, jlongArray, jfloatArray, jdoubleArray
对这些数组类型的操作函数典型的有
jsize (JNICALL *GetArrayLength)
(JNIEnv *env, jarray array);
获得数组的长度,适用于所有类型
你可以这样使用
在C中
jsize length = (*env)->GetArrayLength(env,array);
在C++中
Jsize length = env->GetArrayLength(array);
在C和C++中使用略有区别,请大家使用注意以下,下面提到的函数也是如此.
在标题文件jni.h中,这些数据类型用typedef语句声明为目标平台上的等价数据类型.
还有这两个常量 JNI_FALSE = 0和JNI_TRUE = 1.
jboolean * (JNICALL *GetBooleanArrayElements)
(JNIEnv *env, jbooleanArray array, jboolean *isCopy);
jbyte * (JNICALL *GetByteArrayElements)
(JNIEnv *env, jbyteArray array, jboolean *isCopy);
jchar * (JNICALL *GetCharArrayElements)
(JNIEnv *env, jcharArray array, jboolean *isCopy);
jshort * (JNICALL *GetShortArrayElements)
(JNIEnv *env, jshortArray array, jboolean *isCopy);
jint * (JNICALL *GetIntArrayElements)
(JNIEnv *env, jintArray array, jboolean *isCopy);
jlong * (JNICALL *GetLongArrayElements)
(JNIEnv *env, jlongArray array, jboolean *isCopy);
jfloat * (JNICALL *GetFloatArrayElements)
(JNIEnv *env, jfloatArray array, jboolean *isCopy);
jdouble * (JNICALL *GetDoubleArrayElements)
(JNIEnv *env, jdoubleArray array, jboolean *isCopy);
这些函数获得数组的内容,返回数组指针, isCopy 如果进行拷贝,指向以JNI_TRUE填充的jboolean,否则指向以JNI_FALSE填充的jboolean。
void (JNICALL *ReleaseBooleanArrayElements)
(JNIEnv *env, jbooleanArray array, jboolean *elems, jint mode);
void (JNICALL *ReleaseByteArrayElements)
(JNIEnv *env, jbyteArray array, jbyte *elems, jint mode);
void (JNICALL *ReleaseCharArrayElements)
(JNIEnv *env, jcharArray array, jchar *elems, jint mode);
void (JNICALL *ReleaseShortArrayElements)
(JNIEnv *env, jshortArray array, jshort *elems, jint mode);
void (JNICALL *ReleaseIntArrayElements)
(JNIEnv *env, jintArray array, jint *elems, jint mode);
void (JNICALL *ReleaseLongArrayElements)
(JNIEnv *env, jlongArray array, jlong *elems, jint mode);
void (JNICALL *ReleaseFloatArrayElements)
(JNIEnv *env, jfloatArray array, jfloat *elems, jint mode);
void (JNICALL *ReleaseDoubleArrayElements)
(JNIEnv *env, jdoubleArray array, jdouble *elems, jint mode);
这些函数用于通知虚拟机指针已经不再需要.
其中mode 的参数有
0=在更新数组元素后释放elems缓冲器
JNI_COMMIT=在更新数组元素后不释放elems缓冲器
JNI_ABORT=不更新数组元素释放elems缓冲器
void (JNICALL *GetBooleanArrayRegion)
(JNIEnv *env, jbooleanArray array, jsize start, jsize l, jboolean *buf);
void (JNICALL *GetByteArrayRegion)
(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf);
void (JNICALL *GetCharArrayRegion)
(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf);
void (JNICALL *GetShortArrayRegion)
(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf);
void (JNICALL *GetIntArrayRegion)
(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf);
void (JNICALL *GetLongArrayRegion)
(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf);
void (JNICALL *GetFloatArrayRegion)
(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf);
void (JNICALL *GetDoubleArrayRegion)
(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf);
void (JNICALL *SetBooleanArrayRegion)
(JNIEnv *env, jbooleanArray array, jsize start, jsize l, const jboolean *buf);
void (JNICALL *SetByteArrayRegion)
(JNIEnv *env, jbyteArray array, jsize start, jsize len, const jbyte *buf);
void (JNICALL *SetCharArrayRegion)
(JNIEnv *env, jcharArray array, jsize start, jsize len, const jchar *buf);
void (JNICALL *SetShortArrayRegion)
(JNIEnv *env, jshortArray array, jsize start, jsize len, const jshort *buf);
void (JNICALL *SetIntArrayRegion)
(JNIEnv *env, jintArray array, jsize start, jsize len, const jint *buf);
void (JNICALL *SetLongArrayRegion)
(JNIEnv *env, jlongArray array, jsize start, jsize len, const jlong *buf);
void (JNICALL *SetFloatArrayRegion)
(JNIEnv *env, jfloatArray array, jsize start, jsize len, const jfloat *buf);
void (JNICALL *SetDoubleArrayRegion)
(JNIEnv *env, jdoubleArray array, jsize start, jsize len, const jdouble *buf);
这些函数用于访问和设置局部元素.
其他函数请参照jni.h
四. Java与C连动参数的传递
还有我要和你讨论的是参数传递有值传递和地址(还有引用传递)传递的两种方式.在它们连动时我们也要进行这两种传递的实现.
我们知道Java参数传递都是值传递,而没有地址传递的说法.但是为了带回参数的变化值(你不可能把所有的信息都在函数返回值里返回),我们不得不创造一个等价的地址传递方式.
幸运的是Java里的对象是可以作为参数传递给函数并且可以带回其在函数中的变化(从内部实现机制看,其实就是一种引用传递).
于是我们的出结论:
Java中传递用传递值静态对象时为值传递,用new传递对象引用传递.
我们要编写这样的一个函数:将一个byte[]里的内容拷贝到另一个byte[] 里
int javabytecpy( byte[] dest, byte [] src, int req_byte_len);
我们的java文件内容为
package java2c.testdll;
import java.io.UnsupportedEncodingException;
public class javatest {
public static void main(String args[]){
int rtn = 0;
String str="你好我的祖国--China";
byte[] src_byte_array = str.getBytes();
byte[] dest_byte_array =new byte[src_byte_array.length];
System.out.println("拷贝前源byte数组内容为: " +BytearrayToStgring(src_byte_array));
System.out.println("拷贝后目标byte数组内容为:"+BytearrayToStgring(dest_byte_array));
System.out.println("DLL 函数返回值为"+rtn);
rtn = javabytecpy(dest_byte_array, src_byte_array, 6);
System.out.println("拷贝前源byte数组内容为: " +BytearrayToStgring(src_byte_array));
System.out.println("拷贝后目标byte数组内容为:"+BytearrayToStgring(dest_byte_array));
System.out.println("DLL 函数返回值为"+rtn);
}
// 加载库
static {
// 显示java.library.path内容
System.out.println(System.getProperty("java.library.path"));
// 在这里加载的是C/C++生成的DLL文件名,这里是java_c_dll.dll
System.loadLibrary("java_c_dll");
System.out.println("------------------DLL Started--------------------------");
}
/**
*参数:
*dest 目标数组 对象传递
*src 源数组 对象传递
*dest_byte_len 需要拷贝长度 值传递
*返回值:
*成功 0 ,失败 -1
**/
public native static int javabytecpy( byte[] dest, byte [] src, int req_byte_len);
public static String BytearrayToStgring(byte[] byte_in){
try{
String encoding="GB2312";
String ret_str=new String(byte_in,encoding);
return ret_str;
}
catch(UnsupportedEncodingException exp){
return "";
}
}
}生成的头文件为
java2c_testdll_javatest.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class java2c_testdll_javatest */
#ifndef _Included_java2c_testdll_javatest
#define _Included_java2c_testdll_javatest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: java2c_testdll_javatest
* Method: javabytecpy
* Signature: ([B[BI)I
*/
JNIEXPORT jint JNICALL Java_java2c_testdll_javatest_javabytecpy
(JNIEnv *, jclass, jbyteArray, jbyteArray, jint);
#ifdef __cplusplus
}
#endif
#endif
我们的C/C++实现文件为
java2c_testdll_javatest.cpp
#include "stdio.h"
#include "jni_md.h"
#include "java2c_testdll_javatest.h"
/*
* Class: java2c_testdll_javatest
* Method: execute
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_java2c_testdll_javatest_javabytecpy
( JNIEnv * env,
jclass cls,
jbyteArray dest_byte_array,
jbyteArray src_byte_array,
jint req_len){
printf("DLL调用开始./n");
if ( env->GetArrayLength(src_byte_array) < req_len ){
printf("你需要的长度超过源byte数组./n");
return -1;
};
if ( env->GetArrayLength(dest_byte_array) < env->GetArrayLength(src_byte_array)){
printf("你的目标数组小于源数组./n");
return -1;
};
jbyte* byte_temp = env->GetByteArrayElements(src_byte_array, JNI_FALSE);
env->SetByteArrayRegion(dest_byte_array, 0, req_len, (const jbyte*)byte_temp);
printf("DLL调用结束./n");
return 0;
};
我们运行的结果是
C:/WINNT/system32;.;C:/WINNT/system32;C:/WINNT;C:/WINNT/system32;C:/WINNT;C:/WINNT/System32/Wbem;D:/Program Files/Microsoft Visual Studio/Common/Tools/WinNT;D:/Program Files/Microsoft Visual Studio/Common/MSDev98/Bin;D:/Program Files/Microsoft Visual Studio/Common/Tools;D:/Program Files/Microsoft Visual Studio/VC98/bin;D:/Java/jdk1.5.0_02/bin
------------------DLL Started--------------------------
拷贝前源byte数组内容为: 你好我的祖国--China
拷贝前目标byte数组内容为:
DLL 函数返回值为0
拷贝后源byte数组内容为: 你好我的祖国--China
拷贝后目标byte数组内容为:你好我
DLL 函数返回值为0
DLL调用开始.
DLL调用结束.
这个例子中我们看到了函数传递参数的方法.限于篇幅,我们也不深入讨论了.
C/C++端的编程大家可以参照jni.h里的各种声明,我想熟悉C/C++的都可以看的明白.并很快熟练地掌握.你必须保证你的C/C++程序正确而且没有内存泄露,否则你的虚拟机将会捕捉到无法处理的异常而退出或者崩溃.你可能会花费很多时间才能找到错误.所以我们的程序必须经过严格的测试才能应用.
还有一点提醒大家,与C/C++函数传递参数的时候请你对字符的转换一下,因为java字符unicode编码的,在C/C++中有选择的,普通的是ascii码,你在实际开发过程中,请参照相关的Java下unicode解码编码的资料.
附录一
javah
功能说明:
C 头文件和 Stub 文件生成器。javah 从 Java 类生成 C 头文件和 C 源文件。这些文
件提供了连接胶合,使 Java 和 C 代码可进行交互。
语法:
javah [ 命令选项 ] fully-qualified-classname. . .
javah_g [ 命令选项 ] fully-qualified-classname. . .
补充说明:
javah 生成实现本地方法所需的 C 头文件和源文件。C 程序用生成的头文件和源文件在
本地源代码中引用某一对象的实例变量。.h 文件含有一个 struct 定义,该定义的布局
与相应类的布局平行。该 struct 中的域对应于类中的实例变量。
头文件名以及在头文件中所声明的结构名都来源于类名。如果传给 javah 的类是在某个
包中,则头文件名和结构名前都要冠以该包名。下划线 (_) 用作名称分隔符。
缺省情况下,javah 为每个在命令行中列出的类都创建一个头文件,且将该文件放在当
前目录中。用 -stubs 选项创建源文件。用 -o 选项将所有列出类的结果串接成一个单
一文件。
缺省情况下,javah 为每个在命令行中列出的类都创建一个头文件,且将该文件放在当
前目录中。用 -stubs 选项创建源文件。用 -o 选项将所有列出类的结果串接成一个单
一文件。
命令选项
-o[输出文件] 将命令行中列出的所有类的头文件或源文件串接到输出文件中。-o 或 -
d 两个选项只能选择一个。
-d[目录] 设置 javah 保存头文件或 stub 文件的目录。-d 或 -o 两个选项只能选择一
个。
-stubs 使 javah 从 Java 对象文件生成 C 声明。
-verbose 指明长格式输出,并使 javah 将所生成文件的有关状态的信息输出到标准输
出设备中。
-help 输出 javah 用法的帮助信息。
-version 输出 javah 的版本信息。
-jni 使 javah 创建一输出文件,该文件包含 JNI 风格的本地方法函数原型。这是缺省
输出,所以 -jni 的使用是可选的。
-classpath[路径] 指定 javah 用来查询类的路径。如果设置了该选项,它将覆盖缺省
值或 CLASSPATH 环境变量。目录用冒号分隔。
-bootclasspath[路径] 指定加载自举类所用的路径。缺省情况下,自举类是实现核心
Java 平台的类,位于 jrelib
t.jar 和 jrelibi18n.jar 中。
-old 指定应当生成旧 JDK1.0 风格的头文件。
-force 指定始终写输出文件.
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/FLxyzsby/archive/2006/02/12/597588.aspx