JNI_编程技术__网文整理(中)

android 专栏收录该内容
125 篇文章 1 订阅

15.2.2.3 传递字符串

到目前为止,我们还没有实现Java程序向C程序传递参数,或者C程序向Java程序传递参数。本例程将由Java程序向C程序传入一个字符串,C程序对该字符串转成大写形式后回传给Java程序。

Java源程序如下。

代码清单15-6 在Linux平台上调用C函数的例程——Sample1

      public class Sample1

      {

                   public native String stringMethod(String text);

     public static void main(String[] args)

     {

         System.loadLibrary("Sample1");

          Sample1 sample = new Sample1();

          String text   = sample.stringMethod("Thinking In Java");

         System.out.println("stringMethod: " + text);

     }

}

Sample1.java以“Thinking In Java”为参数调用libSample1.so中的函数stringMethod(),在得到返回的字符串后打印输出。

Sample1.c的源程序如下。

代码清单15-7 在Linux平台上调用C函数的例程——Sample1.c

      #include <Sample1.h>

      #include <string.h>   

      JNIEXPORT jstring JNICALL Java_Sample1_stringMethod(JNIEnv *env, jobject obj, jstring string)

      {

         const char *str = (*env)->GetStringUTFChars(env, string, 0);

         char cap[128];

         strcpy(cap, str);

         (*env)->ReleaseStringUTFChars(env, string, str);

       int i=0;

       for(i=0;i<strlen(cap);i++)

         *(cap+i)=(char)toupper(*(cap+i));

       return (*env)->NewStringUTF(env, cap);

    }

首先请注意函数头部分,函数接收一个jstring类 型的输入参数,并输出一个jstring类型的参数。jstring是jni.h中定义的数据类型,是JNI框架内特有的字符串类型,因为jni.h在 Sample1.h中被引入,因此在Sample1.c中无须再次引入。

程序的第4行是从JNI调用上下文中获取UTF编码的输入字符,将其放在指针str所指向 的一段内存中。第9行是释放这段内存。第13行是将经过大写转换的字符串予以返回,这一句使用了NewStringUTF()函数,将C语言的字符串指针 转换为JNI的jstring类型。JNIEnv也是在jni.h中定义的,代表JNI调用的上下文,GetStringUTFChars()、ReleaseStringUTFChars()和NewStringUTF()均是JNIEnv的函数。

15.2.2.4 传递整型数组

本节例程将首次尝试在JNI框架内启用数组:C程序向Java程序返回一个定长的整型数组成的数组,Java程序将该数组打印输出。

Java程序的源代码如下。

代码清单15-8 在Linux平台上调用C函数的例程——Sample2

       public class Sample2

      {

       public native int[] intMethod();

      public static void main(String[] args)

      {

         System.loadLibrary("Sample2");

         Sample2 sample=new Sample2();

           int[] nums=sample.intMethod();

        for(int i=0;i<nums.length;i++)

           System.out.println(nums[i]);

     }

}

Sample2.java调用libSample2.so中的函数intMethod()。Sample2.c的源代码如下。

代码清单15-9 在Linux平台上调用C函数的例程——Sample2.c

      #include <Sample2.h>

     

      JNIEXPORT jintArray JNICALL Java_Sample2_intMethod(JNIEnv *env, jobject obj)

    {

          int i = 1;

            jintArray array;//定义数组对象

          array = (*env)-> NewIntArray(env, 10);

          for(; i<= 10; i++)

               (*env)->SetIntArrayRegion(env, array, i-1, 1, &i);

  

       /* 获取数组对象的元素个数 */

      int len = (*env)->GetArrayLength(env, array);

       /* 获取数组中的所有元素 */

      jint* elems = (*env)->GetIntArrayElements(env, array, 0);

     for(i=0; i<len; i++)

        printf("ELEMENT %d IS %d/n", i, elems[i]);

return array;

    }

Sample2.c涉及了两个jni.h定义的整型数 相关的数据类型:jint和jintArray,jint是在JNI框 架内特有的整数类型。程序的第7行开辟出一个长度为10 的jint数组。然后依次向该数组中放入元素1-10。第11行至第16行不是程序的必须部分,纯粹是为了向读者们演示GetArrayLength() 和GetIntArrayElements()这两个函数的使用方法,前者是获取数组长度,后者则是获取数组的首地址以便于遍历数组。

15.2.2.5 传递字符串数组

本节例程是对上节例程的进一步深化:虽然仍然是传递数组,但是数组的基类换成了字符串这样一种对象数据类型。Java程序将向C程序传入一个包含中文字符的字符串,C程序并没有处理这个字符串,而是开辟出一个新的字符串数组返回给Java程序,其中还包含两个汉字字符串。

Java程序的源代码如下。

代码清单15-10 在Linux平台上调用C函数的例程——Sample3

      public class Sample3

      {

        public native String[] stringMethod(String text);

    

        public static void main(String[] args)

throws java.io.UnsupportedEncodingException

       {

          System.loadLibrary("Sample3");

           Sample3 sample = new Sample3();

           String[] texts = sample.stringMethod("java编程思想");

       for(int i=0;i<texts.length;i++)

        {

            texts[i]=new String(texts[i].getBytes("ISO8859-1"),"GBK");

            System.out.print( texts[i] );

       }

         System.out.println();

      }

    }

Sample3.java调用libSample3.so中的函数stringMethod()。Sample3.c的源代码如下:

代码清单15-11 在Linux平台上调用C函数的例程——Sample3.c

       #include <Sample3.h>

      #include <string.h>

      #include <stdlib.h>

     

      #define ARRAY_LENGTH 5   

     JNIEXPORT jobjectArray JNICALL Java_Sample3_stringMethod

(JNIEnv *env, jobject obj, jstring string)

       {  

         jclass objClass = (*env)->FindClass(env, "java/lang/String");

        jobjectArray texts= (*env)->NewObjectArray(env,

(jsize)ARRAY_LENGTH, objClass, 0);

         jstring jstr;

          char* sa[] = { "Hello,", "world!", "JNI", "很", "好玩" };

         int i=0;

         for(;i<ARRAY_LENGTH;i++)

          {

            jstr = (*env)->NewStringUTF( env, sa[i] );

            (*env)->SetObjectArrayElement(env, texts, i, jstr);//必须放入jstring

         }

        return texts;

   }

第9、10行是我们需要特别关注的地方:JNI框架并 没有定义专门的字符串数组,而是使用jobjectArray——对象数组,对象数组的基类是jclass,jclass是JNI框架内特有的类型,相当 于Java语言中的Class类型。在本例程中,通过FindClass()函数在JNI上下文中获取到java.lang.String的类型 (Class),并将其赋予jclass变量。

在例程中我们定义了一个长度为5的对象数组texts,并在程序的第18行向其中循环放入预先定义好的sa数组中的字符串,当然前置条件是使用NewStringUTF()函数将C语言的字符串转换为jstring类型。

本例程的另一个关注点是C程序向Java程序传递的中文字符,在Java程序中能否正常显 示的问题。在笔者的试验环境中,Sample3.c是在Linux平台上编辑的,其中的中文字符则是用支持GBK的输入法输入的,而Java程序采用 ISO8859_1字符集存放JNI调用的返回字符,因此在“代码清单15-10在Linux平台上调用C函数的例程——Sample3”的第14行中将 其转码后输出。

15.2.2.6 传递对象数组

本节例程演示的是C程序向Java程序传递对象数组,而且对象数组中存放的不再是字符串,而是一个在Java中自定义的、含有一个topic属性的MailInfo对象类型。

MailInfo对象定义如下。

代码清单15-12 在Linux平台上调用C函数的例程——MailInfo

     public class MailInfo {

         public String topic;

      public String getTopic()

      {

           return this.topic;

        }

public void setTopic(String topic)

   {

    this.topic=topic;

   }

   }

Java程序的源代码如下。

代码清单15-13 在Linux平台上调用C函数的例程——Sample4

       public class Sample4

      {

      public native MailInfo[] objectMethod(String text);

      public static void main(String[] args)

      {

          System.loadLibrary("Sample4");

         Sample4 sample = new Sample4();

          MailInfo[] mails = sample.objectMethod("Thinking In Java");

        for(int i=0;i<mails.length;i++)

              System.out.println(mails[i].topic);

    }

}

Sample4.java调用libSample4.so中的objectMethod()函数。Sample4.c的源代码如下。

代码清单15-14 在Linux平台上调用C函数的例程——Sample4.c

     #include <Sample4.h>

     #include <string.h>

     #include <stdlib.h>    

     #define ARRAY_LENGTH 5

  

      JNIEXPORT jobjectArray JNICALL Java_Sample4_objectMethod(

JNIEnv *env, jobject obj, jstring string)

       { 

    jclass objClass = (*env)->FindClass(env, "java/lang/Object");

   jobjectArray mails= (*env)->NewObjectArray(env,

(jsize)ARRAY_LENGTH, objClass, 0);

    jclass  objectClass = (*env)->FindClass(env, "MailInfo");

    jfieldID topicFieldId = (*env)->GetFieldID(env, objectClass,

"topic", "Ljava/lang/String;");

   

    int i=0;

    for(;i<ARRAY_LENGTH;i++)

    {

        (*env)->SetObjectField(env, obj, topicFieldId, string);

         (*env)->SetObjectArrayElement(env, mails, i, obj);

       }

     

    return mails;

     }

程序的第9、10行读者们应该不会陌生,在上一节的例 程中已经出现过,不同之处在于这次通过FindClass()函数在JNI上下文中获取的是java.lang.Object的类型(Class),并将 其作为基类开辟出一个长度为5的对象数组,准备用来存放MailInfo对象。

程序的第12、13行的目的则是创建一个jfieldID类型的变量,在JNI中,操作对 象属性都是通过jfieldID进行的。第12行首先查找得到MailInfo的类型(Class),然后基于这个jclass进一步获取其名为 topic的属性,并将其赋予jfieldID变量。

程序的第18、19行的目的是循环向对象数组中放入jobject对象。 SetObjectField()函数属于首次使用,该函数的作用是向jobject的属性赋值,而值的内容正是Java程序传入的jstring变量 值。请注意在向对象属性赋值和向对象数组中放入对象的过程中,我们使用了在函数头部分定义的jobject类型的环境参数obj作为中介。至此,JNI框 架固有的两个环境入参env和obj,我们都有涉及。

Chap7:Jni中C++和Java的参数传递

如何使用JNI的一些基本方法和过程在网上多如牛毛,如果你对Jni不甚了解,不知道Jni是做什么的,如何建立一个基本的jni程序,或许可以参考下面下面这些文章:

<利用VC++6.0实现JNI的最简单的例子>

<JNI入门教程之HelloWorld篇>

<SUN JNI Tutorial>

这些资料的例子中,大多数只是输入一些简单的参数,获取没有参数。而在实际的使用过程中,往往需要对参数进行处理转换。才可以被C/C++程序识别。比如我们在C++中有一个结构(Struct)DiskInfo ,需要传递一个类似于DiskInfo *pDiskInfo的参数,类似于在C++这样参数如何传递到Java中呢?下面我们就来讨论C++到Java中方法的一些常见参数的转换:

1.定义Native Java类:

如果你习惯了使用JNI,你就不会觉得它难了。既然本地方法是由其他语言实现的,它们在Java中没有函数体。但是,所有本地代码必须用本地关键词native声明,成为Java类的成员。假设我们在C++中有这么一个结构,它用来描述硬盘信息:

//硬盘信息

struct {

   char name[256];

   int serial;

}DiskInfo;

那么我们需要在Java中定义一个类来与之匹配,声明可以写成这样:

class DiskInfo {

   //名字

   public String name;

   //序列号

   public int serial;

}

在这个类中,申明一些Native的本地方法,来测试方法参数的传递,分别定义了一些函数,用来传递结构或者结构数组,具体定义如下面代码:

/**//****************** 定义本地方法********************/
    //输入常用的数值类型(Boolean,Byte,Char,Short,Int,Float,Double)
    public native void displayParms(String showText, int i, boolean bl);

    //调用一个静态方法
    public native int add(int a, int b);

    //输入一个数组
    public native void setArray(boolean[] blList);

    //返回一个字符串数组
    public native String[] getStringArray();

    //返回一个结构
    public native DiskInfo getStruct();

//返回一个结构数组
    public native DiskInfo[] getStructArray();
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

2.编译生成C/C++头文件

定义好了Java类之后,接下来就要写本地代码。本地方法符号提供一个满足约定的头文件,使用Java工具Javah可以很容易地创建它而不用手动去创建。你对Java的class文件使用javah命令,就会为你生成一个对应的C/C++头文件。

1)、在控制台下进入工作路径,本工程路径为:E:/work/java/workspace/JavaJni。

2)、运行javah 命令:javah-classpath E:/work/java/workspace/JavaJni com.sundy.jnidemo ChangeMethodFromJni

本文生成的C/C++头文件名为:com_sundy_jnidemo_ChangeMethodFromJni.h

3.在C/C++中实现本地方法

生成C/C++头文件之后,你就需要写头文件对应的本地方法。注意:所有的本地方法的第一个参数都是指向JNIEnv结构的。这个结构是用来调用JNI函数的。第二个参数jclass的意义,要看方法是不是静态的(static)或者实例(Instance)的。前者,jclass代表一个类对象的引用,而后者是被调用的方法所属对象的引用。

返回值和参数类型根据等价约定映射到本地C/C++类型,如表JNI类型映射所示。有些类型,在本地代码中可直接使用,而其他类型只有通过JNI调用操作。

表A ※   JNI类型映射

Java类型本地类型 描述

boolean jboolean C/C++8位整型

byte jbyte C/C++带符号的8位整型

char jchar C/C++无符号的16位整型

short jshort C/C++带符号的16位整型

int jint C/C++带符号的32位整型

long jlong C/C++带符号的64位整型e

float jfloat C/C++32位浮点型

double jdouble C/C++64位浮点型

Object jobject 任何Java对象,或者没有对应java类型的对象

Class jclass Class对象

String jstring 字符串对象

Object[] jobjectArray 任何对象的数组

boolean[] jbooleanArray 布尔型数组

byte[] jbyteArray 比特型数组

char[] jcharArray 字符型数组

short[] jshortArray 短整型数组

int[] jintArray 整型数组

long[] jlongArray 长整型数组

float[] jfloatArray 浮点型数组

double[] jdoubleArray 双浮点型数组

3.1 使用数组:

JNI通过JNIEnv提供的操作Java数组的功能。它提供了两个函数:一个是操作java的简单型数组的,另一个是操作对象类型数组的。

因为速度的原因,简单类型的数组作为指向本地类型的指针暴露给本地代码。因此,它们能作为常规的数组存取。这个指针是指向实际的Java数组或者Java数组的拷贝的指针。另外,数组的布置保证匹配本地类型。

为了存取Java简单类型的数组,你就要要使用GetXXXArrayElements函数(见表B),XXX代表了数组的类型。这个函数把Java数组看成参数,返回一个指向对应的本地类型的数组的指针。

表B

函数                  Java数组类型 本地类型

GetBooleanArrayElements jbooleanArray jboolean

GetByteArrayElements jbyteArray jbyte

GetCharArrayElements jcharArray jchar

GetShortArrayElements jshortArray jshort

GetIntArrayElements jintArray jint

GetLongArrayElements jlongArray jlong

GetFloatArrayElements jfloatArray jfloat

GetDoubleArrayElements jdoubleArray jdouble

JNI数组存取函数

当你对数组的存取完成后,要确保调用相应的ReleaseXXXArrayElements函数,参数是对应Java数组和GetXXXArrayElements返回的指针。如果必要的话,这个释放函数会复制你做的任何变化(这样它们就反射到java数组),然后释放所有相关的资源。

为了使用java对象的数组,你必须使用GetObjectArrayElement函数和SetObjectArrayElement函数,分别去get,set数组的元素。GetArrayLength函数会返回数组的长度。

3.2 使用对象

JNI提供的另外一个功能是在本地代码中使用Java对象。通过使用合适的JNI函数,你可以创建Java对象,get、set 静态(static)和实例(instance)的域,调用静态(static)和实例(instance)函数。JNI通过ID识别域和方法,一个域或方法的ID是任何处理域和方法的函数的必须参数。

表C列出了用以得到静态(static)和实例(instance)的域与方法的JNI函数。每个函数接受(作为参数)域或方法的类,它们的名称,符号和它们对应返回的jfieldID或jmethodID。

表C

函数 描述

GetFieldID 得到一个实例的域的ID

GetStaticFieldID 得到一个静态的域的ID

GetMethodID 得到一个实例的方法的ID

GetStaticMethodID 得到一个静态方法的ID

※域和方法的函数

如果你有了一个类的实例,它就可以通过方法GetObjectClass得到,或者如果你没有这个类的实例,可以通过FindClass得到。符号是从域的类型或者方法的参数,返回值得到字符串,如表D所示。

表D

Java类型  符号

boolean Z

byte B

char C

short S

int I

long L

float F

double D

void V

objects对象 Lfully-qualified-class-name;L类名

