作者:diaoyf | 文章来源:http://programmerdigest.cn
JNI(Java Native Interface)Java 本机接口,允许Java 代码使用以其它语言编写的代码和代码库。通常情况下,我们在Java中调用C或C++编写的动态库(Windows中的 .dll,Linux中的 .so),来扩展Java的能力。
从Java程序员角度看,JNI非常底层、代码书写方式怪异,并且有许多“陷阱”。本文从实际运用出发,介绍如何在Linux环境中如何编写、运行 JNI程序,并揭示易犯的错误。
System V消息队列示例
我不打算只介绍一个Hello World程序,因为太简单会遗漏很多问题现象,当然太复杂也不适合我们这个短篇文章。举的例子的是开源项目LAJP中的一段程序:Java调用 System V消息队列。
消息队列是Linux内核提供的进程间通信机制,有个专有名词叫IPC,在Unix/Linux系统中IPC是非常重要的一个组成,我想Java是 因为跨平台的统一而没有将其纳入。
消息队列可以看作是系统内核管理的一片内存区域,进程A可以向消息队列中发送一块或多块数据,进程B从消息队列中读取这些数据,从而完成数据的传 递。在Linux中提供了两套消息队列机制:POSIX消息队列和System V消息队列,在这里我们只演示System V的消息队列。
LAJP开源架构是提供PHP结合Java,共同构建WEB应用的技术,是有关LAJP开源项目的具体信息可访问LAJP 开发网站:http://code.google.com/p/lajp/
C函数
在实际运用中,一般通过调用系统提供的三个C函数来使用消息队列。
函数1: 创建一个新的消息队列或访问一个已存在的消息队列。
-
-
int msgget ( key_t key, int msgflg ) ;
-
-
参数 1 (key): 用来命名一个消息队列,创建或获取(如果系统中已存在同名称的消息队列则获取,否则创建)。
-
参数 2 (msgflg): 用来定义权限等标志。
-
返回: 函数执行成功返回一个正值的消息ID,失败返回 -1 。
-
函数2: 发送数据到消息队列。
-
-
int msgsnd ( int msgid, const void *msg_ptr, size_t msg_sz, int msgflg ) ;
-
-
参数 1 (msgid): 由第一个函数msgget返回的消息队列ID。
-
参数 2 (msg_ptr): 要发送的数据块的指针。
-
参数 3 (msg_sz): 数据块的长度。
-
参数 4 (msgflg): 控制标志。可以是 0 ,也可以是IPC_NOWAIT ( 非阻塞 ) 。
-
返回: 成功 0 ,失败 -1 。
-
-
msg_ptr是一个结构指针,该结构定义在<sys/msg. h > 中:
-
struct msgbuf {
-
long mtype; //消息类型,必须大于0
-
char mtext [ 1 ] ; //消息体
-
}
-
函数3: 从消息队列中接收数据。
-
-
size_t msgrcv ( int msgid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg ) ;
-
-
参数 1 (msgid): 由第一个函数msgget返回的消息队列ID。
-
参数 2 (msg_ptr): 接收缓冲区结构的指针。
-
参数 3 (msg_sz): 缓冲区的长度。
-
参数 4 (msgtype): 消息类型
-
返回: 成功时返回读入到缓冲区的字节数,失败返回 -1 。
-
由于文章篇幅的原因,上面的三个函数的语义并没有描述的非常清楚,但对于JNI的编程已经足够了,和Java的方法相比,C函数有不同的编程习惯:
1. 成功标志: C函数的调用成功和失败总是用-1,0等数字标记,而Java通常是判断是否抛出异常。
2. 数据传入和传出: 从C函数中获取数据或传入数据,通常使用2个参数(数据块指针和长度),而Java有很好的对象封装能力,只用一个byte[]就够了(数组对象的 length属性指定长度)。
JNI Java代码
首先根据上面3个函数的定义,创建3个Java方法与之对应:
-
-
package lajp;
-
-
/**
-
* System V 消息队列JNI
-
* @author diaoyf
-
*
-
*/
-
public class MsgQ
-
{
-
/**
-
* 创建或获得消息队列,返回消息队列标识符msqid.
-
* <br>
-
* 此方法对应System V消息队列函数 int msgget(key_t key,int msgflg)。
-
* Java参数msg_key对应C参数key,msgflag默认为八进制0666
-
*
-
* @param msg_key 消息队列KEY
-
* @return
-
* <li>执行成功返回正值int,表示msqid</li>
-
* <li>失败返回-1</li>
-
*/
-
public native static int msgget ( int msg_key ) ;
-
-
/**
-
* 发送消息(将消息加入消息队列)
-
* <br>
-
* 此方法对应System V消息队列函数 int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg)。
-
*
-
*
-
* @param msqid 消息队列标识符msqid
-
* @param type 消息类型
-
* @param msg 消息体
-
* @param len 消息体长度(在Java中仍冗余这个参数是为了在C中更方便的获取msg的长度)
-
* @return
-
* <li>0:执行成功</li>
-
* <li>-1:执行失败</li>
-
*/
-
public native static int msgsnd ( int msqid, int type, byte [ ] msg, int len ) ;
-
-
/**
-
* 从消息队列中接收消息,本方法为阻塞方法
-
* <br>
-
* 此方法对应System V消息队列函数 int msgrcv(int msgid, void *msg_ptr, size_t msg_sz, int msgtype, int msgflg)。
-
*
-
* @param msqid 消息类型
-
* @param msg 消息接收缓冲区
-
* @param len 接收缓冲区长度(在Java中仍冗余这个参数是为了在C中更方便的获取msg的长度)
-
* @param type 消息类型
-
* @return
-
* <li>执行成功:接收消息的字节数</li>
-
* <li>执行失败:-1</li>
-
*/
-
public native static int msgrcv ( int msqid, byte [ ] msg, int len, int type ) ;
-
}
-
msgrcv方法的含义是读取,一般在Java方法的设计中,读取的数据是通过方法返回值传递给调用者,而msgrcv的返回数据放在msg 中,msg同时又作为方法的入参,这叫做“值–结果参数”,在C语言中这种参数很常见,而Java中比较少见。
提示一:
Java方法必须添加”native”修饰字,且无方法体,表示此方法的实现是通过内部(C语言)实现的。
JNI 编译
JNI的编译需要两步,第一步和普通的Java编译一样,用javac命令从MsgQ.java编译出MsgQ.class,不论用哪种Java开 发环境,编译出的目录总是按照package路径分布的:
-
-
//假定编译的classpath的根路径在/tmp/msg/,编译后的 MsgQ.class的位置:
-
-
/tmp/msg/
-
|
-
|–lajp
-
|
-
|–MsgQ. class
-
下来用javah命令编译出MsgQ.class对应的.h文件:
-
$ cd /tmp/lajp
-
-
$ javah lajp.MsgQ
执行完javah命令后,会在/tmp/lajp/路径中生成文件”lajp_MsgQ.h”。
提示二:
Javah命令仍然是根据classpath和package路径做参照。
编写C代码
查看生成的lajp_MsgQ.h文件:
-
-
/* DO NOT EDIT THIS FILE – it is machine generated */
-
#include <jni.h>
-
/* Header for class lajp_MsgQ */
-
-
#ifndef _Included_lajp_MsgQ
-
#define _Included_lajp_MsgQ
-
#ifdef __cplusplus
-
extern "C" {
-
#endif
-
/*
-
* Class: lajp_MsgQ
-
* Method: msgget
-
* Signature: (I)I
-
*/
-
JNIEXPORT jint JNICALL Java_lajp_MsgQ_msgget
-
( JNIEnv *, jclass, jint ) ;
-
-
/*
-
* Class: lajp_MsgQ
-
* Method: msgsnd
-
* Signature: (II[BI)I
-
*/
-
JNIEXPORT jint JNICALL Java_lajp_MsgQ_msgsnd
-
( JNIEnv *, jclass, jint, jint, jbyteArray, jint ) ;
-
-
/*
-
* Class: lajp_MsgQ
-
* Method: msgrcv
-
* Signature: (I[BII)I
-
*/
-
JNIEXPORT jint JNICALL Java_lajp_MsgQ_msgrcv
-
( JNIEnv *, jclass, jint, jbyteArray, jint, jint ) ;
-
-
#ifdef __cplusplus
-
}
-
#endif
-
#endif
-
注意在函数的参数列表中多了2个参数,JNIEnv和jclass。JNIEnv是JNI环境内置的函数数组指针,一会儿用到;jclass根据 Java类的变化而不同,在我们的程序中MsgQ.class是静态类,因此这类的jclass指的是类本身,如果是非静态类则指的是对象。
下面根据lajp_MsgQ.h头文件,编写C的实现代码:
-
/* 文件名: lajp_MsgQ.c */
-
/* -------------------------------- */
-
/* System V Message queues */
-
/* Java JNI */
-
/* -------------------------------- */
-
#include <sys/types.h>
-
#include <sys/ipc.h>
-
#include <sys/msg.h>
-
#include <sys/sem.h>
-
#include <sys/shm.h>
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <string.h>
-
#include "lajp_MsgQ.h"
-
-
#define MSG_MAX 8192 /** 消息最大长度 */
-
-
/*消息缓冲区*/
-
struct message
-
{
-
long msg_type; //消息标识符
-
char msg_text [ MSG_MAX ] ; // 消息
-
} ;
-
-
/*
-
* 创建或获得消息队列
-
* 参数 key: 消息队列key
-
* 返回: 消息队列ID(成功) -1(失败)
-
*/
-
JNIEXPORT jint JNICALL Java_lajp_MsgQ_msgget
-
( JNIEnv *env, jclass obj, jint key )
-
{
-
jint msqid; /* 消息队列标识符 */
-
if ( ( msqid = msgget ( key, IPC_CREAT | 0666 ) ) == -1 )
-
{
-
perror ( "[JNI ERROR]msgget Error" ) ;
-
}
-
-
return msqid;
-
}
-
-
/*
-
* 发送消息到消息队列
-
* Class: phpjava_MsgQ
-
* Method: msgsnd
-
* Signature: (II[BI)I
-
*/
-
JNIEXPORT jint JNICALL Java_lajp_MsgQ_msgsnd
-
( JNIEnv *env, jclass obj, jint msqid, jint mstype, jbyteArray msg, jint mslen )
-
{
-
if ( MSG_MAX < mslen )
-
{
-
perror ( "[JNI ERROR]msgsnd Error: jbyteArray msg too big." ) ;
-
}
-
-
/* 消息结构 */
-
struct message msgq;
-
/* 复制消息类型 */
-
msgq. msg_type = mstype;
-
/* 复制java传来的消息字节数组 */
-
( *env ) ->GetByteArrayRegion ( env, msg, 0 , mslen, msgq. msg_text ) ;
-
-
int ret; /* 函数返回值 */
-
/* 调用系统函数msgsnd */
-
if ( ( ret = msgsnd ( msqid, &msgq, mslen, 0 ) ) < 0 )
-
{
-
perror ( "[JNI ERROR]msgsnd Error" ) ;
-
}
-
return ret;
-
}
-
-
/*
-
* 从消息队列中获取消息(阻塞)
-
* Class: phpjava_MsgQ
-
* Method: msgrcv
-
* Signature: (I[BII)I
-
*/
-
JNIEXPORT jint JNICALL Java_lajp_MsgQ_msgrcv
-
( JNIEnv *env, jclass obj, jint msqid, jbyteArray msg, jint mslen, jint mstype )
-
{
-
/* 消息结构 */
-
struct message msgq;
-
/* 复制消息类型 */
-
msgq. msg_type = mstype;
-
-
int readmslen;
-
/* 从消息队列读出消息,到msgq */
-
if ( ( readmslen = msgrcv ( msqid, &msgq, MSG_MAX, mstype, 0 ) ) < 0 )
-
{
-
perror ( "[JNI ERROR]msgrcv Error" ) ;
-
}
-
-
if ( mslen < readmslen )
-
{
-
perror ( "[JNI ERROR]msgrcv Error: jbyteArray msg too small." ) ;
-
}
-
-
/* 将msg中的消息复制到java字节数组中 */
-
( *env ) ->SetByteArrayRegion ( env, msg, 0 , readmslen, msgq. msg_text ) ;
-
-
/* 返回消息长度 */
-
return readmslen;
-
}
-
关于Java方法的传参定义是这样的: 基本数据类型(int, float, byte等)传“值”,对象类型(包括String和数组)传“引用”。这条原则同样适用于JNI,Java中的对象数据通过JNI接口传递到C程序中, 或反过来从C程序传递到Java中,需要通过内置的JNI函数做转换。
-
-
( *env ) ->GetByteArrayRegion ( env, msg, 0 , mslen, msgq. msg_text ) ;
-
( *env ) ->SetByteArrayRegion ( env, msg, 0 , readmslen, msgq. msg_text ) ;
-
-
//获得Java的byte[]类型参数使用 GetByteArrayRegion函数,此函数将Java传来的字节数组msg,复制0->mslen长度的数据到msgq结构中的 msg_text字段。
-
-
//SetByteArrayRegion正好相反,复制msgq结构中的 msg_text字段数据,按0->readmslen范围,到Java中的msg字节数组。
-
编译动态库文件
将源文件 lajp_MsgQ.c 和 lajp_MsgQ.h 复制到某个目录中,进入这个目录执行下面的编译命令:
-
gcc lajp_MsgQ.c --share -I. -I/usr/lib/jvm/java -6 -sun/include -I/usr/lib/jvm/java -6 -sun/include/linux -o liblajpmsgq.so
-
gcc的参数:
# 目标文件: liblajpmsgq.so
# 编译参数:
# --share : 编译为动态库(前缀2个减号)
# -I : 搜索编译JNI需要的.h文件, 注意”/usr/lib/jvm/java-6-sun/”要换成编译环境中的JAVA_HOME路径
部署库文件
编译成功后,会生成库文件“liblajpmsgq.so”,将它复制到任一个”java.library.path”路径 中,”java.library.path”路径可通过一个小程序测出:
-
-
public class TestPath
-
{
-
{
-
}
-
}
在我的机器上(Ubuntu 8.04)的输出:/usr/lib/jvm/java-6-sun-1.6.0.17/jre/lib/i386/client:/usr/lib /jvm/java-6-sun-1.6.0.17/jre/lib/i386:/usr/lib/jvm/java-6-sun-1.6.0.17/jre/../lib/i386:/usr/lib/jvm/java-6-sun-1.6.0.17/jre/lib/i386/client:/usr/lib/jvm/java-6-sun-1.6.0.17/jre/lib/i386:/usr/lib/xulrunner-addons:/usr/lib/xulrunner-addons:/usr/java/packages/lib/i386:/lib:/usr/lib
在Java中调用
写两个Java小程序,一发一收,来测测这个JNI。
发送程序:
-
import lajp.MsgQ;
-
-
public class Send
-
{
-
static
-
{
-
//加载liblajpmsgq.so
-
}
-
-
{
-
//获得消息id
-
int msqid = MsgQ. msgget ( 0xFF ) ;
-
-
//消息
-
String ms = "通过消息队列发送的一句话!" ;
-
//转换为自结数组
-
byte [ ] msBytes = ms. getBytes ( ) ;
-
-
//发送消息,发送类型定义为9527
-
int ret = MsgQ. msgsnd ( msqid, 9527 , msBytes, msBytes. length ) ;
-
-
if ( ret == 0 )
-
{
-
}
-
else
-
{
-
}
-
}
-
}
接收程序:
-
import lajp.MsgQ;
-
-
public class Receive
-
{
-
static
-
{
-
//加载liblajpmsgq.so
-
}
-
-
{
-
//获得消息id
-
int msqid = MsgQ. msgget ( 0xFF ) ;
-
-
//定义接收缓冲区
-
byte [ ] buf = new byte [ 1024 ] ;
-
//接收消息,接收类型定义为9527
-
int len = MsgQ. msgrcv ( msqid, buf, 1024 , 9527 ) ;
-
-
if ( len == -1 )
-
{
-
return ;
-
}
-
-
}
-
}
注意:JNI的库文件名是“liblajpmsgq.so”,loadLibrary加载时只写“lajpmsgq”。
消息队列传送消息具有异步性,所以先启动Send或Receive都可以进行消息发送。