Java
与
C/C++
的连动
---
在
Java
与
C/C++
的方法
的相互调用
-----------
徐兆元
前言
关于我的
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[] jobjectArra
y
-----------------------------------------------------------------------------------------------------------------------
还有一个类型
jarray,
它是一个通用数组
从
jini.h
可以看出
在
C
中
,
所以的数组数据类型都是
jobject
的同义类型
,
但是在
C++
中是有继承关系的
.
继承关系为
Jobject <
――
jarray <
――
jobjectArra
y,
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);
这些函数获得数组的内容
,
返回数组指针
, i
sCopy
如果进行拷贝,指向以
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
指定始终写输出文件
.