Arrays数组 [array-type [数组类型

methods方法 (argument-types)return-type(参数类型)返回类型

※确定域和方法的符号

下面我们来看看,如果通过使用数组和对象,从C++中的获取到Java中的DiskInfo 类对象,并返回一个DiskInfo数组:

//返回一个结构数组,返回一个硬盘信息的结构数组

JNIEXPORT jobjectArray JNICALL

Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray

(JNIEnv *env, jobject _obj)

{

   //申明一个object数组

   jobjectArray args = 0;

   //数组大小

   jsize        len = 5;

   //获取object所属类,一般为java/lang/Object就可以了

   jclass objClass = (env)->FindClass("java/lang/Object");

   //新建object数组

   args = (env)->NewObjectArray(len, objClass, 0);

   /**//* 下面为获取到Java中对应的实例类中的变量*/

   //获取Java中的实例类

   jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");

   //获取类中每一个变量的定义

   //名字

   jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");

   //序列号

   jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");

   //给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中

   for(int i=0; i < len; i++ )

    {

       //给每一个实例的变量付值

       jstring jstr = WindowsTojstring(env,"我的磁盘名字是 D:");

       //(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));

       (env)->SetObjectField(_obj,str,jstr);

       (env)->SetShortField(_obj,ival,10);

       //添加到objcet数组中

       (env)->SetObjectArrayElement(args, i, _obj);

    }

   //返回object数组

   return args;

}

全部的C/C++方法实现代码如下:

/**//*

*

* 一缕阳光(sundy)版权所有,保留所有权利。

*/

/**//**

*

* TODO Jni 中一个从Java到C/C++参数传递测试类

*

* @author 刘正伟(sundy)

* @see http://www.cnweblog.com/sundy

* @see mailto:sundy26@126.com

* @version 1.0

* @since 2005-4-30

*

* 修改记录:

*

* 日期              修改人                 描述

*----------------------------------------------------------------------------------------------

*

*

*

*/

// JniManage.cpp : 定义 DLL 应用程序的入口点。

//

package com.sundy.jnidemo;

#include "stdafx.h"

#include <stdio.h>

#include <math.h>

#include "jni.h"

#include "jni_md.h"

#include "./head/Base.h"

#include "head/wmi.h"

#include "head/com_sundy_jnidemo_ChangeMethodFromJni.h" //通过javah –jni javactransfer 生成

#include <stdio.h>

#include "stdlib.h"

#include "string.h"

#pragma comment (lib,"BaseInfo.lib")

#pragma comment (lib,"jvm.lib")

//硬盘信息

struct {

   char name[256];

   int serial;

}DiskInfo;

/**//*BOOL APIENTRY DllMain( HANDLE hModule,

                       DWORD ul_reason_for_call,

                       LPVOID lpReserved

                     )

{

   LPTSTR strName = new CHAR[256] ;

   (*GetHostName)(strName);

   printf("%s/n",strName);

   delete [] strName;

   return TRUE;

}*/

//将jstring类型转换成windows类型

char* jstringToWindows( JNIEnv *env, jstring jstr );

//将windows类型转换成jstring类型

jstring WindowsTojstring( JNIEnv* env, char* str );

//主函数

BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved)

{

   return TRUE;

}

//输入常用的数值类型 Boolean,Byte,Char,Short,Int,Float,Double

JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_displayParms

(JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)

{

   const char* szStr = (env)->GetStringUTFChars(s, 0 );

   printf( "String = [%s]/n", szStr );

   printf( "int = %d/n", i );

   printf( "boolean = %s/n", (b==JNI_TRUE ? "true" : "false") );

   (env)->ReleaseStringUTFChars(s, szStr );

}

//调用一个静态方法,只有一个简单类型输出

JNIEXPORT jint JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_add

(JNIEnv *env, jobject, jint a, jint b)

{

   int rtn = (int)(a + b);

   return (jint)rtn;

}

/**/输入一个数组,这里输入的是一个Boolean类型的数组

JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_setArray

(JNIEnv *env, jobject, jbooleanArray ba)

{

    jboolean* pba = (env)->GetBooleanArrayElements(ba, 0 );

   jsize len = (env)->GetArrayLength(ba);

   int i=0;

   // change even array elements

   for( i=0; i < len; i+=2 )

    {

       pba[i] = JNI_FALSE;

       printf( "boolean = %s/n", (pba[i]==JNI_TRUE ? "true" : "false") );

    }

   (env)->ReleaseBooleanArrayElements(ba, pba, 0 );

}

/**/返回一个字符串数组

JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStringArray

(JNIEnv *env, jobject)

{

   jstring      str;

   jobjectArray args = 0;

   jsize        len = 5;

   char*        sa[] = { "Hello,", "world!", "JNI", "is", "fun" };

   int          i=0;

   args = (env)->NewObjectArray(len,(env)->FindClass("java/lang/String"),0);

   for( i=0; i < len; i++ )

    {

       str = (env)->NewStringUTF(sa[i] );

       (env)->SetObjectArrayElement(args, i, str);

    }

   return args;

}

//返回一个结构,这里返回一个硬盘信息的简单结构类型

JNIEXPORT jobject JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStruct

(JNIEnv *env, jobject obj)

{

   /**//* 下面为获取到Java中对应的实例类中的变量*/

   //获取Java中的实例类

   jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");

   //获取类中每一个变量的定义

   //名字

   jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");

   //序列号

   jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");

   //给每一个实例的变量付值

   (env)->SetObjectField(obj,str,(env)->NewStringUTF("my name is D:"));

   (env)->SetShortField(obj,ival,10);

   

   return obj;

}

//返回一个结构数组,返回一个硬盘信息的结构数组

JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray

(JNIEnv *env, jobject _obj)

{

   //申明一个object数组

   jobjectArray args = 0;

   

   //数组大小

   jsize        len = 5;

   //获取object所属类,一般为ava/lang/Object就可以了

   jclass objClass = (env)->FindClass("java/lang/Object");

   //新建object数组

   args = (env)->NewObjectArray(len, objClass, 0);

   /**//* 下面为获取到Java中对应的实例类中的变量*/

   //获取Java中的实例类

   jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");

   

   //获取类中每一个变量的定义

   //名字

   jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");

   //序列号

   jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");

   //给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中

   for(int i=0; i < len; i++ )

    {

       //给每一个实例的变量付值

       jstring jstr = WindowsTojstring(env,"我的磁盘名字是 D:");

       //(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));

       (env)->SetObjectField(_obj,str,jstr);

       (env)->SetShortField(_obj,ival,10);

       //添加到objcet数组中

       (env)->SetObjectArrayElement(args, i, _obj);

    }

   //返回object数组

   return args;

}

//将jstring类型转换成windows类型

char* jstringToWindows( JNIEnv *env, jstring jstr )

{

   int length = (env)->GetStringLength(jstr );

   const jchar* jcstr = (env)->GetStringChars(jstr, 0 );

   char* rtn = (char*)malloc( length*2+1 );

   int size = 0;

   size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, rtn,(length*2+1), NULL, NULL );

   if( size <= 0 )

       return NULL;

   (env)->ReleaseStringChars(jstr, jcstr );

   rtn[size] = 0;

   return rtn;

}

//将windows类型转换成jstring类型

jstring WindowsTojstring( JNIEnv* env, char* str )

{

   jstring rtn = 0;

   int slen = strlen(str);

   unsigned short * buffer = 0;

   if( slen == 0 )

       rtn = (env)->NewStringUTF(str );

   else

    {

       int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 );

       buffer = (unsigned short *)malloc( length*2 + 1 );

       if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 )

           rtn = (env)->NewString( (jchar*)buffer, length );

    }

   if( buffer )

       free( buffer );

   return rtn;

}

Java 测试native代码

这没有什么多说的,看代码吧

//主测试程序

   public static void main(String[] args) {

       ChangeMethodFromJni changeJni = new ChangeMethodFromJni();

       //输入常用的数值类型(string int boolean)

       System.out

               .println("------------------输入常用的数值类型(string int boolean)-----------");

       changeJni.displayParms("Hello World!", 100, true);

       //调用一个静态方法

       System.out.println("------------------调用一个静态方法-----------");

       int ret = changeJni.add(12, 20);

       System.out.println("The result is: " + String.valueOf(ret));

       //输入一个数组

       System.out.println("------------------输入一个数组-----------");

       boolean[] blList = new boolean[] { true, false, true };

       changeJni.setArray(blList);

       //返回一个字符串数组

       System.out.println("------------------返回一个字符串数组-----------");

       String[] strList = changeJni.getStringArray();

       for (int i = 0; i < strList.length; i++) {

           System.out.print(strList[i]);

       }

       System.out.println();

       System.out.println("------------------返回一个结构-----------");

       //返回一个结构

       DiskInfo disk = changeJni.getStruct();

       System.out.println("name:" + disk.name);

       System.out.println("Serial:" + disk.serial);

       //返回一个结构数组

       System.out.println("------------------返回一个结构数组 -----------");

       DiskInfo[] diskList = changeJni.getStructArray();

       for (int i = 0; i < diskList.length; i++) {

           System.out.println("name:" + diskList[i].name);

           System.out.println("Serial:" + diskList[i].serial);

       }

    }

注:本程序在VS2003,eclipse (jse5.0) winxp sp2编译通过

posted on 2005-05-02 20:22 sundy 阅读(4406) 评论(21) 编辑 收藏 所属分类: Java

评论

# re: Jni中C++和Java的参数传递 2005-05-22 14:35 张磊

请问如果想返回byte[]类型该怎么做 回复 更多评论  

# re: Jni中C++和Java的参数传递 2005-05-23 08:37 sundy

因为:

byte[] jbyteArray 比特型数组

所以你将byte[] 作为一个jbyteArray数组传递就可以了

回复 更多评论  

# re: Jni中C++和Java的参数传递 2005-09-21 14:46 小影

请问如果我想把在C++里面计算好的一个二维数组传回给java程序接受,该怎么写代码呢?我找了很多这方面的书和资料,都没有关于传递二维数组的介绍,请您给予指导,多谢啦^_^ 回复 更多评论  

# re: Jni中C++和Java的参数传递 2005-09-21 17:47 sundy

我没有直接传递过二维数组

但我想你可以把试一试二维数组转换成为一个Hashmap的数组传出来。

请参考"如何在Jni中传递出Hashmap的数组?"的一些代码

回复 更多评论  

# re: Jni中C++和Java的参数传递 2005-12-26 16:32 wangjian

返回一个结构数组时,为什么每个对象的数据都是一样的?即5个Diskinfo的成员值都相同,能不能不相同? 回复更多评论  

# re: Jni中C++和Java的参数传递 2005-12-26 16:55 wangjian

我把5个DiskInfo对象的成员serial分别设置为1、2、3、4、5,可是传递到java后5个对象的serial成员值都是5,为什么这样阿?盼回复,多谢! 回复更多评论  

# re: Jni中C++和Java的参数传递 2005-12-27 21:51 sundy

//给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中

for(int i=0; i < len; i++ )

{

......

//添加到objcet数组中

(env)->SetObjectArrayElement(args, i, _obj);

}

你看看设置的_Obj是不是都是同一个??

回复 更多评论  

# re: Jni中C++和Java的参数传递 2005-12-28 13:32 wangjian

如下所示,我就是把你程序中(env)->SetShortField(_obj,ival,10)的参数10换成i,结果每个对象都是对象的serial成员值都是4,请问怎样实现多个不同对象的传递?

for(int i=0; i < len; i++ )

{

jstring jstr = WindowsTojstring(env,"我的磁盘名字是D:");

(env)->SetObjectField(_obj,str,jstr);

(env)->SetShortField(_obj,ival,i);

(env)->SetObjectArrayElement(args, i, _obj);

}

回复 更多评论  

# re: Jni中C++和Java的参数传递 2005-12-28 15:15 sundy

应该没有问题的呀,

SetObjectArrayElement的时候,_obj是不同的吗?

要不你将for循环改为:

jstring jstr = WindowsTojstring(env,"我的磁盘名字是C:");

(env)->SetObjectField(_obj,str,jstr);

(env)->SetShortField(_obj,ival,0);

(env)->SetObjectArrayElement(args, 0, _obj);

jstring jstr = WindowsTojstring(env,"我的磁盘名字是D:");

(env)->SetObjectField(_obj,str,jstr);

(env)->SetShortField(_obj,ival,1);

(env)->SetObjectArrayElement(args, 1, _obj);

看看对吗?   回复更多评论  

# re: Jni中C++和Java的参数传递 2005-12-29 20:42 wangjian

这样作不对,不过我找到正确的方法了,要用构造函数生成新的对象。回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-01-17 11:07 luli

SQLRETURN SQLAllocHandle( SQLSMALLINT HandleType,

SQLHANDLE InputHandle,

SQLHANDLE * OutputHandlePtr

)

这是odbc api里的一个函数 SQLHANDLE 是一个结构

c#里的引用方式如下

[DllImport("ODBC32.dll")]

private static extern short SQLAllocHandle(short hType, IntPtr inputHandle, out IntPtr outputHandle);

但我不清楚SQLHANDLE 结构具体怎么构造的 因此我无法用java类来模拟

我是菜鸟 望解答谢过了  回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-01-17 14:25 luli

忘了补充 SQLHANDLE InputHandle与SQLHANDLE * OutputHandlePtr

一个是结构 一个是结构指针那我是否该如下模拟

class SQLHANDLE

{

}

public class test

{

SQLHANDLE a=new SQLHANDLE ();

public static void main(String args[]) {

int i=SQLAllocHandle( SQLSMALLINT HandleType, new SQLHANDLE(),a)

}

}

回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-03-21 17:31 Hefe

WideCharToMultiByte();

MultiByteToWideChar();

请问这两个函数实现什么功能,请作者给出代码,多谢!

回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-03-22 08:47 sundy

@Hefe look here: http://www.google.com/search?hl=zh-CN&lr=lang_zh-CN&q=WideCharToMultiByte

回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-03-28 17:40 dijk

要在c函数中调用java类的类成员的方法,比如调用JEditorPane类型成员的setText方法,该怎么办? 回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-04-16 21:33 陈世雄

java中函数的处理中,对于对象类型(非基本类型int,long...)的输入参数,函数中是可以修改输入参数的内容,函数执行完毕,修改仍然是有效的。

jni中是否也是这样呢?

回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-04-18 17:50 王文波

你好:

向你请教一个问题:我想用jini来调用dll。我在jbuilder中新建的简单的project调用jini运行正常。但是,我现在要对一个工程软件进行二次开发,该软件的

开发也使用jbuilder生成一个project,然后放在指定的路径下就可以了,该软件在运行的时候会自动读取该project。我在这个软件二次开发的project中使用

jini,则总是报错:unsatisfiedlinkError get()。其中get()方法名。请问该怎么解决这个问题?

我的邮箱:zwj23232@tom.com回复 更多评论  

# re: Jni中C++和Java的参数传递 2006-05-29 21:25 single

# re: Jni中C++和Java的参数传递 2005-12-29 20:42 wangjian

这样作不对,不过我找到正确的方法了,要用构造函数生成新的对象。回复

---------------------------------------------------

能说说方法吗? 回复更多评论  

# re: Jni中C++和Java的参数传递 2006-08-29 11:34 yangyongfa

我正在做JNI,是在C++中调用JAVA类的方法,请问,我在JAVA类的方法中参数使 用的是byte[],而我在C++中是把一个文件读成unsigned char*,请问怎么可以正确调用JAVA中的方法? 类中方法原型:public boolean AddHoyuBox2DB(String BoxName, byte[] BoxFile,byte[] WDHPic,int BoxFileBinLen, int WDHPicBinLen, String ParameterText, byte[] XXPic,int PicBinLen, byte[] SeriousPics,int SeriousPicsBinLen,String FileLenStr) ?

回复 更多评论  

# re: Jni中C++和Java的参数传递 2007-10-25 15:27 vampire

c的结构提里写有一个**p,指针的指针,在java中该如何封装??? 回复 更多评论  

# re: Jni中C++和Java的参数传递 2007-12-11 13:13 Focus

@single

for(int i=0; i < len; i++ )

{

jobject objTemp = (env)->AllocObject(objectClass); //释放问题??这个是否需要释放不是很懂

//objectClass是函数上面给的那个

jstring jstr = WindowsTojstring(env,"我的磁盘名字是D:");

(env)->SetObjectField(objTemp,str,jstr);

(env)->SetShortField(objTemp,ival,i);

(env)->SetObjectArrayElement(args, i, objTemp);

}

这个 可以实现数组 元素相同的问题

Chap8:如何将java传递过来的jbyteArray转换成C/C++中的BYTE数组

近遇到一个问题,请各位帮忙解决下:
    如何将java传递过来的jbyteArray转换成C/C++中的BYTE数组?BYTE为unsigned char类型
    这两个我理解应该是相同的吧,强制类型转换好像不启作用,应该如何转换呢?


该问题已经关闭: 问题已解决,之前代码有问题 jbyte * arrayBody = env->GetByteArrayElements(data,0); jsize theArrayLengthJ = env->GetArrayLength(data); BYTE * starter = (BYTE *)arrayBody;

Chap5:使用JNI技术实现java程序调用第三方dll(c/c++)文件的功能

JAVA的跨平台的特性深受java程 序员们的喜爱,但正是由于它为了实现跨平台的目的,使得它和本地机器的各种内部联系变得很少,大大约束了它的功能,比如与一些硬件设备通信,往往要花费很 大的精力去设计流程编写代码去管理设备端口,而且有一些设备厂商提供的硬件接口已经经过一定的封装和处理,不能直接使用java程序通过端口和设备通信,这种情况下就得考虑使用java程序去调用比较擅长同系统打交道的第三方程序,从1.1版本开始的JDK提供了解决这个问题的技术标准:JNI技术.
       JNIJava Native Interface(Java本地接口)的缩写,本地是相对于java程序来说的,指直接运行在操作系统之上,与操作系统直接交互的程序.从1.1版本的JDK开始,JNI就作为标准平台的一部分发行.在JNI出现的初期是为了Java程序与本地已编译语言,尤其是C和C++的互操作而设计的,后来经过扩展也可以与c和c++之外的语言编写的程序交互,例如Delphi程序.
       使用JNI技术固然增强了java程序的性能和功能,但是它也破坏了java的跨平台的优点,影响程序的可移植性和安全性,例如由于其他语言(如C/C++)可能能够随意地分配对象/占用内存,Java的指针安全性得不到保证.但在有些情况下,使用JNI是可以接受的,甚至是必须的,例如上面提到的使用java程序调用硬件厂商提供的类库同设备通信等,目前市场上的许多读卡器设备就是这种情况.在这必须使用JNI的情况下,尽量把所有本地方法都封装在单个类中,这个类调用单个的本地库文件,并保证对于每种目标操作系统,都可以用特定于适当平台的版本替换这个文件,这样使用JNI得到的要比失去的多很多.
       现在开始讨论上面提到的问题,一般设备商会提供两种类型的类库文件,windows系统的会包含.dll/.h/.lib文件,而linux系统的会包含.so/.a文件,这里只讨论windows系统下的c/c++编译的dll文件调用方法.
       我把设备商提供的dll文件称之为第三方dll文件,之所以说第三方,是因为JNI直 接调用的是按它的标准使用c/c++语言编译的dll文件,这个文件是客户程序员按照设备商提供的.h文件中的列出的方法编写的dll文件,我称之为第二 方dll文件,真正调用设备商提供的dll文件的其实就是这个第二方dll文件.到这里,解决问题的思路已经产生了,大慨分可以分为三步:
       1>编写一个java类,这个类包含的方法是按照设备商提供的.h文件经过变形/转换处理过的,并且必须使用native定义.这个地方需要注意的问题是java程序中定义的方法不必追求和厂商提供的头文件列出的方法清单中的方法具有相同的名字/返回值/参数,因为一些参数类型如指针等在java中没法模拟,只要能保证这个方法能实现原dll文件中的方法提供的功能就行了;
       2>按JNI的规则使用c/c++语言编写一个dll程序;
       3>按dll调用dll的规则在自己编写的dll程序里面调用厂商提供的dll程序中定义的方法.

       我之前为了给一个java项目添加IC卡读写功能,曾经查了很多资料发现查到的资料都是只说到第二步,所以剩下的就只好自己动手研究了.下面结合具体的代码来按这三个步骤分析.

     1>假设厂商提供的.h文件中定义了一个我们需要的方法:
      __int16 __stdcall readData( HANDLE icdev, __int16 offset, __int16 len, unsigned char *data_buffer );
      a.__int16定义了一个不依赖于具体的硬件和软件环境,在任何环境下都占16 bit的整型数据(java中的int类型是32 bit),这个数据类型是vc++中特定的数据类型,所以我自己做的dll也是用的vc++来编译.
     b.__stdcall表示这个函数可以被其它程序调用,vc++编译的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式.c/c++语言默认的调用方式是__cdecl,所以在自己做可被java程序调用的dll时一定要加上__stdcall的声明,否则在java程序执行时会报类型不匹配的错误.
     c.HANDLE icdev是windows操作系统中的一个概念,属于win32的一种数据类型,代表一个核心对象在某一个进程中的唯一索引,不是指针,在知道这个索引代表的对象类型时可以强制转换成此类型的数据.
    这些知识都属于win32编程的范围,更为详细的win32资料可以查阅相关的文档.
    这个方法的原始含义是通过设备初始时产生的设备标志号icdev,读取从某字符串在内存空间中的相对超始位置offset开始的共len个字符,并存放到data_buffer指向的无符号字符类型的内存空间中,并返回一个16 bit的整型值来标志这次的读设备是否成功,这里真正需要的是unsigned char *这个指针指向的地址存放的数据,而java中没有指针类型,所以可以考虑定义一个返回字符串类型的java方法,原方法中返回的整型值也可以按经过一定的规则处理按字符串类型传出,由于HANDLE是一个类型于java中的Ojbect类型的数据,可以把它当作int类型处理,这样java程序中的方法定义就已经形成了:
    String readData( int icdev, int offset, int len );
    声明这个方法的时候要加上native关键字,表明这是一个与本地方法通信的java方法,同时为了安全起见,此文方法要对其它类隐藏,使用private声明,再另外写一个public方法去调用它,同时要在这个类中把本地文件加载进来,最终的代码如下:

package test;

public class LinkDll
{
    //从指定地址读数据
    private native String readData( int icdev, int offset, int len );
    public String readData( int icdev, int offset, int len )
    {
        return this.readDataTemp( icdev, offset, len );
    }

    static  
    {         
        System.loadLibrary( "TestDll" );//如果执行环境是linux这里加载的是SO文件,如果是windows环境这里加载的是dll文件
    }
}

2>使用JDK的javah命令为这个类生成一个包含类中的方法定义的.h文件,可进入到class文件包的根目录下(只要是在 classpath参数中的路径即可),使用javah命令的时候要加上包名javah test.LinkDll,命令成功后生成一个名为test_LinkDll.h的头文件.
    文件内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated*/
#include <jni.h>

/* Header for class test_LinkDll */
#ifndef _Included_test_LinkDll #define

Included_test_LinkDll
#ifdef __cplusplus extern "C" { #endif
/*
* Class:     test_LinkDll
* Method:    readDataTemp
* Signature: (III)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_test_LinkDll_readDataTemp(JNIEnv *, jobject, jint, jint, jint);
#ifdef __cplusplus } #endif
#endif

    可以看出,JNI为了实现和dll文件的通信,已经按它的标准对方法名/参数类型/参数数目作了一定的处理,其中的JNIEnv*/jobjtct这两个参数是每个JNI方法固有的参数,javah命令负责按JNI标准为每个java方法加上这两个参数.JNIEnv是指向类型为JNIEnv_的一个特殊JNI数据结构的指针,当由C++编译器编译时JNIEnv_结构其实被定义为一个类,这个类中定义了很多内嵌函数,通过使用"->"符号,可以很方便使用这些函数,如:
    (env)->NewString( jchar* c, jint len )
    可以从指针c指向的地址开始读取len个字符封装成一个JString类型的数据.
    其中的jchar对应于c/c++中的char,jint对应于c/c++中的len,JString对应于java中的String,通过查看jni.h可以看到这些数据类型其实都是根据java和c/c++中的数据类型对应关系使用typedef关键字重新定义的基本数据类型或结构体.
    具体的对应关系如下:
Java类型     本地类型            描述
boolean       jboolean             C/C++8位整型
byte             jbyte                   C/C++带符号的8位整型
char             jchar                   C/C++无符号的16位整型
short            jshort                  C/C++带符号的16位整型
int                 jint                      C/C++带符号的32位整型
long             jlong                   C/C++带符号的64位整型e
float             jfloat                   C/C++32位浮点型
double        jdouble               C/C++64位浮点型
Object          jobject                任何Java对象,或者没有对应java类型的对象
Class         jclass                  Class对象
String          jstring                 字符串对象
Object[]      jobjectArray         任何对象的数组
boolean[]    jbooleanArray     布尔型数组
byte[]          jbyteArray           比特型数组
char[]           jcharArray            字符型数组
short[]          jshortArray           短整型数组
int[]             jintArray               整型数组
long[]          jlongArray            长整型数组
float[]         jfloatArray             浮点型数组
double[]     jdoubleArray        双浮点型数组
    更为详细的资料可以查阅JNI文档.
    需要注意的问题:test_LinkDll.h文件包含了jni.h文件;

3>使用vc++ 6.0编写TestDll.dll文件,这个文件名是和java类中loadLibrary的名称一致.
a>使用vc++6.0 新建一个Win32 Dynamic-Link Library的工程文件,工程名指定为TestDll
b>把源代码文件和头文件使用"Add Fiels to Project"菜单加载到工程中,若使用c来编码,源码文件后缀名为.c,若使用c++来编码,源码文件扩展名为.cpp,这个一定要搞清楚,因为对于不同的语言,使用JNIEnv指针的方式是不同的.
c>在这个文件里调用设备商提供的dll文件,设备商一般提供三种文件:dll/lib/h,这里假设分别为A.dll/A.lib/A.h.
这个地方的调用分为动态调用和静态调用静态调用即是只要把被调用的dll文件放到path路径下,然后加载lib链接文件和.h头文件即可直接调用A.dll中的方法:
把设备商提供的A.h文件使用"Add Fiels to Project"菜单加载到这个工程中,同时在源代码文件中要把这个A.h文件使用include包含进来;
然后依次点击"Project->settings"菜单,打开link选项卡,把A.lib添加到"Object/library modules"选项中.
具体的代码如下:
//读出数据,需要注意的是如果是c程序在调用JNI函数时必须在JNIEnv的变量名前加*,如(*env)->xxx,如果是c++程序,则直接使用(env)->xxx

#include<WINDOWS.H>
#include<MALLOC.H>
#include<STDIO.H>
#include<jni.h>
#include "test_LinkDll.h"
#include "A.h"

JNIEXPORT jstring JNICALL Java_test_LinkDll_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_len )
{
    //*************************基本数据声明与定义******************************
     HANDLE H_icdev = (HANDLE)ji_icdev;//设备标志符
    __int16 i16_len = (__int16)ji_len;//读出的数据长度,值为3,即3个HEX形式的字符
    __int16 i16_result;//函数返回值
    __int16 i16_coverResult;//字符转换函数的返回值
        int i_temp;//用于循环的中间变量
      jchar jca_result[3] = { 'e', 'r', 'r' };//当读数据错误时返回此字符串

    //无符号字符指针,指向的内存空间用于存放读出的HEX形式的数据字符串
    unsigned char* uncp_hex_passward = (unsigned char*)malloc( i16_len );

    //无符号字符指针,指向的内存空间存放从HEX形式转换为ASC形式的数据字符串
    unsigned char* uncp_asc_passward = (unsigned char*)malloc( i16_len * 2 );

    //javachar指针,指向的内存空间存放从存放ASC形式数据字符串空间读出的数据字符串
    jchar *jcp_data = (jchar*)malloc(i16_len*2+1);

    //javaString,存放从java char数组生成的String字符串,并返回给调用者
    jstring js_data = 0;

    //*********读出3个HEX形式的数据字符到uncp_hex_data指定的内存空间**********
    i16_result = readData( H_icdev, 6, uncp_hex_data );//这里直接调用的是设备商提供的原型方法.

    if ( i16_result != 0 )
    {
        printf( "读卡错误....../n" );
        //这个地方调用JNI定义的方法NewString(jchar*,jint),把jchar字符串转换为JString类型数据,返回到java程序中即是String
        return (env)->NewString( jca_result, 3 );
    }

    printf( "读数据成功....../n" );

    //**************HEX形式的数据字符串转换为ASC形式的数据字符串**************
    i16_coverResult = hex_asc( uncp_hex_data, uncp_asc_data, 3 );
    if ( i16_coverResult != 0 )
    {
        printf( "字符转换错误!/n" );
        return (env)->NewString( jca_result, 3 );
    }

    //**********ASC char形式的数据字符串转换为jchar形式的数据字符串***********
    for ( i_temp = 0; i_temp < i16_len; i_temp++ ) 
        jcp_data[i_temp] = uncp_hex_data[i_temp];

    //******************jchar形式的数据字符串转换为java String****************
    js_data = (env)->NewString(jcp_data,i16_len); 

    return js_data;
}

动态调用,不需要lib文件,直接加载A.dll文件,并把其中的文件再次声明,代码如下:
#include<STDIO.H>
#include<WINDOWS.H>
#include "test_LinkDll.h"

//首先声明一个临时方法,这个方法名可以随意定义,但参数同设备商提供的原型方法的参数保持一致.
typedef int ( *readDataTemp )( int, int, int, unsigned char * );//从指定地址读数据

//从指定地址读数据
JNIEXPORT jstring JNICALL Java_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_offset, jint ji_len )
{
    int i_temp;
    int i_result;
    int i_icdev = (int)ji_icdev;
    int i_offset = (int)ji_offset;
    int i_len = (int)ji_len;
    jchar jca_result[5] = { 'e', 'r', 'r' };
    unsigned char *uncp_data = (unsigned char*)malloc(i_len);
    jchar *jcp_data = (jchar *)malloc(i_len);
    jstring js_data = 0;

    //HINSTANCE是win32中同HANDLE类似的一种数据类型,意为Handle to an instance,常用来标记App实例,在这个地方首先把A.dll加载到内存空间,以一个App的形式存放,然后取

得它的instance交给dllhandle,以备其它资源使用.
    HINSTANCE dllhandle;
    dllhandle = LoadLibrary( "A.dll" );
   
    //这个地方首先定义一个已声明过的临时方法,此临时方法相当于一个结构体,它和设备商提供的原型方法具有相同的参数结构,可互相转换
    readDataTemp readData;

    //使用win32的GetProcAddress方法取得A.dll中定义的名为readData的方法,并把这个方法转换为已被定义好的同结构的临时方法,
    //然后在下面的程序中,就可以使用这个临时方法了,使用这个临时方法在这时等同于使用A.dll中的原型方法.
    readData = (readDataTemp) GetProcAddress( dllhandle, "readData" );

    i_result = (*readData)( i_icdev, i_offset, i_len, uncp_data );

    if ( i_result != 0 )
    {
        printf( "读数据失败....../n" );
        return (env)->NewString( jca_result, 3 );
    }

    for ( i_temp = 0; i_temp < i_len; i_temp++ )
    {
        jcp_data[i_temp] = uncp_data[i_temp];
    }

    js_data = (env)->NewString( jcp_data, i_len );

    return js_data;
}

4>以上即是一个java程序调用第三方dll文件的完整过程,当然,在整个过程的工作全部完成以后,就可以使用java类LinkDll中的public String radData( int, int, int )方法了,效果同直接使用c/c++调用这个设备商提供的A.dll文件中的readData方法几乎一样.

总结:JNI技术确实是提高了java程序的执行效率,并且扩展了java程序的功能,但它也确确实实破坏了java程序的最重要的优点:平台无关性,所以除非必须(不得不)使用JNI技术,一般还是提倡写100%纯java的程序.根据自己的经验及查阅的一些资料,把可以使用JNI技术的情况罗列如下:
    1>需要直接操作物理设备,而没有相关的驱动程序,这时候我们可能需要用C甚至汇编语言来编写该设备的驱动,然后通过JNI调用;
    2>涉及大量数学运算的部分,用java会带来些效率上的损失;
    3>用java会产生系统难以支付的开销,如需要大量网络链接的场合;
    4>存在大量可重用的c/c++代码,通过JNI可以减少开发工作量,避免重复开发.
另外,在利用JNI技术的时候要注意以下几点:
    1>由于Java安全机制的限制,不要试图通过Jar文件的方式发布包含本地化方法的Applet到客户端;
    2>注意内存管理问题,虽然在本地方法返回Java后将自动释放局部引用,但过多的局部引用将使虚拟机在执行本地方法时耗尽内存;
    3>JNI技术不仅可以让java程序调用c/c++代码,也可以让c/c++代码调用java代码.

注:有一个名叫Jawin开源项目实现了直接读取第三方dll文件,不用自己辛苦去手写一个起传值转换作用的dll文件,有兴趣的可以研究一下.但 是我用的时候不太顺手,有很多规则限制,像自己写程序时可以随意定义返回值,随意转换类型,用这个包的话这些都是不可能的了,所以我的项目还没开始就把它 抛弃了.

Chap9:如何编写jni方法(转载)

作者:crazycow 发布时间:2008-11-16 14:44:21.0

http://blog.chinaunix.net/u1/38994/showart_1099528.html
一、概述:

在 这篇文章中将会简单介绍如何编制一些简单的JNI 方法。我们都知道JNI方法可以帮助我们调用用C/c++编写的函数,这样如果一项工作已经用C /c++语言实现的话,我们就可以不用花很大的力气再用JAVA语言对这一工作进行再实现,只要编制相应的JNI函数,就可以轻松实现JAVA语言对C /c++函数的调用,从而大大减轻程序开发人员的工作量。

在这个项目中,我们编制了很多小实例,通过阅读,运行这些小实例,你可以轻松的学会如何编制JNI方法。这篇文档可以帮助你更好的理解及实现这些实例。

现 在让我们进入主题。首先,我们看一下这个项目的体系构架。该项目分为两部分,一部分用c语言是c语言的例子,另一部分是c++语言的例子。每部分都包含 java,src源文件目录,以及一个Makefile文件。java目录中是需要调用JNI函数的JAVA源程序,含有后缀名.java。src 目录 中含有JNI函数的实现代码,包括.c或.cpp文件和.h文件。Makefile文件是对 java 、src 目录下的文件进行编译组织进而生成可执 行文件的文件。当Makefile文件执行以后还会生成以下子目录:lib , class ,bin目录 。lib 目录中包含项目中生成的静态函数库 文件libJNIExamples.so,java程序所调用的JNI方法都是通过这个库来调用的。class 目录中包含由java目录下 的.java 文件生成的.class文件。bin目录中是一个可执行的shell脚本文件。在执行该脚本的时候,项目所有程序实例的运行结果都将一并显 示在屏幕上。


具体执行步骤为:

make

cd bin

./run.sh


下面来介绍一下在这个项目中所实现的实例:

   1. 如何调用标准C/c++中的函数--例如:printf(...)
   2. 如何调用C/c++中自定义的函数
   3. 如何在jni函数中访问java类中的对象实例域
   4. 如何在jni函数中访问java类中的静态实例域
   5. 如何在jni函数中调用java对象的方法
   6. 如何在jni函数中调用java类的静态方法
   7. 如何在jni函数中传递基本数据类型参数
   8. 如何在jni函数中传递对象类型参数
   9. 如何在jni函数中处理字符串
  10. 如何在jni函数中处理数组
  11. 处理jni函数中的返回值情况
  12. 在jni中实现创建java类对象


二、基本步骤:

在介绍这些例子之前,让我们先来看看编写jni方法所需要的基本步骤,这些实例都是用c来实例来讲解,至于c++的实例和c的实例区别不大,只要作稍微的修改即可,在文档的末尾我们将介绍这些内容:

1、要想定义jni方法,首先得要在java语言中对这一方法进行声明(自然这一声明过程要在类中进行)

声明格式如下:

publicnativevoid print();   System.loadLibrary(“JNIExamples”);   } 


jni 函数用关键字native方法声明。

2、对该类的源文件进行编译使用javac命令,生成相应的.class文件。
3、用javah -jni为函数生成一个在java调用和实际的c函数之间的转换存根,该存根通过从虚拟机栈中取出参数信息,并将其传递给已编译的C函数来实现转换。
4、建立一个特殊的共享库,并从该共享库到处这个存根,在上面的例子中使用了System.loadLibrary,来加载libJNIExamples共享库。


三、配置运行环境:

在 编写一个简单的jni函数之前我们必须配置相应的运行环境。jdk的配置在这里就不作介绍,这里主要说的是库的路径。当调用 System.loadLibrary(..)时,编译器会到我们系统设置的库路径中寻找该库。修改路径的方法和修改任何环境变量的方法基本相同,只要在 /etc/bash.bashrc目录下增加一行LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)即可。也可以通 过命令行export LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)


四、运行实例分析:

1、实例一:在jni中调用标准c中自带的函数printf():

下面以实例1为例来详细说明编写jni方法的详细过程。

(1)、定义包含jni函数的类Print.java:

/*********************************************************************** * the print() function will call the printf() funcion which is a ANSI c funciton * *************************************************************************/

Public native  void print();     

System.loadLibrary("JNIExamples");   

}  



在上面的实例 中,使用public native void print();语句来定义了一个Print类的jni方法。并用 Sysgem.loadLibrary(“JNIExamples”)语句来加载libJNIExamples.so库。注意:加载的语句一定要用 static关键字声明在静态块中,以保证引用该类时该库始终被加载。

(2)、对该类进行编译:javac Print.java。生成Print.class类,然后用javah 产生一个Print.h的头文件:javah Print。长生的Print.h文件格式如下:

/* DO NOT EDIT THIS FILE - it is machine generated *//* Header for class Print */  JNIEXPORT void JNICALL Java_Print_print     (JNIEnv *, jobject);   }  


其中的加粗字体为要实现的JNI函数生命部分。

(3)、编写JNI函数的实现部分Print.c

JNIEXPORT void JNICALL Java_Print_print (JNIEnv *env, jobject obj)  

{    

printf("example1:in this example a printf() function in ANSI C is called/n");    

printf("Hello,the output is generated by printf() function in ANSI C/n"); 


在这个文件中实现了一个简单的Jni方法。该方法调用ANSI C 中的printf()函数,输出了两个句子。

(4)、将本地函数编译到libJNIExamples.so的库中:
使用语句:gcc -fPIC -I/usr/jdk1.5/include -I/usr/jdk1.5/include/linux -shared -o libJNIExamples.so Print.c。

(5)、至此Jni函数已全部实现,可以在java代码中调用拉。
在此我们使用一个简单的类来对实现的jni方法进行测试,下面是PrintTest.java的源代码部分:

publicstaticvoid main(String[] args) {       Print p = new Print();       p.print();     }   } 


(6)、对PrintTest.java进行编译执行得到如下结果:
example1:in this example a printf() function in ANSI C is called
Hello,the output is generated by printf() function in ANSI C .

下面介绍的每个实例实现的步骤也都是按着上述步骤执行的。所以介绍时只介绍实现的关键部分。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

2、实例二、调用c语言用户定义的函数

(源程序为:java/Cfunction.java java/C_functionTest.java src/Cfunction.c src/Cfunction.h )
当需要在java程序中调用用c所实现的函数是,需要在需要调用该c函数的类中定义一个jni方法,在该jni方法中去调用该c函数,相当于用java方法把c函数封装起来,以供java程序调用。
在实例二中我们简单定义了一个printHello()函数,该函数的功能只是输出一句话,如果要在java程序中调用该函数,只需在jni函数中调用即可,和调用ANSI C中自带的prinf()函数没有任何区别。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

3、实例三、在jni函数中访问java类中的对象实例域

(源程序为:java/CommonField.java java/CommonFieldTest.java src/CommonField.c src/CommonField.h )
jni函数的实现部分是在c 语言中实现的,如果它想访问java中定义的类对象的实例域需要作三步工作,

(1)调用GetObjectClass()函数得到该对像的类,该函数返回一个jclass类型值。

(2)调用GetFieldID()函数得到要访问的实例域在该类中的id。
(3)调用GetXXXField()来得到要访问的实例域的值。其中XXX和要访问的实例域的类型相对应。
在jni中java 编程语言和c 语言数据类型的对应关系为java原始数据类型前加 'j' 表示对应c语言的数据类型例如boolean 为jboolean ,int 为 jint,double 为jdouble等。对象类型的对应类型为jobject。
在 本实例中,您可以看到我们在java/CommonField.java 中定义了类CommonField类,其中包含int a , int b 两 个实例域,我们要在jni函数getCommonField()中对这两个域进行访问和修改。你可以在 src/CommonField.c中找到该函数 的实现部分。
以下语句是对该域的访问(以下代码摘自:src/CommonField.c):

jclass class_Field = (*env)->GetObjectClass(env,obj);  

jfieldID fdA = (*env)->GetFieldID(env,class_Field,"a","I"); 

jfieldID fdB = (*env)->GetFieldID(env,class_Field,"b","I");  

jint valueA = (*env)->GetIntField(env,obj,fdA); 

jint valueB = (*env)->GetIntField(env,obj,fdB); 



在jni 中对所有jni函数的调用都要用到env指针,该指针也是每一个本地方法的第一个参数,他是函数指针表的指针,所以,必须在每一个jni调用前面加上(*env)->GetObjectClass(env,obj)函数调用返回obj对像的类型,其中obj 参数表示要你想要得到类型的类对象。
jfieldID GetFieldID(JNIEnv *env,jclass cl, const char name[], const char sig[]) 该 函数返回一个域的标识符name 表示域名,sig表示编码的域签名。所谓编码的签名即编码类型的签名在上例中类中的a实例域为int 型,用"I”来表 示,同理"B” 表示byte ,"C” 表示 char , “D”表示 double ,”F” 表示float,“J”表示long, “S” 表 示short , “V” 表示void ,”Z”表示 boolean类型。
GetIntField(env,obj,fdA),用来访问obj对象的fdA域,如果要访问的域为double类型,则要使用GetDoubleField(env,obj,fdA)来访问,即类型对应GetXXXField中的XXX。

以下函数用来修改域的值:

(*env)->SetIntField(env,obj,fdA,109);   (*env)->SetIntField(env,obj,fdB,145); 


这和获得域的值类似,只是该函数多了一个要设置给该域的值参数。
访问对象实例域的相关函数如下:
jfieldID GetFieldID(JNIEnv *env, jclass cl, const char name[], const char sig[])
该函数返回一个域的标识符。各参数含义如下:
env JNI 接口指针;cl 类对象 ; name 域名; sig 编码的域签名

XXX GetXXXField(JNIEnv *env, jobject obj, jfieldID id)
该函数返回域的值。域类型XXX是Object, Boolean, byte, char , short, int ,long ,float, double 中类型之一。
参数 env JNI借口指针;obj为域所在对象;id为域的标识符。
void SetXXXField(JNIEnv *env,jobject obj, jfieldID id, XXX value)

该函数用于设置域的值。XXX的含义同上,
参数中env, obj , id 的含义也同上,value 值为将要设置的值。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

4、实例四:在jni函数中访问类的静态实例域

(java/Field.java java/FieldTest.java src/Field.c src/Field.h) 

因为静态实例域并不属于某个对象,而是属于一个类,所以在要访问静态实例域时,和访问对象的实例域不同,它所调用的函数是(以实例四来说明,一下代码摘自src/Field.c):

jclass class_Field = (*env)->FindClass(env,"Field"); 

jfieldID fdA = (*env)->GetStaticFieldID(env,class_Field,"a","I"); 

jint valueA = (*env)->GetStaticIntField(env,class_Field,fdA); 

(*env)->SetStaticIntField(env,class_Field,fdA,111); 

由于没有 对象,必须使用FindClass代替GetObjectClass来获得类引用。在FindClass()的第二个参数是类的编码签名,类的编码签名和基本类型的编码签名有所不同,如果类在当前包中,就直接是类的名称,如果类不在当前包中则要加入该类的详细路径:例如String类在java.lang 包中,则String的签名要写成( Ljava/lang/String;),其中的(L和;)是不可少的,其中(;)是表达是的终止符。其他三个函数和访问对象数据域基本没什么区别。

5、实例五:在jni函数中调用java对象的方法

(java/CommonMethod.java java/CommonMethodTest.java src/CommonMehod.c src/CommonMethod.h )

在jni函数中我们不仅要对java对象的数据域进行访问,而且有时也需要调用java中类对象已经实现的方法,实例五就是关于这方面的实现的。在src/CommonMethod.c中我们可以找到下面的代码:

JNIEXPORT void JNICALL Java_CommonMethod_callMethod (

JNIEnv *env, jobject obj, jint a, jstring s)  

{    

printf("example 5:in this example,a object's method will be called/n");    

jclass class_CommonMethod = (*env)->GetObjectClass(env,obj);   

   jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print",

"(ILjava/lang/String;)V");   

(*env)->CallVoidMethod(env,obj,md,a,s); 

该代码部分展示了如何实现对java类对象函数的调用过程。从以上代码部分我们可以看到,要实现该调用需要有三个步骤,调用三个函数

jclass class_CommonMethod = (*env)->GetObjectClass(env,obj); 

jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print","(ILjava/lang/String;)V"); 

(*env)->CallVoidMethod(env,obj,md,a,s); 


GetObjectClass(...)函数获得要调用对象的类;GetMethodID(...)获得要调用的方法相对于该类的ID号;CallXXXMethod(...)调用该方法。

在 编写该调用过程的时候,需要注意的仍然是GetMethodID(...)函数中编码签名的问题,在该实例中,我们要做的是找到CommonMethod 类的print(int a, String s)方法,该方法打印整数a,和字符串s 的直。在函数的编码签名部分(该部分以加粗、并加有下划 线)GetMethodID(env,class_CommonMethod,"print","(ILjava/lang /String;)V"); 从左往右可以查看,括号中的内容为要调用方法的参数部分内容,I表示第一个参数为int类型,“Ljava/lang /String;”表示第二个参数为String类型,V表示返回值类型为空void,如果返回值类型不为空,则使用相应的类型签名。返回值类型是和下面将要使用的调用该方法的函数CallXXXMethod(...)相关联的,该函数的xxx要用相应的类型来替换,在此实例中为void,如果返回值类型 为int类型则调用该方法的函数就为CallIntMethod(...)。

6、实例六:在jni函数中调用java类的静态方法

(java/Method.java java/MethodTest.java src/Method.h src/Method.c)

实例五中介绍了如何调用类对象的方法,在此实例中我们将介绍如何调用java类的静态方法在此实例中我们在/java/Method.java中定义了静态方法:

public static void print() { 

  System.out.println("this is a static method of class Method");

该函数的功能就是打印字符串“ this is a static method of class Method”;
我们在src/Method.c中实现了对该方法调用的jni函数:

JNIEXPORT void JNICALL Java_Method_callMethod (JNIEnv *env, jobject obj)  

{    

printf("example 6:in this example, the class's static method will be called/n");   

jclass class_Method = (*env)->FindClass(env,"Method");   

jmethodID md = (*env)->GetStaticMethodID(env,class_Method,"print","()V");   

(*env)->CallStaticVoidMethod(env,class_Method,md);   } 



和实例五不同的是,我们要调用的三个函数变为:
FindClass(...)、GetStaticMethodID(...)、CallStaticVoidMethod(...)。
其中的机制和实例五是一样的。再次就不做过多的介绍。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

7、实例七:jni函数中传递基本数据类型参数

(java/Basic.java java/BasicTest.java src/Basic.c src/Basic.h) 在java/Basic.java中,我们定义了一个public native void raiseValue(int a)函数,该函数将打印使value的值增加a,并打印原来的value和新的value值。
在src/Basic.c中给出了该jni函数的实现部分。

JNIEXPORT void JNICALL Java_Basic_raiseValue (

JNIEnv *env, jobject obj, jint a) 

{    

printf("example 7: in this example, a integer type parament will be passed to the jni method/n");   

jclass class_Basic = (*env)->GetObjectClass(env,obj);   

jfieldID fd = (*env)->GetFieldID(env,class_Basic,"value","I");    

jint v = (*env)->GetIntField(env,obj,fd);    

v = v+a;   

(*env)->SetIntField(env,obj,fd,v);  


在此函数实现中,因为要访问Basic类中的value域,所以调用了GetObjectClass(...), GetFieldID(...), GetIntField(...)函数获取value值,下面一步 的 “ = v+a; ”说明,传递基本类型参数的处理方式和在c语言中的基本数据类型的处理无异。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

8、实例八:在jni函数中传递对象类型参数

(java/Book.java java/BookTest.java src/BookTest.c src/BookTest.h)

  在该实例中演示了在jni函数中传递对象函数的过程。

  我们在该实例中定义了一个类Book

total_page = t;    

}

  publicint getTotalPage() {     }  

publicint getCurrentPage() {     }     

current_page++;   


然后我们在java/BookTest.java中定义jni函数

public native void bookCurrentStatus(Book b);

该函数需要一个Book类型的参数,并返回该参数的当前状态,包括该书一共有多少页的total_page,以及当前页current_page。函数的实现部分为(src/BookTest.c)

JNIEXPORT void JNICALL Java_BookTest_bookCurrentStatus (JNIEnv *env,

jobject this_obj, jobject obj)  

{   

printf("example 8: in this example, a object parament will be passed to the jni method。/n");   

jclass class_book = (*env)->GetObjectClass(env,obj);   

jmethodID id_getTotal = (*env)->GetMethodID(env,

class_book,"getTotalPage","()I"); 

jmethodID id_getCurrent = (*env)->GetMethodID(env,

class_book,"getCurrentPage","()I");  

  jint total_page = (*env)->CallIntMethod(env,

obj,id_getTotal);   

jint current_page = (*env)->CallIntMethod(env,

obj,id_getCurrent);    

printf("the total page is:%d and the current page is :%d/n",

total_page,current_page); 

该函数包含三个参数(JNIEnv *env, jobject this_obj, jobject obj) ,第二 个jobject this_obj参数表示当前的jni 函数所属于的类对象,第三个jobject obj参数表示传递的参数Book类型的类对象。

对于实现部分,基本和实例五--调用java类对象的方法中的操作相同,就不作详解。

9、实例九:在jni函数中处理字符串

(java/Str.java java/StrTest.java src/Str.c src/Str.h)
在该实例中我们讲解如何传递、处理字符串参数。
在java/Str.java中我们定义了一个 printString(String s) 的方法,用来处理字符串参数。
在src/Str.c中我们可以看到该函数的实现部分:

JNIEXPORT void JNICALL Java_Str_printString (JNIEnv *env,

jobject obj, jstring s)  

{    

printf("example 9: in this example, a String object parament will be passed to the jni method./n");   

const char* string = (char*)(*env)->GetStringUTFChars(env,s,NULL);    

printf("%s is put out in native method/n",string); 

(*env)->ReleaseStringUTFChars(env,s,(jbyte*)string);  


实现过程中调用了两个函数:GetStringUTFChars(...)、 ReleaseStringUTFChars(...)。
GetStringUTFChars(...) 用来获取String对象的字符串,并将其抓那还为char*类型,这应该字符串就可以在c语言中进行处理拉。 ReleaseStringUTFChars(...)用于当该字符串使用完成后,将其进行垃圾回收。记住,当使用完字符串时一定不要忘记调用该函数。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

10、实例十:在jni函数中处理数组

(java/Arr.java java/ArrTest.java src/Arr.c src/Arr.h)

java中所有的数组类型都有相对应的c语言类型,其中jarray类型表示一个泛型数组

boolean[] --jbooleanArray

byte[]--jbyteArray

char[]--jcharArary

int[]---jcharArray

short[]---jshortArray

long[]---jlongArray

float[]--jfloatArray

double[]—-jdoubleArray

Object[]--- jobjectArray。

当访问数组时,可以通过GetObjectAraryElement和SetObjectArrayElement方法访问对象数组的元素。

而 对于一般类型数组,你可以调用GetXXXAraryElements来获取一个只想数组起始元素的指针,而当你不在使用该数组时,要记得调用 ReleaseXXXArrayElements,这样你所作的改变才能保证在原始数组里得到反映。当然如果你需要得到数组的长度,可以调用 GetArrayLength函数。

在本实例中,我们在Arr.java中定义一个本地方法:print(int intArry[]),该函数的功能为对该数组进行输出,在src/Arr.c中我们可以看到该方法的实现过程如下:

JNIEXPORT void JNICALL Java_Arr_print (JNIEnv *env,

jobject obj, jintArray intArray)  

{    

printf("example 10:in this example, a array parament will be passed to the jni method./n");    

jint* arr = (*env)->GetIntArrayElements(env,intArray,NULL);  

n = (*env)->GetArrayLength(env,intArray);  

printf("the native method output the int array/n");  

for( i = 0;i<(*env)->GetArrayLength(env,intArray);i++)   

{      

printf("%d ",arr[i]);    

}    

(*env)->ReleaseIntArrayElements(env,intArray,arr,0);  

我们在此调用了GetIntArrayElements(...)来获取一个指向intArray[]数组第一个元素的指针。
用getArrayLength(..)函数来得到数组的长度,以方便数组遍历时使用。最后应用ReleaseArrayElements(...)函数来释放该数组指针。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

11、实例十一:在jni中的返回值问题

(java/ReturnValue.java java/ReturnValueTest.java java/BookClass.java src/ReturnValue.c src/ReturnValue.h)

在java/ReturnValue类中定义了三个jni方法: returnInt(),returnString() ,returnObject()
三个方法,分别返回int , String , Object 类型的值。

其在src/ReturnValue.c中的实现分别为:

JNIEXPORT jint JNICALL Java_ReturnValue_returnInt   (

JNIEnv *env, jobject obj)  

{   

jclass  class_ReturnValue = (*env)->GetObjectClass(env,obj);    

jfieldID  fd = (*env)->GetFieldID(env,class_ReturnValue,"value","I");   

jint  v = (*env)->GetIntField(env,obj,fd); 

return v;

}    

* Signature: ()Ljava/lang/String;

JNIEXPORT jstring JNICALL Java_ReturnValue_returnString   (

JNIEnv *env, jobject obj)  

{

printf("example 11: in this example, the int and object of return value will be proceeding/n");

jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);  

  jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"name","Ljava/lang/String;");  

  jstring jstr = (jstring)(*env)->GetObjectField(env,obj,fd);

}  

* * Method: returnObject

JNIEXPORT jobject JNICALL Java_ReturnValue_returnObject   (

JNIEnv *env, jobject obj)  

{  

    jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);   

jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"myBook","LBookClass;");   

jobject  jbook = (jstring)(*env)->GetObjectField(env,obj,fd);  


在这里分别涉及到了对java类对象的一般参数,String参数,以及Object参数的访问。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

12、实例十二:在jni中创建java类对象:

(java/Test.java src/CreateObj.c src/CreateObj.h)


如果想要在jni函数创建java类对象则要引用java 类的构造器方法,通过调用NewObject函数来实现。

NewObject函数的调用方式为:

jobject obj_new = (*env)->NewObject(env,class, methodid, paraments);
在该实例中,我们在java/Test.java 中定义了Book1类,要在CreateObj类的modifyProperty() jni方法中创建该类对象。我们可以在src/CreateObj.c中看到该jni方法创建对象的过程:

jobject     book; 

jclass      class_book;  

jmethodID  md_book;  

class_book = (*env)->FindClass(env,"LBook1;");  

md_book = (*env)->GetMethodID(env,class_book,"<init>","(IILjava/lang/String;)V"); 

book = (*env)->NewObject(env,class_book,md_book,100,1,"huanghe"); 

在 创建对象的过程中可以看到,要创建一个java类对象,首先需要得到得到使用FindClass函数得到该类,然后使用GetMethodID方法得到该 类的构造器方法id,主义在此时构造器的函数名始终为:"”,其后函数的签名要符合函数签名规则。在此我们的构造器有三个参数:int , int, String.

并且其返回值类型要永久为空,所以函数签名为:"(IILjava/lang/String;)V"

然后我们调用NewObject()函数来创建该类的对象,在此之后就可以使用该对象拉。

以上内容介绍的是jni函数c语言的实现实例。如果想要使用c++的实例,我们只需要把其中的每一个函数调用过程作稍微的修改:

例如:(*env)->NewObject(env,class_book,md_book,100,1,”huanghe”);

修改为:(env)->NewObject(class_book,md_book,100,1,”huanghe”);

即修改(*env)为(env)再把参数中的env去掉。然后把所有c的函数改为c++的函数就OK拉。

具体情况可以去查看我们的c++实例代码.

Chap10:在 Windows 中实现 Java 本地方法

级别: 初级

David WendtWebSphere Development Research Triangle Park, NC

1999 年 5 月 01 日

本文为在 32 位 Windows 平台上实现 Java 本地方法提供了实用的示例、步骤和准则。这些示例包括传递和返回常用的数据类型。

本文中的示例使用 Sun Microsystems 公司创建的 Java DevelopmentKit (JDK) 版本 1.1.6和 Java本地接口 (JNI)规范。 用 C 语言编写的本地代码是用 MicrosoftVisual C++ 编译器编译生成的。

简介

本文提供调用本地 C 代码的 Java 代码示例,包括传递和返回某些常用的数据类型。本地方法包含在特定于平台的可执行文件中。就本文中的示例而言,本地方法包含在 Windows 32 位动态链接库 (DLL) 中。

不过我要提醒您,对 Java 外部的调用通常不能移植到其他平台上,在 applet 中还可能引发安全异常。实现本地代码将使您的 Java 应用程序无法通过 100% 纯 Java 测试。但是,如果必须执行本地调用,则要考虑几个准则:

  1. 将您的所有本地方法都封装在单个类中,这个类调用单个 DLL。对于每种目标操作系统,都可以用特定于适当平台的版本替换这个 DLL。这样就可以将本地代码的影响减至最小,并有助于将以后所需的移植问题包含在内。
  2. 本地方法要简单。尽量将您的 DLL 对任何第三方(包括 Microsoft)运行时 DLL 的依赖减到最小。使您的本地方法尽量独立,以将加载您的 DLL 和应用程序所需的开销减到最小。如果需要运行时 DLL,必须随应用程序一起提供它们。

,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

1.Java 调用 C

对于调用 C 函数的 Java 方法,必须在Java 类中声明一个本地方法。在本部分的所有示例中,我们将创建一个名为 MyNative 的类,并逐步在其中加入新的功能。这强调了一种思想,即将本地方法集中在单个类中,以便将以后所需的移植工作减到最少。

示例 1 -- 传递参数

在第一个示例中,我们将三个常用参数类型传递给本地函数: String intboolean 。本例说明在本地C 代码中如何引用这些参数

public class MyNative

{

  public void  showParms( String s, int i, boolean b )

  {

    showParms0(  s, i , b );

  }

  private native  void showParms0( String s, int i, boolean b );

  static

  {

     System.loadLibrary( "MyNative" );

  }

}

请注意,本地方法被声明为专用的,并创建了一个包装方法用于公用目的。这进一步将本地方法同代码的其余部分隔离开来,从而允许针对所需的平台对它进行优化。static子句加载包含本地方法实现的 DLL。

下一步是生成 C 代码来实现 showParms0 方法。此方法的 C 函数原型是通过对 .class 文件使用 javah 实用程序来创建的,而 .class 文件是通过编译 MyNative.java 文件生成的。这个实用程序可在 JDK 中找到。下面是 javah 的用法:

javac  MyNative.java(将 .java 编译为 .class)

javah -jni MyNative(生成 .h 文件) 

这将生成一个 MyNative.h 文件,其中包含一个本地方法原型,如下所示:

/*

* Class:     MyNative

* Method:    showParms0

* Signature:  (Ljava/lang/String;IZ)V

*/

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *,  jobject, jstring, jint, jboolean);

第一个参数是调用 JNI 方法时使用的 JNI Environment 指针。第二个参数是指向在此 Java 代码中实例化的 Java 对象 MyNative 的一个句柄。其他参数是方法本身的参数。请注意,MyNative.h 包括头文件 jni.h。jni.h 包含 JNI API 和变量类型(包括jobject、jstring、jint、jboolean,等等)的原型和其他声明。

本地方法是在文件 MyNative.c 中用 C 语言实现的:

#include <stdio.h>

#include "MyNative.h"

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *env,  jobject obj, jstring s, jint i, jboolean b)

{

  const char*  szStr = (*env)->GetStringUTFChars( env, s, 0 );

  printf(  "String = [%s]/n", szStr );

  printf(  "int = %d/n", i );

  printf(  "boolean = %s/n", (b==JNI_TRUE  ? "true" : "false") );

   (*env)->ReleaseStringUTFChars( env, s, szStr );

}

JNI API,GetStringUTFChars,用来根据 Java 字符串或jstring 参数创建 C 字符串。这是必需的,因为在本地代码中不能直接读取 Java 字符串,而必须将其转换为 C 字符串或 Unicode。有关转换 Java 字符串的详细信息,请参阅标题为 NLS Strings and JNI 的一篇论文。但是,jboolean 和jint 值可以直接使用。

MyNative.dll 是通过编译 C 源文件创建的。下面的编译语句使用Microsoft Visual C++ 编译器:

cl -Ic:/jdk1.1.6/include  -Ic:/jdk1.1.6/include/win32 -LD MyNative.c

       -FeMyNative.dll 

其中 c:/jdk1.1.6是 JDK 的安装路径。

MyNative.dll 已创建好,现在就可将其用于 MyNative 类了。
可以这样测试这个本地方法:在 MyNative 类中创建一个 main 方法来调用 showParms 方法,如下所示:

   public static  void main( String[] args )

   {

     MyNative obj  = new MyNative();

      obj.showParms( "Hello", 23, true );

      obj.showParms( "World", 34, false );

   }

当运行这个 Java 应用程序时,请确保 MyNative.dll 位于 Windows 的 PATH 环境变量所指定的路径中或当前目录下。当执行此 Java 程序时,如果未找到这个 DLL,您可能会看到以下的消息:

java  MyNative 

Can't find class  MyNative 

这是因为 static 子句无法加载这个 DLL,所以在初始化 MyNative 类时引发异常。Java 解释器处理这个异常,并报告一个一般错误,指出找不到这个类。
如果用 -verbose 命令行选项运行解释器,您将看到它因找不到这个 DLL 而加载 UnsatisfiedLinkError 异常。

如果此 Java 程序完成运行,就会输出以下内容:

java  MyNative 

String =  [Hello] 

int = 23

boolean =  true 

String =  [World] 

int

      = 34 

boolean = false 示例 2 -- 返回一个值

本例将说明如何在本地方法中实现返回代码。
将这个方法添加到 MyNative 类中,这个类现在变为以下形式:

public class MyNative

{

  public void  showParms( String s, int i, boolean b )

  {

    showParms0(  s, i , b );

  }

  public int  hypotenuse( int a, int b )

  {

    return  hyptenuse0( a, b );

  }

  private native  void showParms0( String s, int i, boolean b );

  private native  int  hypotenuse0( int a, int b );

  static

  {

     System.loadLibrary( "MyNative" );

  }

  /* 测试本地方法 */

  public static  void main( String[] args )

  {

    MyNative obj  = new MyNative();

     System.out.println( obj.hypotenuse(3,4) );

     System.out.println( obj.hypotenuse(9,12) );

  }

}

公用的 hypotenuse 方法调用本地方法 hypotenuse0 来根据传递的参数计算值,并将结果作为一个整数返回。这个新本地方法的原型是使用 javah 生成的。请注意,每次运行这个实用程序时,它将自动覆盖当前目录中的MyNative.h。按以下方式执行 javah:

javah -jni MyNative 

生成的 MyNative.h 现在包含 hypotenuse0 原型,如下所示:

/*

* Class:     MyNative

* Method:    hypotenuse0

* Signature:  (II)I

*/

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *,  jobject, jint, jint);

该方法是在 MyNative.c 源文件中实现的,如下所示:

#include <stdio.h>

#include <math.h>

#include "MyNative.h"

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *env,  jobject obj, jstring s, jint i, jboolean b)

{

  const char*  szStr = (*env)->GetStringUTFChars( env, s, 0 );

  printf(  "String = [%s]/n", szStr );

  printf(  "int = %d/n", i );

  printf(  "boolean = %s/n", (b==JNI_TRUE  ? "true" : "false") );

   (*env)->ReleaseStringUTFChars( env, s, szStr );

}

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *env,  jobject obj, jint a, jint b)

{

  int rtn =  (int)sqrt( (double)( (a*a) + (b*b) ) );

  return  (jint)rtn;

}

再次请注意,jint 和 int 值是可互换的。
使用相同的编译语句重新编译这个 DLL:

cl -Ic:/jdk1.1.6/include  -Ic:/jdk1.1.6/include/win32 -LD MyNative.c

       -FeMyNative.dll 

现在执行 java MyNative 将输出 5 和 15 作为斜边的值。

示例 3 -- 静态方法

您可能在上面的示例中已经注意到,实例化的 MyNative 对象是没必要的。实用方法通常不需要实际的对象,通常都将它们创建为静态方法。本例说明如何用一个静态方法实现上面的示例。更改 MyNative.java 中的方法签名,以使它们成为静态方法:

  public static  int hypotenuse( int a, int b )

  {

    return  hypotenuse0(a,b);

  }

  ...

  private static  native int  hypotenuse0( int a, int b  );

现在运行 javah 为 hypotenuse0创建一个新原型,生成的原型如下所示:

/*

* Class:     MyNative

* Method:    hypotenuse0

* Signature:  (II)I

*/

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *,  jclass, jint, jint);

C 源代码中的方法签名变了,但代码还保持原样:

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *env,  jclass cls, jint a, jint b)

{

  int rtn =  (int)sqrt( (double)( (a*a) + (b*b) ) );

  return  (jint)rtn;

}

本质上,jobject 参数已变为 jclass 参数。此参数是指向MyNative.class 的一个句柄。main 方法可更改为以下形式:

  public static  void main( String[] args )

  {

     System.out.println( MyNative.hypotenuse( 3, 4 ) );

     System.out.println( MyNative.hypotenuse( 9, 12 ) );

  }

因为方法是静态的,所以调用它不需要实例化 MyNative 对象。本文后面的示例将使用静态方法。

示例 4 -- 传递数组

本例说明如何传递数组参数。本例使用一个基本类型,boolean,并将更改数组元素。下一个示例将访问 String(非基本类型)数组。将下面的方法添加到 MyNative.java 源代码中:

  public static  void setArray( boolean[] ba )

  {

    for( int i=0;  i < ba.length; i++ )

      ba[i] =  true;

    setArray0( ba  );

  }

  ...

  private static  native void setArray0( boolean[] ba );

在本例中,布尔型数组被初始化为 true,本地方法将把特定的元素设置为 false。同时,在 Java 源代码中,我们可以更改 main 以使其包含测试代码:

    boolean[] ba  = new boolean[5];

     MyNative.setArray( ba );

    for( int i=0;  i < ba.length; i++ )

       System.out.println( ba[i] );

在编译源代码并执行 javah 以后,MyNative.h 头文件包含以下的原型:

/*

* Class:     MyNative

* Method:    setArray0

* Signature: ([Z)V

*/

JNIEXPORT void JNICALL Java_MyNative_setArray0

  (JNIEnv *,  jclass, jbooleanArray);

请注意,布尔型数组是作为单个名为jbooleanArray 的类型创建的。
基本类型有它们自已的数组类型,如 jintArray 和 jcharArray。
非基本类型的数组使用 jobjectArray 类型。下一个示例中包括一个 jobjectArray。这个布尔数组数组元素是通过 JNI 方法 GetBooleanArrayElements 来访问的。
针对每种基本类型都有等价的方法。这个本地方法是如下实现的:

JNIEXPORT void JNICALL Java_MyNative_setArray0

  (JNIEnv *env,  jclass cls, jbooleanArray ba)

{

  jboolean* pba =  (*env)->GetBooleanArrayElements( env, ba, 0 );

  jsize len =  (*env)->GetArrayLength(env, ba);

  int i=0;

  // 更改偶数数组元素

  for( i=0; i  < len; i+=2 )

    pba[i] = JNI_FALSE;

   (*env)->ReleaseBooleanArrayElements( env, ba, pba, 0 );

}

指向布尔型数组的指针可以使用GetBooleanArrayElements 获得。
数组大小可以用 GetArrayLength 方法获得。使用ReleaseBooleanArrayElements 方法释放数组。现在就可以读取和修改数组元素的值了。jsize 声明等价于 jint(要查看它的定义,请参阅 JDK 的 include 目录下的 jni.h 头文件)。

示例 5 -- 传递 Java String 数组

本例将通过最常用的非基本类型,Java String,说明如何访问非基本对象的数组。字符串数组被传递给本地方法,而本地方法只是将它们显示到控制台上。
MyNative 类定义中添加了以下几个方法:

  public static  void showStrings( String[] sa )

  {

    showStrings0(  sa );

  }

  private static  void showStrings0( String[] sa );

并在 main 方法中添加了两行进行测试:

  String[] sa =  new String[] { "Hello,", "world!", "JNI", "is",  "fun." };

   MyNative.showStrings( sa );

本地方法分别访问每个元素,其实现如下所示。

JNIEXPORT void JNICALL Java_MyNative_showStrings0

  (JNIEnv *env,  jclass cls, jobjectArray sa)

{

  int len =  (*env)->GetArrayLength( env, sa );

  int i=0;

  for( i=0; i  < len; i++ )

  {

    jobject obj =  (*env)->GetObjectArrayElement(env, sa, i);

    jstring str =  (jstring)obj;

    const char*  szStr = (*env)->GetStringUTFChars( env, str, 0 );

    printf(  "%s ", szStr );

     (*env)->ReleaseStringUTFChars( env, str, szStr );

  }

  printf(  "/n" );

}

数组元素可以通过 GetObjectArrayElement 访问。

在本例中,我们知道返回值是 jstring 类型,所以可以安全地将它从 jobject 类型转换为 jstring 类型。字符串是通过前面讨论过的方法打印的。有关在 Windows 中处理 Java 字符串的信息,请参阅标题为 NLS Strings and JNI的一篇论文。

示例 6 -- 返回 Java String 数组

最后一个示例说明如何在本地代码中创建一个字符串数组并将它返回给Java 调用者。MyNative.java 中添加了以下几个方法:

  public static  String[] getStrings()

  {

    return  getStrings0();

  }

  private static  native String[] getStrings0();

更改 main 以使 showStrings 将 getStrings 的输出显示出来:

   MyNative.showStrings( MyNative.getStrings() );

实现的本地方法返回五个字符串。

JNIEXPORT jobjectArray JNICALL  Java_MyNative_getStrings0

  (JNIEnv *env,  jclass cls)

{

  jstring      str;

  jobjectArray  args = 0;

  jsize        len = 5;

  char*        sa[] = { "Hello,",  "world!", "JNI",  "is", "fun" };

  int          i=0;

  args =  (*env)->NewObjectArray(env, len, (*env)->FindClass(env,  "java/lang/String"), 0);

  for( i=0; i  < len; i++ )

  {

    str =  (*env)->NewStringUTF( env, sa[i] );

     (*env)->SetObjectArrayElement(env, args, i, str);

  }

  return args;

}

字符串数组是通过调用NewObjectArray 创建的,同时传递了 String 类和数组长度两个参数。Java String 是使用 NewStringUTF 创建的。String元素是使用 SetObjectArrayElement 存入数组中的。

2.调试

现 在您已经为您的应用程序创建了一个本地 DLL,但在调试时还要牢记以下几点。如果使用 Java 调试器 java_g.exe,则还需要创建 DLL 的一个“调试”版本。这只是表示必须创建同名但带有一个 _g 后缀的 DLL 版本。就 MyNative.dll而言,使用 java_g.exe 要求在 Windows 的 PATH 环境指定的路径中有一个 MyNative_g.dll 文件。在大多数情况下,这个 DLL 可以通过将原文件重命名或复制为其名称带缀 _g 的文件。

现在,Java 调试器不允许您进入本地代码,但您可以在 Java 环境外使用 C 调试器(如 Microsoft Visual C++)调试本地方法。首先将源文件导入一个项目中。
将编译设置调整为在编译时将 include 目录包括在内:

c:/jdk1.1.6/include;c:/jdk1.1.6/include/win32 

将配置设置为以调试模式编译 DLL。在 Project Settings 中的 Debug 下,将可执行文件设置为 java.exe(或者 java_g.exe,但要确保您生成了一个 _g.dll 文件)。程序参数包括包含 main 的类名。如果在 DLL 中设置了断点,则当调用本地方法时,执行将在适当的地方停止。

下面是设置一个 Visual C++ 6.0 项目来调试本地方法的步骤。

  1. 在 Visual     C++ 中创建一个 Win32 DLL 项目,并将.c 和 .h 文件添加到这个项目中。


<!--[if !vml]--><!--[endif]-->

<!--[if !vml]--><!--[endif]-->

  • 在 Tools 下拉式菜单的 Options 设置下设置 JDK 的 include 目录。下面的对话框显示了这些目录。


<!--[if !vml]--><!--[endif]-->

  • 选择 Build 下拉式菜单下的 Build MyNative.dll 来建立这个项目。确保将项目的活动配置设置为调试(这通常是缺省值)。
  • 在 Project     Settings 下,设置 Debug 选项卡来调用适当的 Java 解释器,如下所示:


<!--[if !vml]--><!--[endif]-->

当执行这个程序时,忽略“在 java.exe 中找不到任何调试信息”的消息。当调用本地方法时,在 C 代码中设置的任何断点将在适当的地方停止 Java 程序的执行。

3.其他信息

JNI 方法和 C++

上面这些示例说明了如何在 C 源文件中使用 JNI 方法。如果使用 C++,则请将相应方法的格式从:

(*env)->JNIMethod( env, .... ); 

更改为:

env->JNIMethod( ... ); 

在C++ 中,JNI 函数被看作是 JNIEnv 类的成员方法。

字符串和国家语言支持

本文中使用的技术用 UTF 方法来转换字符串。使用这些方法只是为了方便起见,如果应用程序需要国家语言支持(NLS),则不能使用这些方法。有关在 Windows 和NLS 环境中处理 Java 字符串正确方法,请参标题为NLS Strings and JNI 的一篇论文。

4.小结

本文提供的示例用最常用的数据类据(如 jint 和 jstring)说明了如何实现本地方法,并讨论了 Windows 特定的几个问题,如显示字符串。本文提供的示例并未包括全部 JNIJNI 还包括其他参数类型,如jfloat、jdouble、jshort、jbyte 和 jfieldID,以及用来处理这些类型的方法。有关这个主题的详细信息,请参阅 Sun Microsystems 提供的 Java 本地接口规范。

5.关于作者:    David Wendt 是 IBM WebSphere Studio 的一名程序员,该工作室位于北卡罗莱纳州的Research Triangle Park。可以通过 wendt@us.ibm.com 与他联系。

Chap11:JNI编程系列之基础篇

编程技术   2008-12-06 19:50   阅读2   评论0

字号: 大  中 小

JNI编程系列之基础篇

最近干一个活需要从Java调用C++编译的动态链接库,研究了一下JNI,现在将网上搜罗的文档和自己的体会贡献出来。

JNI的做法是:通过在方法前加上关键字native来识别本地方法,然后用本地语言(如C,C++)来实现该方法,并编译成动态链接库,在Java的类中调用该动态链接库,然后就可以像使用Java自己的方法一样使用native方法了。这样做的好处是既具有了Java语言的便利性,又具有了C语言的效率;另一个好处是可以利用已有的C代码,避免重复开发。

下面从最简单的JNI程序入手,介绍如何进行JNI编程。

下面是一个简单的Java程序HelloWorld.java,

class HelloWorld {

private native void print();

public static void main(String[] args) {

new HelloWorld().print();

}

static {

System.loadLibrary("HelloWorld");

}

}

在这个例子中,注意到两个关键的地方。

首先是第二行

private native void print();

如果没有native关键字,这一行代码就是普通Java方法的声明。关键字native表明这是一个用本地语言实现的方法。

第二个地方是

System.loadLibrary("HelloWorld");

这行代码的作用是调用名为HelloWorld的动态链接库,在Windows下,是HelloWorld.dll,在Linux下是HelloWorld.so。

显然现在这个Java程序是不能运行的。要运行它先要做下面的工作。执行

> javac HelloWorld.java

> javah -jni HelloWorld

执行完这两条语句之后,会生成一个名为HelloWorld.h的文件,它的内容应该是这样的,

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class HelloWorld */

#ifndef _Included_HelloWorld

#define _Included_HelloWorld

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class:     HelloWorld

* Method:    print

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);

#ifdef __cplusplus

}

#endif

#endif

注意到在这个程序的开头有这样一行代码,

#include <jni.h>

这里的jni.h,只要你安装了JDK就能在安装目录下找到它。

不要修改这个文件的内容,现在要做的是写一个名为HelloWorld.cpp程序,实现上面这个.h文件里的函数,

//------------

#include "HelloWorld.h"

#include <iostream>

JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject) {

std::cout << "Hello World!"<< std::endl;

}

//--------------

这是一个最简单的C++程序。将它编译为动态链接库,我们得到HelloWorld.dll,将这个.dll文件拷到HelloWorld.java文件所在的目录下。执行

> java HelloWorld

你会看到屏幕上输出

> Hello World!

现在来总结一下,要实现JNI编程,需要以下几个步骤:

1. 写一个Java程序,将你希望用C语言实现的方法用native关键字标识出来,同时加上调用动态链接库的语句。

  System.loadLibrary("HelloWorld");

2. 执行下面两条语句,生成.h文件

  

  

  > javac HelloWorld.java

  

在class或bin目录下(其下或其子目录下有 javac命令生成的*.class文件)执行

  > javah -jni HelloWorld

3. 根据.h文件,写一个.cpp程序,编译成动态链接库,并将其复制到.java文件所在的路径下。

4. 执行java HelloWorld

这样,就学会了最简单的JNI编程,网上能google到的大部分文章也就到此为止了。但是你一定还有很多疑问,就像我刚开始一样,最容易想到的就是,如果本地方法要传递参数或者返回值怎么办?本地方法的定义在.java文件中,参数或者返回值的类型都是Java的类型。而它的实现是通过C程序完成的,参数和返回值的类型只能是C的类型。诸如此类的问题,上面这个简单的例子是回答不了的。在下一篇,我将解释这些问题。

Chap12:JNI编程系列之中级篇(上)

编程技术   2008-12-06 23:41   阅读9   评论0

字号: 大  中 小

本篇将介绍在JNI编程中如何传递参数和返回值。

首先要强调的是,native方法不但可以传递Java的基本类型做参数,还可以传递更复杂的类型,比如String,数组,甚至自定义的类。这一切都可以在jni.h中找到答案。

1. Java基本类型的传递

用过Java的人都知道,Java中的基本类型包括boolean,byte,char,short,int,long,float,double这样几种,如果你用这几种类型做native方法的参数,当你通过javah -jni生成.h文件的时候,只要看一下生成的.h文件,就会一清二楚,这些类型分别对应的类型是jboolean,jbyte,jchar,jshort,jint,jlong,jfloat,jdouble 。这几种类型几乎都可以当成对应的C++类型来用,所以没什么好说的。

2. String参数的传递

Java的String和C++的string是不能对等起来的,所以处理起来比较麻烦。先看一个例子,

//*****************

class Prompt {

// native method that prints a prompt and reads a line

private native String getLine(String prompt);

public static void main(String args[]) {

Prompt p = new Prompt();

String input = p.getLine("Type a line: ");

System.out.println("User typed: " + input);

}

static {

System.loadLibrary("Prompt");

}

}

//*****************

在这个例子中,我们要实现一个native方法

String getLine(String prompt);

读入一个String参数,返回一个String值。

通过执行javah -jni得到的头文件是这样的

//*****************

#include <jni.h>

#ifndef _Included_Prompt

#define _Included_Prompt

#ifdef __cplusplus

extern "C" {

#endif

JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);

#ifdef __cplusplus

}

#endif

#endif

//*****************

jstring是JNI中对应于String的类型,但是和基本类型不同的是,jstring不能直接当作C++的string用。如果你用

cout << prompt << endl;

编译器肯定会扔给你一个错误信息的。

其实要处理jstring有很多种方式,这里只讲一种我认为最简单的方式,看下面这个例子,

//*****************

#include "Prompt.h"

#include <iostream>

JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)

{

const char* str;

str = env->GetStringUTFChars(prompt, false);

if(str == NULL) {

return NULL; /* OutOfMemoryError already thrown */

}

std::cout << str << std::endl;

env->ReleaseStringUTFChars(prompt, str);

// return a string

char* tmpstr = "return string succeeded";

jstring rtstr = env->NewStringUTF(tmpstr);

return rtstr;

}

//*****************

在上面的例子中,作为参数的prompt不能直接被C++程序使用,先做了如下转换

str = env->GetStringUTFChars(prompt, false);

将jstring类型变成一个char*类型。

返回的时候,要生成一个jstring类型的对象,也必须通过如下命令,

jstring rtstr = env->NewStringUTF(tmpstr);

这里用到的GetStringUTFChars和NewStringUTF都是JNI提供的处理String类型的函数,还有其他的函数这里就不一一列举了。

/****************************************************/

JNI编程系列之中级篇(下)

编程技术   2008-12-06 23:44   阅读7   评论0 

字号: 大  中 小

3. 数组类型的传递

和String一样,JNI为Java基本类型的数组提供了j*Array类型,比如int[]对应的就是jintArray。来看一个传递int数组的例子,Java程序就不写了,

//*******************

JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)

{

   jint *carr;

   carr = env->GetIntArrayElements(arr, false);

   if(carr == NULL) {

       return 0; /* exception occurred */

}

   jint sum = 0;

   for(int i=0; i<10; i++) {

       sum += carr[i];

}

   env->ReleaseIntArrayElements(arr, carr, 0);

   return sum;

}

//*******************

这个例子中的GetIntArrayElements和ReleaseIntArrayElements函数就是JNI提供用于处理int数组的函数。如果试图用arr[i]的方式去访问jintArray类型,毫无疑问会出错。JNI还提供了另一对函数GetIntArrayRegion和ReleaseIntArrayRegion访问int数组,就不介绍了,对于其他基本类型的数组,方法类似。

4. 二维数组和String数组

在JNI中,二维数组和String数组都被视为object数组,因为数组和String被视为object。仍然用一个例子来说明,这次是一个二维int数组,作为返回值。

//*******************

JNIEXPORT jobjectArray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size)

{

   jobjectArray result;

   jclass intArrCls = env->FindClass("[I");

   result = env->NewObjectArray(size, intArrCls, NULL);

   for (int i = 0; i < size; i++) {

       jint tmp[256]; /* make sure it is large enough! */

       jintArray iarr = env->NewIntArray(size);

       for(int j = 0; j < size; j++) {

           tmp[j] = i + j;

       }

       env->SetIntArrayRegion(iarr, 0, size, tmp);

       env->SetObjectArrayElement(result, i, iarr);

       env->DeleteLocalRef(iarr);

    }

   return result;

}

//*******************

上面代码中的第三行,

jobjectArray result;

因为要返回值,所以需要新建一个jobjectArray对象。

jclass intArrCls = env->FindClass("[I");

是创建一个jclass的引用,因为result的元素是一维int数组的引用,所以intArrCls必须是一维int数组的引用,这一点是如何保证的呢?注意FindClass的参数"[I",JNI就是通过它来确定引用的类型的,I表示是int类型,[标识是数组。对于其他的类型,都有相应的表示方法,

Z boolean

B byte

C char

S short

I int

J long

F float

D double

String是通过“Ljava/lang/String;”表示的,那相应的,String数组就应该是“[Ljava/lang/String;”。

还是回到代码,

result = env->NewObjectArray(size, intArrCls, NULL);

的作用是为result分配空间。

jintArray iarr = env->NewIntArray(size);

是为一维int数组iarr分配空间。

env->SetIntArrayRegion(iarr, 0, size, tmp);

是为iarr赋值。

env->SetObjectArrayElement(result, i, iarr);

是为result的第i个元素赋值。

通过上面这些步骤,我们就创建了一个二维int数组,并赋值完毕,这样就可以做为参数返回了。

如果了解了上面介绍的这些内容,基本上大部分的任务都可以对付了。虽然在操作数组类型,尤其是二维数组和String数组的时候,比起在单独的语言中编程要麻烦,但既然我们享受了跨语言编程的好处,必然要付出一定的代价。

有一点要补充的是,本文所用到的函数调用方式都是针对C++的,如果要在C中使用,所有的env->都要被替换成(*env)->,而且后面的函数中需要增加一个参数env,具体请看一下jni.h的代码。另外还有些省略的内容,可以参考JNI的文档:Java Native Interface 6.0 Specification,在JDK的文档里就可以找到。如果要进行更深入的JNI编程,需要仔细阅读这个文档。接下来的高级篇,也会讨论更深入的话题。

Chap13:JNI编程系列之高级篇

编程技术   2008-12-10 17:08   阅读6   评论0

字号: 大  中 小

在本篇中,将会涉及关于JNI编程更深入的话题,包括:在native方法中访问Java类的域和方法,将Java中自定义的类作为参数和返回值传递等等。了解这些内容,将会对JNI编程有更深入的理解,写出的程序也更清晰,易用性更好。

1. 在一般的Java类中定义native方法

在前两篇的例子中,都是将native方法放在main方法的Java类中,实际上,完全可以在任何类中定义native方法。这样,对于外部来说,这个类和其他的Java类没有任何区别。

2. 访问Java类的域和方法

native方法虽然是native的,但毕竟是方法,那么就应该同其他方法一样,能够访问类的私有域和方法。实际上,JNI的确可以做到这一点,我们通过几个例子来说明,

public class ClassA {

String str_ = "abcde";

int number_;

public native void nativeMethod();

private void javaMethod() {

System.out.println("call java method succeeded");

}

static {

System.loadLibrary("ClassA");

}

}

在这个例子中,我们在一个没有main方法的Java类中定义了native方法。我们将演示如何在nativeMethod()中访问域str_,number_和方法javaMethod(),nativeMethod()的C++实现如下,

JNIEXPORT void JNICALL Java_testclass_ClassCallDLL_nativeMethod(JNIEnv *env, jobject obj)

{

// access field

jclass cls = env->GetObjectClass(obj);

jfieldID fid = env->GetFieldID(cls, "str_", "Ljava/lang/String;");

jstring jstr = (jstring)env->GetObjectField(obj, fid);

const char *str = env->GetStringUTFChars(jstr, false);

if(std::string(str) == "abcde")

std::cout << "access field succeeded" << std::endl;

jint i = 2468;

fid = env->GetFieldID(cls, "number_", "I");

env->SetIntField(obj, fid, i);

// access method

jmethodID mid = env->GetMethodID(cls, "javaMethod", "()V");

env->CallVoidMethod(obj, mid);

}

上面的代码中,通过如下两行代码获得str_的值,

jfieldID fid = env->GetFieldID(cls, "str_", "Ljava/lang/String;");

jstring jstr = (jstring)env->GetObjectField(obj, fid);

第一行代码获得str_的id,在GetFieldID函数的调用中需要指定str_的类型,第二行代码通过str_的id获得它的值,当然我们读到的是一个jstring类型,不能直接显示,需要转化为char*类型。

接下来我们看如何给Java类的域赋值,看下面两行代码,

fid = env->GetFieldID(cls, "number_", "I");

env->SetIntField(obj, fid, i);

第一行代码同前面一样,获得number_的id,第二行我们通过SetIntField函数将i的值赋给number_,其他类似的函数可以参考JDK的文档。

访问javaMethod()的过程同访问域类似,

jmethodID mid = env->GetMethodID(cls, "javaMethod", "()V");

env->CallVoidMethod(obj, mid);

需要强调的是,在GetMethodID中,我们需要指定javaMethod方法的类型,域的类型很容易理解,方法的类型如何定义呢,在上面的例子中,我们用的是()V,V表示返回值为空,()表示参数为空。如果是更复杂的函数类型如何表示?看一个例子,

long f (int n, String s, int[] arr);

这个函数的类型符号是(ILjava/lang/String;[I)J,I表示int类型,Ljava/lang/String;表示String类型,[I表示int数组,J表示long。这些都可以在文档中查到。

3. 在native方法中使用用户定义的类

JNI不仅能使用Java的基础类型,还能使用用户定义的类,这样灵活性就大多了。大体上使用自定义的类和使用Java的基础类(比如String)没有太大的区别,关键的一点是,如果要使用自定义类,首先要能访问类的构造函数,看下面这一段代码,我们在native方法中使用了自定义的Java类ClassB,

jclass cls = env->FindClass("Ltestclass/ClassB;");

jmethodID id = env->GetMethodID(cls, "<init>", "(D)V");

jdouble dd = 0.033;

jvalue args[1];

args[0].d = dd;

jobject obj = env->NewObjectA(cls, id, args);

首先要创建一个自定义类的引用,通过FindClass函数来完成,参数同前面介绍的创建String对象的引用类似,只不过类名称变成自定义类的名称。然后通过GetMethodID函数获得这个类的构造函数,注意这里方法的名称是"<init>",它表示这是一个构造函数。

jobject obj = env->NewObjectA(cls, id, args);

生成了一个ClassB的对象,args是ClassB的构造函数的参数,它是一个jvalue*类型。

通过以上介绍的三部分内容,native方法已经看起来完全像Java自己的方法了,至少主要功能上齐备了,只是实现上稍麻烦。而了解了这些,JNI编程的水平也更上一层楼。下面要讨论的话题也是一个重要内容,至少如果没有它,我们的程序只能停留在演示阶段,不具有实用价值。

4. 异常处理

在C++和Java的编程中,异常处理都是一个重要的内容。但是在JNI中,麻烦就来了,native方法是通过C++实现的,如果在native方法中发生了异常,如何传导到Java呢?

JNI提供了实现这种功能的机制。我们可以通过下面这段代码抛出一个Java可以接收的异常,

jclass errCls;

env->ExceptionDescribe();

env->ExceptionClear();

errCls = env->FindClass("java/lang/IllegalArgumentException");

env->ThrowNew(errCls, "thrown from C++ code");

如果要抛出其他类型的异常,替换掉FindClass的参数即可。这样,在Java中就可以接收到native方法中抛出的异常。

至此,JNI编程系列的内容就完全结束了,这些内容都是本人的原创,通过查阅文档和网上的各种文章总结出来的,相信除了JDK的文档外,没有比这更全面的讲述JNI编程的文章了。当然,限于篇幅,有些地方不可能讲的很细。限于水平,也可能有一些错误。文中所用的代码,都亲自编译执行过。希望这些内容能为需要的朋友提供帮助,毕竟,分享是一种美德。

Chap14:如何在C/C++中调用Java

    

  

作者:刘冬 发文时间:2003.02.17

Java跨平台的特性使Java越来越受开发人员的欢迎,但也往往会听到不少的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出 一个控制台窗口,这个控制台窗口让本来非常棒的界面失色不少。怎么能够让通过Java开发的GUI程序不弹出Java的控制台窗口呢?其实现在很多流行的 开发环境例如JBuilder、Eclipse都是使用纯Java开发的集成环境。这些集成环境启动的时候并不会打开一个命令窗口,因为它使用了 JNI(Java Native Interface)的技术。通过这种技术,开发人员不一定要用命令行来启动Java程序,可以    通过编写一个本地GUI 程序直接启动Java程序,这样就可避免另外打开一个命令窗口,让开发的Java程序更加专业。

JNI允许运行在虚拟机的Java程序能够与其它语言(例如C和C++)编写的程序或者类库进行相互间的调用。同时JNI提供的一整套的API,允许将Java虚拟机直接嵌入到本地的应用程序中。图1是Sun站点上对JNI的基本结构的描述。

<!--[if !vml]--><!--[endif]-->

图1 JNI基本结构描述图

本文将介绍如何在C/C++中调用Java方法,并结合可能涉及到的问题介绍整个开发的步骤及可能遇到的难题和解决方法。本文所采用的工具是 Sun公司创建的 Java Development Kit (JDK) 版本 1.3.1,以及微软公司的Visual C++ 6开发环境。

1.环境搭建

为了让本文以下部分的代码能够正常工作,我们必须建立一个完整的开发环境。首先需要下载并安装JDK 1.3.1,其下载地址为“http://java.sun.com”。假设安装路径为C:/JDK。下一步就是设置集成开发环境,通过Visual C++ 6的菜单Tools→Options打开选项对话框如图2。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

<!--[if !vml]--><!--[endif]-->

图2 设置集成开发环境图
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->


将目录C:/JDK/include和C:/JDK/include/win32加入到开发环境的Include Files目录中,

同时将C: /JDK/lib目录添加到开发环境的Library Files目录中。这三个目录是JNI定义的一些常量、结构及方法的头文件和库文件。

集成开发环境 已经设置完毕,同时为了执行程序需要把Java虚拟机所用到的动态链接库所在的目录C:/JDK /jre/bin/classic设置到系统的Path 环境变量中。这里需要提出的是,某些开发人员为了方便直接将JRE所用到的DLL文件直接拷贝到系统目录下。这样做是不行的,将导致初始化Java虚拟机 环境失败(返回值-1),原因是Java虚拟机是以相对路径来寻找所用到的库文件和其它一些相关文件的。

至此整个JNI的开发环境设置完毕,为了让此次 JNI旅程能够顺利进行,还必须先准备一个Java类。在这个类中将用到Java中几乎所有有代表性的属性及方法,如静态方法与属性、数组、异常抛出与捕 捉等。我们定义的Java程序(Demo.java)如下,本文中所有的代码演示都将基于该Java 程序,代码如下:

package jni.test;

/**

* 该类是为了演示JNI如何访问各种对象属性等

* @author liudong

*/

public class Demo { 

//用于演示如何访问静态的基本类型属性

public static int COUNT = 8;

//演示对象型属性

public String msg;
private int[] counts;
public Demo() {
this("缺省构造函数");
}
/**
  * 演示如何访问构造器
  */
public Demo(String msg) {
  System.out.println("<init>:" + msg);
  this.msg = msg;
  this.counts = null;
}
/**
  * 该方法演示如何访问一个访问以及中文字符的处理
  */
public String getMessage() {
  return msg;
}

/**

  * 演示数组对象的访问

  */
public int[] getCounts() {
  return counts;
}
/**
  * 演示如何构造一个数组对象
*/
public void setCounts(int[] counts) {
  this.counts = counts;
}
/**
  * 演示异常的捕捉
*/
public void throwExcp() throws IllegalAccessException {

  throw new IllegalAccessException("exception occur.");

}

}

2.初始化虚拟机

本地代码在调用Java方法之前必须先加载Java虚拟机,而后所有的Java程序都在虚拟机中执行。

为了初始化Java虚拟机,JNI 提供了 一系列的接口函数Invocation API。通过这些API可以很方便地将虚拟机加载到内存中。创建虚拟机可以用函 数 jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)。对于这个函数有一点 需要注意的是,在JDK 1.1中第三个参数总是指向一个结构JDK1_ 1InitArgs,这个结构无法完全在所有版本的虚拟机中进行无缝移植。在 JDK 1.2中已经使用了一个标准的初始化结构JavaVMInitArgs来替代JDK1_1InitArgs。下面我们分别给出两种不同版本的示例代码。

在JDK 1.1初始化虚拟机:

#include <jni.h>
int main() {
JNIEnv *env;
JavaVM *jvm;
JDK1_1InitArgs vm_args;
jint res;
/* IMPORTANT: 版本号设置一定不能漏 */
vm_args.version = 0x00010001;
/*获取缺省的虚拟机初始化参数*/
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* 添加自定义的类路径 */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/*设置一些其他的初始化参数*/
/* 创建虚拟机 */
res = JNI_CreateJavaVM(&jvm,&env,&vm_args);
if (res < 0) {
  fprintf(stderr, "Can't create Java VM/n");
  exit(1);
}
/*释放虚拟机资源*/
(*jvm)->DestroyJavaVM(jvm);
}
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

JDK 1.2初始化虚拟机:

/* invoke2.c */
#include <jni.h>
int main() {
int res;
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
vm_args.version=JNI_VERSION_1_2;//这个字段必须设置为该值
/*设置初始化参数*/
options[0].optionString = "-Djava.compiler=NONE";
options[1].optionString = "-Djava.class.path=.";
options[2].optionString = "-verbose:jni"; //用于跟踪运行时的信息
/*版本号设置不能漏*/
vm_args.version = JNI_VERSION_1_2;
vm_args.nOptions = 3;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
if (res < 0) {
  fprintf(stderr, "Can't create Java VM/n");
  exit(1);
}   
(*jvm)->DestroyJavaVM(jvm);
fprintf(stdout, "Java VM destory./n");
}



为了保证JNI代码的可移植性,建议使用JDK 1.2的方法来创建虚拟机。JNI_CreateJavaVM函数的第二个参数JNIEnv *env,就是贯穿整个JNI始末的一个参数,因为几乎所有的函数都要求一个参数就是JNIEnv *env。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

3.访问类方法

初始化了Java虚拟机后,就可以开始调用Java的方法。要调用一个Java对象的方法必须经过几个步骤:

3.1.获取指定对象的类定义(jclass)

有两种途径来获取对象的类定义:

第一种是在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码:
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把点号换成斜杠


然后通过对象直接得到其所对应的类定义:

jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的对象,类型是jobject

3.2.读取要调用方法的定义(jmethodID)

我们先来看看JNI中获取方法定义的函数:

jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name,
const char *sig);
jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char
*name, const char *sig);

这两个函数的区别在于GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。这两个函 数都需要提供四个参数:env就是初始化虚拟机得到的JNI环境;第二个参数class是对象的类定义,也就是第一步得到的obj;第三个参数是方法名 称;最重要的是第四个参数,这个参数是方法的定义。因为我们知道Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要第四个参数来指定方法的具体定义。但是怎么利用一个字符串来表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类 中每个属性、方法的定义。下面就来看看jni.test.Demo的定义:

打开命令行窗口并运行 javap -s -p jni.test.Demo 得到运行结果如下:

Compiled from Demo.java
public class jni.test.Demo extends java.lang.Object {
public static int COUNT;
/*   I   */
public java.lang.String msg;
/*   Ljava/lang/String;   */
private int counts[];
/*   [I   */
public jni.test.Demo();
/*   ()V   */
public jni.test.Demo(java.lang.String);
/*   (Ljava/lang/String;)V   */
public java.lang.String getMessage();
/*   ()Ljava/lang/String;   */
public int getCounts()[];
/*   ()[I   */
public void setCounts(int[]);
/*   ([I)V   */
public void throwExcp() throws java.lang.IllegalAccessException;
/*   ()V   */
static {};
/*   ()V   */
}
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

我们看到类中每个属性和方法下面都有一段注释。注释中不包含空格的内容就是第四个参数要填的内容(关于javap具体参数请查询JDK的使用帮助)。下面这段代码演示如何访问jni.test.Demo的getMessage方法:

/* 

假设我们已经有一个jni.test.Demo的实例obj 
*/  

jmethodID mid;

jclass cls = (*env)-> GetObjectClass (env, obj); //获取实例的类定义
mid=(*env)->GetMethodID( env,cls,"getMessage"," ()Ljava/lang/String; " );
/*如果mid为0表示获取方法定义失败*/

jstring msg = (*env)-> CallObjectMethod(env, obj, mid);

/* 

如果该方法是静态的方法那只需要将最后一句代码改为以下写法即可:
jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid);
*/

3.3.调用方法

为了调用对象的某个方法,可以使用函数Call<TYPE>Method或者 CallStatic<TYPE>Method(访问类的静态方法),<TYPE>根据不同的返回类型而定。这些方法都是使用可变参数的定义,如果访问某个方法需要参数时,只需要把所有参数按照顺序填写到方法中就可以。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。

4访问类属性

访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

4.1.获取指定对象的类(jclass)

这一步与访问类方法的第一步完全相同,具体使用参看访问类方法的第一步。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

4.2.读取类属性的定义(jfieldID)

在JNI中是这样定义获取类属性的方法的:

jfieldID (JNICALL *GetFieldID)

(JNIEnv *env, jclass clazz, const char *name, const char *sig);

jfieldID (JNICALL *GetStaticFieldID)

(JNIEnv *env, jclass clazz, const char *name, const char *sig);

这两个函数中第一个参数为JNI环境;clazz为类的定义;name为属性名称;第四个参数同样是为了表达属性的类型。前面我们使用javap工具获取类的详细定义的时候有这样两行:

public java.lang.String msg;

/*   Ljava/lang/String;   */

其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

4.3.读取和设置属性值

有了属性的定义要访问属性值就很容易了。有几个方法用来读取和设置类的属性,它们是:Get<TYPE>Field、 Set<TYPE>Field、GetStatic<TYPE>Field、 SetStatic<TYPE>Field。比如读取Demo类的msg属性就可以用GetObjectField,而访问COUNT用 GetStaticIntField,相关代码如下:

jfieldID field = (*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;");

jstring msg = (*env)->GetObjectField(env, cls, field); //msg就是对应Demo的msg

jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I");

jint count = (*env)->GetStaticIntField(env,cls,field2);

5.访问构造函数

很多人刚刚接触JNI的时候往往会在这一节遇到问题,查遍了整个jni.h看到这样一个函数NewObject,它应该是可以用来访问类的构造函 数。但是该函数需要提供构造函数的方法定义,其类型是jmethodID。从前面的内容我们知道要获取方法的定义首先要知道方法的名称,但是构造函数的名称怎么来填写呢?其实访问构造函数与访问一个普通的类方法大体上是一样的,惟一不同的只是方法名称不同及方法调用时不同而已。访问类的构造函数时方法名必须填写“<init>”。下面的代码演示如何构造一个Demo类的实例:

jclass cls = (*env)->FindClass(env, "jni/test/Demo");

/** 

首先通过类的名称获取类的定义,相当于Java中的Class.forName方法

*/

if (cls == 0)  

<error handler>

jmethodID mid = (*env)->GetMethodID(env,cls,"<init>","(Ljava/lang/String;)V ");

if(mid == 0) 

<error handler>

jobject demo = (*env)->NewObject( env ,cls,mid,0 );

/**

访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义

上面的代码我们构造了一个Demo的实例并传一个空串null

*/

6.数组处理


6.1创建一个新数组

要创建一个数组,我们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型j<TYPE>Array及数组操作的函数 New<TYPE>Array,其中<TYPE>就是数组中元素的类型。例如,要创建一个大小为10并且每个位置值分别为 1-10的整数数组,编写代码如下:

int i = 1;

jintArray array; //定义数组对象

(*env)-> NewIntArray(env, 10);

for(; i<= 10; i++) 

(*env)->SetIntArrayRegion(env, array, i-1, 1, &i);

6.2访问数组中的数据

访问数组首先应该知道数组的长度及元素的类型。现在我们把创建的数组中的每个元素值打印出来,代码如下:

int i;

/* 获取数组对象的元素个数 */

int len = (*env)->GetArrayLength(env, array);

/* 获取数组中的所有元素 */

jint* elems = (*env)-> GetIntArrayElements(env, array, 0);

for(i=0; i< len; i++)

printf("ELEMENT %d IS %d/n", i, elems[i]);

7.中文处理


中文字符的处理往往是让人比较头疼的事情,特别是使用Java语言开发的软件,在JNI这个问题更加突出。由于Java中所有的字符都 是 Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的定义一般都没有使用Unicode的编码方式。为了让本地方法能够访问 Java中定义的中文字符及Java访问本地方法产生的中文字符串,我定义了两个方法用来做相互转换。

· 方法一,将Java中文字符串转为本地字符串
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

/**

第一个参数是虚拟机的环境指针

第二个参数为待转换的Java字符串定义

第三个参数是本地存储转换后字符串的内存块

第三个参数是内存块的大小

*/

int JStringToChar(JNIEnv *env, jstring str, LPTSTR desc, int desc_len)

{

int len = 0;

if(desc==NULL||str==NULL)

return -1;

//在VC中wchar_t是用来存储宽字节字符(UNICODE)的数据类型

wchar_t *w_buffer = new wchar_t[1024];

ZeroMemory(w_buffer,1024*sizeof(wchar_t));

//使用GetStringChars而不是GetStringUTFChars

wcscpy(w_buffer,env->GetStringChars(str,0));

env->ReleaseStringChars(str,w_buffer);

ZeroMemory(desc,desc_len);

//调用字符编码转换函数(Win32 API)将UNICODE转为ASCII编码格式字符串

//关于函数WideCharToMultiByte的使用请参考MSDN

len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL);

//len = wcslen(w_buffer);

if(len>0 && len<desc_len)

  desc[len]=0;

delete[] w_buffer;

return strlen(desc);

}
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

· 方法二,将C的字符串转为Java能识别的Unicode字符串


jstring NewJString(JNIEnv* env,LPCTSTR str)

{

if(!env || !str)

  return 0;

int slen = strlen(str);

jchar* buffer = new jchar[slen];

int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen);

if(len>0 && len < slen)

  buffer[len]=0;

jstring js = env->NewString(buffer,len);

delete [] buffer;

return js;

}

8.异常

由于调用了Java的方法,因此难免产生操作的异常信息。这些异常没有办法通过C++本身的异常处理机制来捕捉到,但JNI可以通过一些函数来获 取Java中抛出的异常信息。之前我们在Demo类中定义了一个方法throwExcp,下面将访问该方法并捕捉其抛出来的异常信息,代码如下:

/**
假设我们已经构造了一个Demo的实例obj,其类定义为cls
*/
jthrowable excp = 0; /* 异常信息定义 */
jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V");
/*如果mid为0表示获取方法定义失败*/
jstring msg = (*env)-> CallVoidMethod(env, obj, mid);
/* 在调用该方法后会有一个IllegalAccessException的异常抛出 */
excp = (*env)->ExceptionOccurred(env);
if(excp){ 
(*env)->ExceptionClear(env);
//通过访问excp来获取具体异常信息
/*

在Java中,大部分的异常信息都是扩展类java.lang.Exception,因此可以访问excp的toString
或者getMessage来获取异常信息的内容。访问这两个方法同前面讲到的如何访问类的方法是相同的。

*/

}

9.线程和同步访问

有些时候需要使用多线程的方式来访问Java的方法。我们知道一个Java虚拟机是非常消耗系统的内存资源,差不多每个虚拟机需要内存大约在 20MB左右。为了节省资源要求每个线程使用的是同一个虚拟机,这样在整个的JNI程序中只需要初始化一个虚拟机就可以了。所有人都是这样想的,但是一旦 子线程访问主线程创建的虚拟机环境变量,系统就会出现错误对话框,然后整个程序终止。

其实这里面涉及到两个概念,它们分别是虚拟机(JavaVM *jvm)和虚拟机环境(JNIEnv *env)。真正消耗大量系统资源的是 jvm而不是env,jvm是允许多个线程访问的,但是env只能被创建它本身的线程所访问,而且每个线程必须创建自己的虚拟机环境env。这时候会有人提出疑问,主线程在初始化虚拟机的时候就创建了虚拟机环境env。为了让子线程能够创建自己的env,JNI提供了两个函数:AttachCurrentThread和DetachCurrentThread。下面代码就是子线程访问Java方法的框架:

DWORD WINAPI ThreadProc(PVOID dwParam)

{

JavaVM jvm = (JavaVM*)dwParam; /* 将虚拟机通过参数传入 */

JNIEnv* env;

(*jvm)-> AttachCurrentThread(jvm, & env, NULL);

.........

(*jvm)-> DetachCurrentThread(jvm);

}
< !--[if !supportLineBreakNewLine]-->
< !--[endif]-->

10.时间


关于时间的话题是我在实际开发中遇到的一个问题。当要发布使用了JNI的程序时,并不一定要求客户要安装一个Java运行环境,因为可以在安装程 序中打包这个运行环境。为了让打包程序利于下载,这个包要比较小,因此要去除JRE(Java运行环境)中一些不必要的文件。但是如果程序中用到 Java中的日历类型,例如java.util.Calendar等,那么有个文件一定不能去掉,这个文件就是[JRE]/lib/tzmappings。它是一个时区映射文件,一旦没有该文件就会发现时间操作上经常出现与正确时间相差几个小时的情况。下面是打包JRE中必不可少 的文件列表(以Windows环境为例),其中[JRE]为运行环境的目录,同时这些文件之间的相对路径不能变。

文件名  目录

hpi.dll       [JRE]/bin
ioser12.dll    [JRE]/bin
java.dll      [JRE]/bin
net.dll       [JRE]/bin
verify.dll     [JRE]/bin
zip.dll       [JRE]/bin
jvm.dll      [JRE]/bin/classic
rt.jar        [JRE]/lib
tzmappings   [JRE]/lib


由于rt.jar有差不多10MB,但是其中有很大一部分文件并不需要,可以根据实际的应用情况进行删除。例如程序如果没有用到Java Swing,就可以把涉及到Swing的文件都删除后重新打包。

Chap15:基本JNI调用技术(c/c++与java互调)

一.C/C++调用Java

在C/C++中调用Java的方法一般分为五个步骤:初始化虚拟机、获取类、创建类对象、调用方法和退出虚拟机。

1. 初始化虚拟机

代码如下:

    JNIEnv *env;

    JavaVM *jvm;

    JavaVMInitArgs vm_args;

    JavaVMOption options[3];

    int res;

    //设置参数

options[0].optionString = "-Djava.compiler=NONE";

//classpath有多个时,UNIX下以“:”分割。

    options[1].optionString = "-Djava.class.path=.";

    options[2].optionString = "-verbose:jni";

    vm_args.version = JNI_VERSION_1_4;

    vm_args.nOptions = 3;

    vm_args.options = options;

    vm_args.ignoreUnrecognized = JNI_TRUE;

    res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);

      if (res >= 0)

{

     //创建虚拟机成功

}

一个应用程序只需要一个虚拟机,但是每个线程需要自己的虚拟机运行环境。我们从一个虚拟机获取多个当前线程的运行环境,代码如下:

int result=0;

result=jvm->AttachCurrentThread( reinterpret_cast<void**>(& env ), 0 );

if(result>=0)

{

     //获取运行环境成功

}

当线程退出时,需要释放本线程使用的运行环境。

jvm->DetachCurrentThread();

2 获取类

在进行方法调用之前,需要先获取相应的类,类名称必须包括包名,其中的“.”用“/”代替。

jclass JavaClass;

JavaClass = env->FindClass("com/test/TestInterface");

   if(JavaClass != 0)

   {

            //获取成功

   }

3 创建类对象

如果需要调用的方法静态方法,则可以跳过本步骤。反之,则需要构造该对象。构造对象是通过调用类的构造函数来实现的,构咱函数的方法声明为<init>, GetMethodID方法的参数在下一步骤详细说明。

jobject obj;

jmethodID ctor;

ctor = env->GetMethodID(JavaClass,"<init>","()V");

if(ctor != 0)//获取方法成功

   {

         obj = env->NewObject(JavaClass, ctor);

   }

4 调用方法

调用一个方法需要两个步骤:获取方法句柄和调用方法。

jmethodID methodID = env->GetMethodID( JavaClass, "setTest","(I)V");

if(methodID!=0)//获取方法成功

{

env->CallVoidMethod( obj, methodID,12);

}

GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。他们传入参数的参数依次为:类定义、方法名称和方法的定义,方法的定义可以用jdk中带的javap工具反编译class文件获取,其格式如下:

public void setTest(int inTest);

Signature: (I)V

Signature后面的内容就是方法的定义。

CallVoidMethod是对获取的方法进行调用,JNI接口中提供了一系列的同 类方法,包括静态方法的调用函数(如:CallStaticXXXMethod)和非静态的方法(如:CallXXXMethod),其中XXX表示的不 同方法返回类型,包括int、object等等。

5 退出虚拟机

退出虚拟机调用方法如下:

jvm->DestroyJavaVM();

在JNI接口定义中,只有最后一个线程退出时,该方法才会返回,但是我只用一个线程,调用该方法也无法返回。故此建议系统退出时执行该方法,或者整个程序退出时,让虚拟机自己释放。

[注意]:

l 在处理中文字符串时,需要注意Java的char是双字节的,采用Unicode编码,在和C++中的char转换时,需要用到系统API:WideCharToMultiByte和MultiByteToWideChar。

l 注意对运行环境中对象引用时的释放,以免引起内存泄漏。

jstring str;

wchar_t *w_buffer =(wchar_t *)env->GetStringChars(str,0);

env->ReleaseStringChars(str,(const unsigned short *)w_buffer);

6 处理异常

C/C++中调用Java时,一定要捕获并处理Java方法抛出的异常信息,否则可能导致C/C++进程的核心转储(Core Dump)。

异常应在每个方法调用后检查:

msg = (jstring)env->CallObjectMethod(obj, mid);

        if (env->ExceptionOccurred())

        {

           env->ExceptionDescribe();         

            env->ExceptionClear();

           return 0;

        }

二.Java调用C/C++

Java调用C/C++时,遵循几个步骤:

1、 用Java native关键字声明方法为本地方法(非Java语言实现)。

2、 编译该声明类,得到XXX.class文件。

3、 用“javah –jni XXX”命令从该class文件生成C语言头文件(XXX.h)。

4、 采用C语言实现该头文件声明的方法,将实现类编译成库文件(libXXX.so)。

5、 在Java程序中使用System.loadLibrary(XXX)加载该库文件(需要设置-Djava.library.path环境变量指向该库文件存放路径)。

6、 即可象调用Java方法一样,调用native方式声明的本地方法。

Chap16:JNI的c代码中,另外一个线程获取 JNIEnv

2009-06-19 14:36

JNI 中,JNIEnv* 指针变量只对当前线程有效。如果是其他的线程,需要先获得 JVM* 指针,然后再获得当前线程的JNIEnv* 指针。部分示例代码为:

/** Invoker.cpp, Invoker.java */

#include< jni.h>

#include< stdio.h>

#include "Invoker.h"

#include "invoker_include.h"

JavaVM *   jvm;

JNIEnv *   static_env;

jobject *  jObject; // 线程间公用,必须使用 global reference

jclass     c;        // 必须使用 global reference

jmethodID m;   // 必须使用global reference

/*****************************

* Class:     Invoker

* Method:    register

* Signature: ()V

*****************************/

JNIEXPORT void JNICALL Java_Invoker_register (JNIEnv *env, jobject arg)

{

jObject = arg;

// printf("object: %x, %x. /n", &arg, &jObject);

printf("[main] Invoker registered. /n");

jclass bgpClass = (*env)->GetObjectClass(env, arg);

jmethodID methodId = (*env)->GetMethodID(env, bgpClass, "invoke", "()V");

printf("[main]-class: %d, method: %d /n", bgpClass, methodId);

(*env)->CallVoidMethod(env, arg, methodId);

// Global reference

(*env)->GetJavaVM(env, &jvm);

     jObject = (*env)->NewGlobalRef(env, arg);

     c = (*env)->NewGlobalRef(env, bgpClass);

     m = (*env)->NewGlobalRef(env, methodId);

start(invoke_java_method);

(*env)->DeleteGlobalRef(env, c);             // 手动销毁 global reference

(*env)->DeleteGlobalRef(env, m);            // 手动销毁 global reference

(*env)->DeleteGlobalRef(env, jObject);

(*jvm)->DetachCurrentThread(jvm);         // 销毁线程

(*jvm)->DestroyJavaVM(jvm);              // ?销毁虚拟机

}

// Test method

JNIEXPORT void JNICALL Java_Invoker_println (JNIEnv *env, jobject obj, jstring string)

{

const char *str = (*env)->GetStringUTFChars(env, string, 0);

printf("[main] %s/n",str);

(*env)->ReleaseStringUTFChars(env, string, str);

}

// Callback method 回调函数

int invoke_java_method ()

{

(*jvm)->AttachCurrentThread(jvm, (void**)&static_env, 0);           // 获得当前线程可以使用的 JNIEnv * 指针

    (*static_env)->CallVoidMethod(static_env, jObject, m);   // 调用 Java 方法

printf("[callback] java method invoked, invoker class: %x ... /n", &jObject);

}

chap 17:当JNI遇到多线程--java对象如何被C++中的多个线程访问?

java中要访问C++代码时, 使用JNI是唯一选择. 然而,在多线程的情况下, 可能出现以下问题:

问题描述:

一个java对象通过JNI调用DLL中一个send()函数向服务器发送消息,不等服务器消息到来就立即返回.同时把JNI接口的指针JNIEnv *env,和jobject obj保存在DLL中的变量里.

一段时间后,DLL中的消息接收线程接收到服务器发来的消息,并试图通过保存过的env和obj来调用先前的java对象的方法来处理此消息.

然而,JNI文档上说,JNI接口的指针JNIEnv*不能在c++的线程间共享,在我的程序中,如果接收线程试图调用java对象的方法,程序会突然退出.

不知道有没有方法突破JNI接口的指针不能在多个c++线程中共享的限制?

解决办法:

http://java.sun.com/docs/books/jni/html/pitfalls.html#29161 提到,
JNI接口指针不可为多个线程共用,但是java虚拟机的JavaVM指针是整个jvm公用的. 于是,在DLL中可以调用:

static JavaVM* gs_jvm;

env->GetJavaVM(&gs_jvm); 来获取JavaVM指针.获取了这个指针后,在DLL中的另一个线程里,可以调用:

JNIEnv *env;

gs_jvm->AttachCurrentThread((void **)&env, NULL);

来将DLL中的线程 "attached to the virtual machine"(不知如何翻译...),同时获得了这个线程在jvm中的  JNIEnv指针.

由于我需要做的是在DLL中的一个线程里改变某个java对象的值,所以,还必须获取那个java对象的jobject指针.同 JNIEnv 指针一样,jobject指针也不能在多个线程中共享. 就是说,不能直接在保存一个线程中的jobject指针到全局变量中,然后在另外一个线程中使用它.幸运的是,可以用

gs_object=env->NewGlobalRef(obj);

来将传入的obj保存到gs_object中,从而其他线程可以使用这个gs_object来操纵那个java对象了.

示例代码如下:

(1)java代码:

//file name: Test.java

import java.io.*;

class Test  implements  Runnable

{

public int value  = 0;

private Thread tx=null;

public Test()

{

  tx=new Thread(this,"tx");

}

static

{

System.loadLibrary("Test");

}

   public native void setEnev();

public static void main(String args[])

{

  Test t = new Test();

  t.setEnev();

  System.out.println("ok in java main");

  t.tx.start ();

  try

  {

   Thread.sleep(10000000);

  }catch(Exception e)

  {

  System.out.println("error in main");

  }

}

public void run()

{

  try

  {

    while(true)

    {

      Thread.sleep(1000);

     System.out.println(value);

    }

  }catch(Exception e)

  {

   System.out.println("error in run");

  }

}

}

(2) DLL代码:

//cpp file name:  Test.cpp:

#include "test.h"

#include<windows.h>

#include<stdio.h>

static JavaVM *gs_jvm=NULL;

static jobject gs_object=NULL;

static int gs_i=10;

void WINAPI ThreadFun(PVOID argv)

{

JNIEnv *env;

gs_jvm->AttachCurrentThread((void **)&env, NULL);

jclass cls = env->GetObjectClass(gs_object);

jfieldID fieldPtr = env->GetFieldID(cls,"value","I");

while(1)

{

Sleep(100);

//在DLL中改变外面的java对象的value变量的值.

env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);

}

}

JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)

{

printf("come into test.dll/n");

//Returns “0” on success; returns a negative value on failure.

int retGvm=env->GetJavaVM(&gs_jvm);

//直接保存obj到DLL中的全局变量是不行的,应该调用以下函数:

gs_object=env->NewGlobalRef(obj);

HANDLE ht=CreateThread( NULL,0,

(LPTHREAD_START_ROUTINE)ThreadFun,0,

NULL,NULL);

printf("the Handle ht is:%d/n",ht);

}

chap 18:JNI在多线程中的应用

引文地址:http://blog.csdn.net/hust_liuX/archive/2006/12/25/1460486.aspx

我在这里将文章整理了一下,重新修改了部分描述和增加了一些重要的说明事项。修改文如下:

问题描述:

一个java对象通过JNI调用DLL中一个send()函数向服务器发送消息,不等服务器消息到来就立即返回,同时把JNI接口的指针JNIEnv *env(虚拟机环境指针),和jobject obj保存在DLL中的变量里.

一段时间后,DLL中的消息接收线程接收到服务器发来的消息,
并试图通过保存过的env和obj来调用先前的java对象的方法(相当于JAVA回调方法)来处理此消息.此时程序会突然退出(崩溃).

解决办法:

    解决此问题首先要明白造成这个问题的原因。那么崩溃的原因是什么呢?

JNI文档上有明确表述:  The JNIEnv pointer, passed as the first argument to every native method, can only be used in the thread with which it is associated. It is wrong to cache the JNIEnv interface pointer obtained from one thread, and use that pointer in another thread.

    意思就是JNIEnv指针不能直接在多线程中共享使用。上面描述的程序崩溃的原因就在这里:回调时的线程和之前保存变量的线程共享了这个JNIEnv *env指针和jobject obj变量。

http://java.sun.com/docs/books/jni/html/other.html#26206 提到,JNIEnv *env指针不可为多个线程共用,但是java虚拟机的JavaVM指针是整个jvm公用的,我们可以通过JavaVM来得到当前线程的JNIEnv指针。

于是,在第一个线程A中调用:

JavaVM* gs_jvm;

env->GetJavaVM(&gs_jvm); //来获取JavaVM指针.获取了这个指针后,将该JavaVM保存起来。

在另一个线程B里,调用

JNIEnv *env;

gs_jvm->AttachCurrentThread((void **)&env, NULL);

//这里就获得了B这个线程在jvm中的JNIEnv指针.

这里还必须获取那个java对象的jobject指针,因为我们要回调JAVA方法.同 JNIEnv 指针一样,jobject指针也不能在多个线程中共享. 就是说,不能直接在保存一个线程中的jobject指针到全局变量中,然后在另外一个线程中使用它.幸运的是,可以用  

  1. gs_object=env->NewGlobalRef(obj);//创建一个全局变量

来将传入的obj(局部变量)保存到gs_object中,从而其他线程可以使用这个gs_object(全局变量)来操纵这个java对象了.


示例代码如下:

(1)java代码:Test.java:

  1. import java.io.*;
  2. class Test implements Runnable
  3. {
  4. public int value   = 0;
  5. static{ System.loadLibrary("Test");}
  6. public native void     setEnev();//本地方法
  7. public static void     main(String args[]) throws Exception
  8. {
  9.     Test t = new Test();
  10.     t.setEnev(); //调用本地方法
  11.     while(true)
  12.      {
  13.            Thread.sleep(1000);
  14.            System.out.println(t.value);
  15.      }
  16.    }
  17. }

(2) DLL代码:Test.cpp:

<!--[if !supportLists]-->1.   <!--[endif]-->#include "test.h"

<!--[if !supportLists]-->2.   <!--[endif]-->#include<windows.h>

<!--[if !supportLists]-->3.   <!--[endif]-->#include<stdio.h>

<!--[if !supportLists]-->4.   <!--[endif]-->static JavaVM *gs_jvm=NULL;

<!--[if !supportLists]-->5.   <!--[endif]-->static jobject gs_object=NULL;

<!--[if !supportLists]-->6.   <!--[endif]-->static intgs_i=10;

<!--[if !supportLists]-->7.   <!--[endif]-->

<!--[if !supportLists]-->8.   <!--[endif]-->JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)

<!--[if !supportLists]-->9.   <!--[endif]-->{

<!--[if !supportLists]-->10.  <!--[endif]-->     env->GetJavaVM(&gs_jvm); //保存到全局变量中JVM

<!--[if !supportLists]-->11.  <!--[endif]-->    //直接赋值obj到DLL中的全局变量是不行的,应该调用以下函数:

<!--[if !supportLists]-->12.  <!--[endif]-->     gs_object=env->NewGlobalRef(obj);

<!--[if !supportLists]-->13.  <!--[endif]-->

<!--[if !supportLists]-->14.  <!--[endif]-->HANDLEht=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadFun,0,NULL,NULL);

<!--[if !supportLists]-->15.  <!--[endif]-->}

<!--[if !supportLists]-->16.  <!--[endif]-->

<!--[if !supportLists]-->17.  <!--[endif]-->void WINAPI ThreadFun(PVOIDargv)//JNI中线程回调这个方法

<!--[if !supportLists]-->18.  <!--[endif]-->{

<!--[if !supportLists]-->19.  <!--[endif]-->JNIEnv *env;

<!--[if !supportLists]-->20.  <!--[endif]-->gs_jvm->AttachCurrentThread((void**)&env, NULL);

<!--[if !supportLists]-->21.  <!--[endif]-->jclass cls = env->GetObjectClass(gs_object);

<!--[if !supportLists]-->22.  <!--[endif]-->jfieldID fieldPtr = env->GetFieldID(cls,"value","I");

<!--[if !supportLists]-->23.  <!--[endif]-->

<!--[if !supportLists]-->24.  <!--[endif]-->while(1)

<!--[if !supportLists]-->25.  <!--[endif]-->{

<!--[if !supportLists]-->26.  <!--[endif]-->     Sleep(100);

<!--[if !supportLists]-->27.  <!--[endif]-->   //这里改变JAVA对象的属性值(回调JAVA)

<!--[if !supportLists]-->28.  <!--[endif]-->   env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);

<!--[if !supportLists]-->29.  <!--[endif]-->   }

<!--[if !supportLists]-->30.  <!--[endif]-->}

<!--[if !supportLists]-->31.  <!--[endif]-->

<!--[if !supportLists]-->32.  <!--[endif]-->

JNI

There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example:

  • A JNIEnv pointer is only valid in the thread associated with it. You     must not pass this pointer from one thread to another, or cache and use it     in multiple threads. The Java virtual machine passes a native method the     same JNIEnv pointer in consecutive invocations from the same thread, but     passes different JNIEnv pointers when invoking that native method from different     threads. Avoid the common mistake of caching the JNIEnv pointer of one     thread and using the pointer in another thread.
  • Local references are valid only     in the thread that created them. You must not pass local references from     one thread to another. You should always convert local references to     global references whenever there is a possibility that multiple threads     may use the same reference.

chap 19:JNI限制(多线程)

JNI限制:
There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example:
A JNIEnv pointer is only valid in the thread associated with it. You must not pass this pointer from one thread to another, or cache and use it in multiple threads.

The Java virtual machine passes a native method the same JNIEnv pointer in consecutive invocations from the same thread, but passes different JNIEnv pointers when invoking that native method from different threads. Avoid the common mistake of caching the JNIEnv pointer of one thread and using the pointer in another thread.

Local references are valid only in the thread that created them. You must not pass local references from one thread to another. You should always convert local references to global references whenever there is a possibility that multiple threads may use the same reference.

                                 本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lovingprince/archive/2008/08/19/2793504.aspx

chap 20:使用 Java Native Interface 的最佳实践

JNI 的发展

JNI 自从 JDK 1.1 发行版以来一直是 Java 平台的一部分,并且在 JDK 1.2 发行版中得到了扩展。JDK 1.0 发行版包含一个早期的本机方法接口,但是未明确分隔本机代码和 Java 代码。在这个接口中,本机代码可以直接进入 JVM 结构,因此无法跨 JVM 实现、平台或者甚至各种 JDK 版本进行移植。使用 JDK 1.0 模型升级含有大量本机代码的应用程序,以及开发能支持多个 JVM 实现的本机代码的开销是极高的。

JDK 1.1 中引入的 JNI 支持:

  • 版本独立性
  • 平台独立性
  • VM 独立性
  • 开发第三方类库

有一个有趣的地方值得注意,一些较年轻的语言(如 PHP)在它们的本机代码支持方面仍然在努力克服这些问题。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

2009 年 7 月 27 日

Java™ 本机接口(Java Native Interface,JNI)是一个标准的Java API,它支持将 Java 代码与使用其他编程语言编写的代码相集成。如果您希望利用已有的代码资源,那么可以使用 JNI 作为您工具包中的关键组件 —— 比如在面向服务架构(SOA)和基于云的系统中。但是,如果在使用时未注意某些事项,则 JNI 会迅速导致应用程序性能低下且不稳定。本文将确定 10 大 JNI 编程缺陷,提供避免这些缺陷的最佳实践,并介绍可用于实现这些实践的工具。

Java 环境和语言对于应用程序开发来说是非常安全和高效的。但是,一些应用程序却需要执行纯 Java 程序无法完成的一些任务,比如:

  • 与旧有代码集成,避免重新编写。
  • 实现可用类库中所缺少的功能。举例来说,在 Java 语言中实现 ping 时,您可能需要 Internet Control     Message Protocol (ICMP) 功能,但基本类库并未提供它。
  • 最好与使用 C/C++ 编写的代码集成,以充分发掘性能或其他与环境相关的系统特性。
  • 解决需要非 Java 代码的特殊情况。举例来说,核心类库的实现可能需要跨包调用或者需要绕过其他 Java 安全性检查。

JNI 允许您完成这些任务。它明确分开了 Java 代码与本机代码(C/C++)的执行,定义了一个清晰的 API 在这两者之间进行通信。从很大程度上说,它避免了本机代码对 JVM 的直接内存引用,从而确保本机代码只需编写一次,并且可以跨不同的 JVM 实现或版本运行。

借助 JNI,本机代码可以随意与 Java 对象交互,获取和设计字段值,以及调用方法,而不会像 Java 代码中的相同功能那样受到诸多限制。这种自由是一把双刃剑:它牺牲 Java 代码的安全性,换取了完成上述所列任务的能力。在您的应用程序中使用 JNI 提供了强大的、对机器资源(内存、I/O 等)的低级访问,因此您不会像普通 Java 开发人员那样受到安全网的保护。JNI 的灵活性和强大性带来了一些编程实践上的风险,比如导致性能较差、出现 bug 甚至程序崩溃。您必须格外留意应用程序中的代码,并使用良好的实践来保障应用程序的总体完整性。

本文介绍 JNI 用户最常遇到的 10 大编码和设计错误。其目标是帮助您认识到并避免它们,以便您可以编写安全、高效、性能出众的 JNI 代码。本文还将介绍一些用于在新代码或已有代码中查找这些问题的工具和技巧,并展示如何有效地应用它们。

JNI 编程缺陷可以分为两类:

  • 性能:代码能执行所设计的功能,但运行缓慢或者以某种形式拖慢整个程序。
  • 正确性:代码有时能正常运行,但不能可靠地提供所需的功能;最坏的情况是造成程序崩溃或挂起。

1.性能缺陷

程序员在使用 JNI 时的 5 大性能缺陷如下:

1.1.不缓存方法 ID、字段 ID 和类

要访问 Java 对象的字段并调用它们的方法,本机代码必须调用 FindClass()GetFieldID()GetMethodId()GetStaticMethodID()。对于 GetFieldID()GetMethodID()GetStaticMethodID(), 为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。

举例来说,清单 1 展示了调用静态方法所需的 JNI 代码:


清单 1. 使用 JNI 调用静态方法

                               
int val=1;
jmethodID method;
jclass cls;
 
cls = (*env)->FindClass(env, "com/ibm/example/TestClass");
if ((*env)->ExceptionCheck(env)) {
   return ERR_FIND_CLASS_FAILED;
}
method = (*env)->GetStaticMethodID(env, cls, "setInfo", "(I)V");
if ((*env)->ExceptionCheck(env)) {
   return ERR_GET_STATIC_METHOD_FAILED;
}
(*env)->CallStaticVoidMethod(env, cls, method,val);
if ((*env)->ExceptionCheck(env)) {
   return ERR_CALL_STATIC_METHOD_FAILED;
}

当我们每次希望调用方法时查找类和方法 ID 都会产生六个本机调用,而不是第一次缓存类和方法 ID 时需要的两个调用。

缓存会对您应用程序的运行时造成显著的影响。考虑下面两个版本的方法,它们的作用是相同的。清单 2 使用了缓存的字段 ID:


清单 2. 使用缓存的字段 ID

                               
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){
 
   jint avalue = (*env)->GetIntField(env, allValues, a);
   jint bvalue = (*env)->GetIntField(env, allValues, b);
   jint cvalue = (*env)->GetIntField(env, allValues, c);
   jint dvalue = (*env)->GetIntField(env, allValues, d);
   jint evalue = (*env)->GetIntField(env, allValues, e);
   jint fvalue = (*env)->GetIntField(env, allValues, f);
 
   return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
}

<!--[if !vml]--><!--[endif]-->

性能技巧 #1

查找并全局缓存常用的类、字段 ID 和方法 ID。

清单 3 没有使用缓存的字段 ID:


清单 3. 未缓存字段 ID

                               
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){
   jclass cls = (*env)->GetObjectClass(env,allValues);
   jfieldID a = (*env)->GetFieldID(env, cls, "a", "I");
   jfieldID b = (*env)->GetFieldID(env, cls, "b", "I");
   jfieldID c = (*env)->GetFieldID(env, cls, "c", "I");
   jfieldID d = (*env)->GetFieldID(env, cls, "d", "I");
   jfieldID e = (*env)->GetFieldID(env, cls, "e", "I");
   jfieldID f = (*env)->GetFieldID(env, cls, "f", "I");
   jint avalue = (*env)->GetIntField(env, allValues, a);
   jint bvalue = (*env)->GetIntField(env, allValues, b);
   jint cvalue = (*env)->GetIntField(env, allValues, c);
   jint dvalue = (*env)->GetIntField(env, allValues, d);
   jint evalue = (*env)->GetIntField(env, allValues, e);
   jint fvalue = (*env)->GetIntField(env, allValues, f);
   return avalue + bvalue + cvalue + dvalue + evalue + fvalue
}

清单 2 用3,572 ms 运行了 10,000,000 次。清单 3 用了 86,217 ms — 多花了 24 倍的时间。

1.2.触发数组副本

JNI 在 Java 代码和本机代码之间提供了一个干净的接口。为了维持这种分离,数组将作为不透明的句柄传递,并且本机代码必须回调 JVM 以便使用 set 和 get 调用操作数组元素。Java 规范让 JVM 实现决定让这些调用提供对数组的直接访问,还是返回一个数组副本。举例来说,当数组经过优化而不需要连续存储时,JVM 可以返回一个副本。(参见 参考资料 获取关于JVM 的信息)。

随后,这些调用可以复制被操作的元素。举例来说,如果您对含有 1,000 个元素的数组调用 GetLongArrayElements(),则会造成至少分配或复制8,000 字节的数据(每个 long 1,000 元素 * 8 字节)。当您随后使用 ReleaseLongArrayElements() 更新数组的内容时,需要另外复制 8,000 字节的数据来更新数组。即使您使用较新的 GetPrimitiveArrayCritical(),规范仍然准许 JVM 创建完整数组的副本。

<!--[if !vml]--><!--[endif]-->

性能技巧 #2

获取和更新仅本机代码需要的数组部分。在只要数组的一部分时通过适当的 API 调用来避免复制整个数组。

GetTypeArrayRegion()SetTypeArrayRegion() 方法允许您获取和更新数组的一部分,而不是整个数组。通过使用这些方法访问较大的数组,您可以确保只复制本机代码将要实际使用的数组部分。

举例来说,考虑相同方法的两个版本,如清单 4 所示:


清单 4. 相同方法的两个版本

                               
jlong getElement(JNIEnv* env, jobject obj, jlongArray arr_j, 
                 int element){
   jboolean isCopy;
   jlong result;
   jlong* buffer_j = (*env)->GetLongArrayElements(env, arr_j, &isCopy);
   result = buffer_j[element];
   (*env)->ReleaseLongArrayElements(env, arr_j, buffer_j, 0);
   return result;
}
 
jlong getElement2(JNIEnv* env, jobject obj, jlongArray arr_j, 
                  int element){
     jlong result;
     (*env)->GetLongArrayRegion(env, arr_j, element,1, &result);
     return result;
}

第一个版本可以生成两个完整的数组副本,而第二个版本则完全没有复制数组。当数组大小为 1,000 字节时,运行第一个方法 10,000,000 次用了 12,055 ms;而第二个版本仅用了 1,421 ms。第一个版本多花了 8.5 倍的时间!

<!--[if !vml]--><!--[endif]-->

性能技巧 #3

在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。

另一方面,如果您最终要获取数组中的所有元素,则使用 GetTypeArrayRegion() 逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则清单 4 中这两个 getElement() 方法都不适用。比较好的方法是在一个调用中获取大小合理的数组部分,然后再迭代所有这些元素,重复操作直到覆盖整个数组。

1.3.回访而不是传递参数

在 调用某个方法时,您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。在面向对象设计中,传递对象通常能提供较好的封装,因为对象字段的变 化不需要改变方法签名。但是,对于 JNI 来说,本机代码必须通过一个或多个 JNI 调用返回到 JVM 以获取需要的各个字段的值。这些额外的调用会带来额外的开销,因为从本机代码过渡到Java 代码要比普通方法调用开销更大。因此,对于 JNI 来说,本机代码从传递进来的对象中访问大量单独字段时会导致性能降低。

考虑清单 5 中的两个方法,第二个方法假定我们缓存了字段 ID:


清单 5. 两个方法版本

                               
int sumValues(JNIEnv* env, jobject obj, jint a, jint b,jint c, jint d, jint e, jint f){
   return a + b + c + d + e + f;
}
 
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){
 
   jint avalue = (*env)->GetIntField(env, allValues, a);
   jint bvalue = (*env)->GetIntField(env, allValues, b);
   jint cvalue = (*env)->GetIntField(env, allValues, c);
   jint dvalue = (*env)->GetIntField(env, allValues, d);
   jint evalue = (*env)->GetIntField(env, allValues, e);
   jint fvalue = (*env)->GetIntField(env, allValues, f);
   
   return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
}

<!--[if !vml]--><!--[endif]-->

性能技巧 #4

如果可能,将各参数传递给 JNI 本机代码,以便本机代码回调 JVM 获取所需的数据。

sumValues2() 方法需要 6 个 JNI 回调,并且运行10,000,000 次需要 3,572 ms。其速度比 sumValues() 慢6 倍,后者只需要 596 ms。通过传递 JNI 方法所需的数据,sumValues() 避免了大量的 JNI 开销。

1.4.错误认定本机代码与 Java 代码之间的界限

本 机代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本机代码以及从本机代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本机代码要比普通调用多花 5 倍的时间。同样,从本机代码中调用 Java 代码也需要耗费大量的时间。

<!--[if !vml]--><!--[endif]-->

性能技巧 #5

定义 Java 代码与本机代码之间的界限,最大限度地减少两者之间的互相调用。

因 此,在设计 Java 代码与本机代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用,并且应该竭力在本机代码中弥补越界调用造成的成本损失。最大限 度地减少越界调用的一个关键因素是确保数据处于 Java/本机界限的正确一侧。如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。

举例来说,如果我们希望使用 JNI 为某个串行端口提供接口,则可以构造两种不同的接口。第一个版本如清单 6 所示:


清单 6. 到串行端口的接口:版本 1

                               
/**
 * Initializes the serial port and returns a java SerialPortConfig objects
 * that contains the hardware address for the serial port, and holds
 * information needed by the serial port such as the next buffer 
 * to write data into
 * 
 * @param env JNI env that can be used by the method
 * @param comPortName the name of the serial port
 * @returns SerialPortConfig object to be passed ot setSerialPortBit 
 *          and getSerialPortBit calls
 */
jobject initializeSerialPort(JNIEnv* env, jobject obj,  jstring comPortName);
 
/**
 * Sets a single bit in an 8 bit byte to be sent by the serial port
 *
 * @param env JNI env that can be used by the method
 * @param serialPortConfig object returned by initializeSerialPort
 * @param whichBit value from 1-8 indicating which bit to set
 * @param bitValue 0th bit contains bit value to be set 
 */
void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, 
  jint whichBit,  jint bitValue);
 
/**
 * Gets a single bit in an 8 bit byte read from the serial port
 *
 * @param env JNI env that can be used by the method
 * @param serialPortConfig object returned by initializeSerialPort
 * @param whichBit value from 1-8 indicating which bit to read
 * @returns the bit read in the 0th bit of the jint 
 */
jint getSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, 
  jint whichBit);
 
/**
 * Read the next byte from the serial port
 * 
 * @param env JNI env that can be used by the method
 */
void readNextByte(JNIEnv* env, jobject obj);
 
/**
 * Send the next byte
 *
 * @param env JNI env that can be used by the method
 */
void sendNextByte(JNIEnv* env, jobject obj);

清单 6 中,串行端口的所有配置数据都存储在由 initializeSerialPort() 方法返回的 Java 对象中,并且将 Java 代码完全控制对硬件中各数据位的设置。清单 6 所示版本的一些问题会造成其性能差于清单 7 中的版本:


清单 7. 到串行端口的接口:版本 2

                               
/**
 * Initializes the serial port and returns an opaque handle to a native
 * structure that contains the hardware address for the serial port 
 * and holds information needed by the serial port such as 
 * the next buffer to write data into
 *
 * @param env JNI env that can be used by the method
 * @param comPortName the name of the serial port
 * @returns opaque handle to be passed to setSerialPortByte and 
 *          getSerialPortByte calls 
 */
jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName);
 
/**
 * sends a byte on the serial port
 * 
 * @param env JNI env that can be used by the method
 * @param serialPortConfig opaque handle for the serial port
 * @param byte the byte to be sent
 */
void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig, 
    jbyte byte);
 
/**
 * Reads the next byte from the serial port
 * 
 * @param env JNI env that can be used by the method
 * @param serialPortConfig opaque handle for the serial port
 * @returns the byte read from the serial port
 */
jbyte readSerialPortByte(JNIEnv* env, jobject obj,  jlong serialPortConfig);

<!--[if !vml]--><!--[endif]-->

性能技巧 #6

构造应用程序的数据,使它位于界限的正确的侧,并且可以由使用它的代码访问,而不需要大量跨界调用。

最显著的一个问题就是,清单 6 中的接口在设置或检索每个位,以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是,清单 6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据;将它存储在 Java 侧会导致本机代码向 Java 代码发起大量回调以获取/设置此配置信息。清单 7 将配置信息存储在一个本机结构中(比如,一个struct),并向 Java 代码返回了一个不透明的句柄,该句柄可以在后续调用中返回。这意味着,当本机代码正在运行时,它可以直接访问该结构,而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此,使用 清单 7 的实现的性能将大大改善。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值