JNI设计实践之路

JNI编程完全指南(一):类型系统与基础数据类型 本文深入解析JNI类型系统设计,重点介绍基本数据类型及其映射机制。JNI通过定义jboolean、jbyte等固定大小类型,确保Java与C/C++之间的类型安全转换和跨平台一致性。文章详细分析了8种基本数据类型(jboolean-jdouble)的源码定义、特性和使用规范,并提供了类型映射表展示Java-JNI-Native的三方对应关系。特别强调了jvalue联合体的设计用途,它支持通用方法调用和参数传递。通过类型大小保证和实战示例,帮助开发者正确使用JNI数据类型,为后续JNI编程打下基础。 阅读详情
JNI 设计实践之路
作者:杨小华  
一、       前言
本文为在 32 位 Windows 平台上实现 Java 本地方法提供了实用的示例、步骤和准则。本文中的示例使用 Sun公司的 Java Development Kit (JDK) 版本 1.4.2。 用 C ++语言编写的本地代码是用 Microsoft Visual C++  6.0编译器编译生成。规定在Java程序中function/method称为方法,在C++程序中称为函数。
本文将围绕求圆面积逐步展开,探讨java程序如何调用现有的DLL?如何在C++程序中 创建,检查及更新 Java 对象?如何在 C++ 和 Java 程序中互抛异常,并进行异常处理?最后将探讨 Eclipse 及 JBuilder 工具可执行文件为什么不到 100K 大小以及所采用的技术方案?
二、       JNI基础知识简介
Java 语言及其标准 API 应付应用程序的编写已绰绰有余。但在某些情况下,还是必须使用非 Java 代码,例如:打印、图像转换、访问硬件、访问现有的非 Java 代码等。与非 Java 代码的沟通要求获得编译器和 JVM 的专门支持,并需附加的工具将 Java 代码映射成非 Java 代码。目前,不同的开发商为我们提供了不同的方案,主要有以下方法:
1.         JNI ( Java Native Interface )
2.         JRI ( Java Runtime Interface )
3.         J/Direct
4.         RNI ( Raw Native Interface )
5.         Java/COM 集成方案
6.         CORBA ( Common Object Request Broker Architecture )
其中方案 1 是 JDK 自带的一部分,方案 2 由网景公司所提供,方案 3 、 4 、 5 是微软所提供的方案,方案 6 是一家非盈利组织开发的一种集成技术,利用它可以在由不同语言实现的对象之间进行“相互操作”的能力。
在开发过程中,我们一般采用第 1 种方案―― JNI 技术。因为只用当程序用 Microsoft Java 编译器编译,而且只有在 Microsoft Java 虚拟机 (JVM) 上运行的时候,才采用方案 3 、 4 、 5 。而方案 6 一般应用在大型的分布式应用中。
       JNI 是一种包容极广的编程接口,允许我们从 Java 应用程序里调用本地化方法。也就是说, JNI 允许运行在虚拟机上的 Java 程序能够与其它语言(例如 C/ C++/ 汇编语言)编写的程序或者类库进行相互间的调用。同时 JNI 也提供了一整套的 API ,允许将 Java 虚拟机直接嵌入到本地的应用程序中。 其中 JNI 所扮演的角色可用图一描述:
 
图一 JNI 基本结构描述图
目前 JNI 只能与用 C 和 C ++编写的本地化方法打交道。利用 JNI ,我们本地化方法可以:
1.         创建、检查及更新 Java 对象
2.         调用 Java 和非 Java 程序所编写的方法 ( 函数 ) ,以及 win32 API 等 .
3.         捕获和抛出“异常”
4.         装载类并获取类信息
5.         进行运行期类型检查
所以,原来在 Java 程序中能对类及对象所做的几乎所有事情都可以在本地化方法中实现。
下图表示了通过 JNI , Java 程序和非 Java 程序相互调用原理。
图二    Java 程序和非 Java 程序通过 JNI 相互调用原理
       通过 JNI ,编写 Java 程序调用非 Java 程序一般步骤:
1.)    编写对本地化方法及自变量进行声明的 Java 代码
2.)    利用头文件生成器 javah 生成本地化方法对应的头文件
3.)    利用 C 和 C++ 实现本地化方法(可调用非 Java 程序),并编译、链接生成 DLL 文件
4.)    Java 程序通过生成的 DLL 调用非 Java 程序
     同时我们也可以通过 JNI , 将 Java 虚拟机直接嵌入到本地的应用程序中, 步骤很简单,只需要在 C/C++ 程序中以 JNI API 函数为媒介调用 Java 程序。
以上步骤虽简单,但有很多地方值得注意。如果一招不慎,可能造成满盘皆输。
三、       Java程序调用非Java程序
3.1  本地化方法声明及头文件生成
任务:现有一求圆面积的 Circle.dll (用 MFC 编写,参数:圆半径 返回值:圆面积)文件,在 Java 程序中调用该 Dll 。
在本地化声明中,可分无包和有包两种情况。我们主要对有包的情况展开讨论。
实例 1 :
package com.testJni;
public class Circle
{
   public native void cAreas(int radius) ;
   static
   {
        //System.out.println(System.getProperty("java.library.path"));
        System.loadLibrary("CCircle");
    }
}
在 Java 程序中,需要在类中声明所调用的库名称 System.loadLibrary( String libname );
该函数是将一个 Dll/so 库载入内存 ,并建立同它的链接。 定位库的操作依赖于具体的操作系统。在 windows 下,首先从当前目录查找,然后再搜寻 ”PATH” 环境变量列出的目录。如果找不到该库,则会抛出异常 UnsatisfiedLinkError 。库的扩展名可以不用写出来,究竟是 Dll 还是 so ,由系统自己判断。 这里加载的是 3.2 中生成的 DLL ,而不是其他应用程序生成的 Dll 。 还需要对将要调用的方法做本地声明,关键字为 native 。表明此方法在本地方法中实现,而不是在 Java 程序中,有点类似于关键字 abstract 。
    我们写一个 Circle.bat 批处理文件编译 Circle.java 文件 ,内容如下 ( 可以用其他工具编译 ) :
javac -d . Circle.java
javah com.testJni.Circle
pause
对于有包的情况一定要注意这一点 ,就是在用 javah 时有所不同。开始时我的程序始终运行都不成功,问题就出在这里。本类名称的前面均是包名。这样生成的头文件就是: com_testJni_Circle.h 。 开始时,在包含包的情况下我用 javah Circle 生成的头文件始终是 Circle.h 。在网上查资料时,看见别人的头文件名砸那长,我的那短。但不知道为什么,现在大家和我一样知道为什么了吧。:)。
如果是无包的情况,则将批处理文件换成如下内容:
javac Circle.java
javah Circle
pause
3.2  本地化方法实现
刚才生成的 com_testJni_Circle.h 头文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_testJni_Circle */
#ifndef _Included_com_testJni_Circle
#define _Included_com_testJni_Circle
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_testJni_Circle
 * Method:    cAreas
 * Signature:  (I)V
 */
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas
 (JNIEnv *, jobject, jint);
#ifdef __cplusplus
}
#endif
#endif
如果在本地化方法声明中,方法 cAreas () 声明为 static 类型,则与之相对应的 Java_com_testJni_Circle_cAreas() 函数中的第二个参数类型为 jclass 。也就是
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jclass newCircle,jint radius) 。
这里 JNIEXPORT 和 JNICALL 都是 JNI 的关键字,其实是一些宏 ( 具体参看 jni_md.h 文件 ) 。
从以上头文件中,可以看出函数名生成规则为: Java[ _ 包名 ]_ 类名 _ 方法名 [ _ 函数签名 ] ( 其中 [ ] 是可选项 ) ,均以字符下划线 ( _ ) 分割 。如果是无包的情况,则不包含 [ _ 包名 ] 选项。 如果本地化方法中有方法重载,则在该函数名最后面追加函数签名,也就是 Signature 对应的值, 函数签名参见表一。
 
函数签名
Java 类型
V
void
Z
boolean
B
byte
C
char
S
short
I
int
J
long
F
float
D
double
L fully-qualified-class ;
fully-qualified-class
[ type
type []
( arg-types ) ret-type
method type
表一 函数签名与 Java 类型的 映射
在具体实现的时候,我们只关心函数原型: 
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *, jobject, jint);
现在就让我们开始激动人心的一步吧 : ) 。启动 VC 集成开发环境,新建一工程,在 project 里选择 win32 Dynamic-link Library ,输入工程名,然后点击 ok ,接下去步骤均取默认 ( 图三 ) 。如果不取默认,生成的工程将会有 DllMain () 函数,反之将无这个函数。我在这里取的是空。
图三    新建 DLL 工程
然后选择菜单 File->new->Files->C++ Source File ,生成一个空 *.cpp 文件,取名为 CCircle 。与 3.1 中 System.loadLibrary("CCircle"); 参数保持一致。将 JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *, jobject, jint); 拷贝到 CPP 文件中,并包含其头文件。
对应的 CCircle.cpp 内容如下:
#include<iostream.h>
#include"com_testJni_Circle.h"
#include"windows.h"
 
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jobject newCircle,jint radius)
{
     // 调用求圆面积的 Circle.dll
     typedef void (*PCircle)(int radius);
     HINSTANCE hDLL;
     PCircle Circle;
     hDLL=LoadLibrary("Circle.dll");// 加载动态链接库 Circle.dll 文件
     Circle=(PCircle)GetProcAddress(hDLL,"Circle");
     Circle(8);
     FreeLibrary(hDLL);// 卸载 Circle.dll 文件;
}
 
在编译前一定要注意下列情况。
注意: 一定要把 SDK 目录下 include 文件夹及其下面的 win32 文件夹中的头文件拷贝到 VC 目录的 include 文件夹下。或者在 VC 的 tools/options/directories 中设置 ,如图四所示。
图四    头文件设置
我们知道 dll 文件有两种指明导出函数的方法,一种是在 .def 文件中定义,另一种是在定义函数时使用关键字 __declspec(dllexport) 。而关键字 JNIEXPORT 实际在 jni_md.h 中如下定义, #define JNIEXPORT __declspec(dllexport) ,可见 JNI 默认的导出函数使用第二种。使用第二种方式产生的导出函数名会根据编译器发生变化,在有的情况下会发生找不到导出函数的问题(我们在 java 控制台程序中调用很正常,但把它移植到 JSP 页面时,就发生了该问题, JVM 开始崩溃,百思不得其解,后来加入一个 .def 文件才解决问题)。其实在《 windows 核心编程》一书中,第 19.3.2 节就明确指出创建用于非 Visual C++ 工具的 DLL 时,建议加入一个 def 文件,告诉 Microsoft 编译器输出没有经过改变的函数名。因此最好采用第一种方法,定义一个 .def 文件来指明导出函数。本例中可以新建一个 CCircle.def 文件,内容如下:
; CCircle.def : Declares the module parameters for the DLL.
 
LIBRARY      "CCircle"
DESCRIPTION 'CCircle Windows Dynamic Link Library'
 
EXPORTS
    ; Explicit exports can go here
    Java_com_testJni_Circle_cAreas
现在开始对所写的程序进行编译。选择 build->rebuild all 对所写的程序进行编译。点击 build->build CCirclee.DLL 生成 DLL 文件。
也可以用命令行cl来编译。语法格式参见JDK文档JNI部分。
再次强调(曾经为这个东西大伤脑筋) : DLL 放置地方
1)        当前目录。
2)        Windows 的系统目录及 Windows 目录
3)        放在 path 所指的路径中
4)        自己在 path 环境变量中设置一个路径,要注意所 指引的路径应该到 .dll 文件的上一级,如果指到 .dll ,则会报错。
下面就开始测试我们的所写的 DLL 吧(假设 DLL 已放置正确)。
import com.testJni.Circle;
public class test
{
   public static void main(String argvs[])
   {
      Circle myCircle;
      myCircle = new Circle();
      myCircle.cAreas(2);
   }     
}
编译,运行程序,将会弹出如下界面:

图五   运行结果
以上是我们通过 JNI 方法调用的一个简单程序。实际情况要比这复杂的多。
现在开始来讨论 JNI 中参数的情况,我们来看一个程序片断。
实例二:
JNIEXPORT jstring JNICALL Java_MyNative_cToJava
(JNIEnv *env, jclass obj)
{
              jstring jstr;
              char str[]="Hello,word!/n";
              jstr=env->NewStringUTF(str);
              return jstr;
}
在 C 和 Java 编程语言之间传送值时,需要理解这些值类型在这两种语言间的对应关系。这些都在头文件 jni.h 中定义,用 typedef 语句声明了这些类在目标平台上的代价类。头文件也定义了常量如: JNI_FALSE=0 和 JNI_TRUE=1; 表二和表三说明了 Java 类型和 C 类型之间的映射关系。
Java 语言
C/C++ 语言
bit 位数
boolean
jboolean
8 unsigned
byte
jbyte
8
char
jchar
16 unsigned
short
jshort
16
int
jint
32
long
jlong
64
float
jfloat
32
double
jdouble
64
void
void
0
表二    Java 基本类型到本地类型的映射
表三 Java 中的类到本地类的映射
JNI 函数 NewStringUTF() 是从一个包含 UTF 格式编码字符的 char 类型数组中创建一个新的 jstring 对象。 jstring 是以 JNI 为中介使 Java 的 String 类型与本地的 string 沟通的一种类型,我们可以视而不见 ( 具体对应见表二和表三 ) 。 如果你 使用的函数是 GetStringUTFChars()( 将 jstring 转换为 UTF-8 字符串 ) ,必须同时使用 ReleaseStringUTFChars() 函数,通过它来通知虚拟机去回收 UTF-8 串占用的内存,否则将会造成内存泄漏,最终导致系统崩溃。因为 JVM 在调用本地方法时,是在虚拟机中开辟了一块本地方法栈供本地方法使用,当本地方法使用完 UTF-8 串后,得释放所占用的内存。其中程序片断 jstr=env->NewStringUTF(str); 是 C++ 中的写法,不必使用 env 指针。因为 JNIEnv 函数的 C++ 版本包含有直接插入成员函数,他们负责查找函数指针。而对于 C 的写法,应改为: jstr=(*env)->NewStringUTF(env,str); 因为所有 JNI 函数的调用都使用 env 指针,它是任意一个本地方法的第一个参数。 env 指针是指向一个函数指针表的指针。因此在每个 JNI 函数访问前加前缀 (*env)-> , 以确保间接引用函数指针。
C/C++ 和 Java 互传参数需要自己在编程过程中仔细摸索与体味。
四、       C/C++访问Java成员变量和成员方法
我们修改 3.1 中的 Java 程序声明,加入如下代码:
   private int circleRadius;
   public Circle()
   {
       circleRadius=0;
   }
   public void setCircleRadius(int radius)
   {
       circleRadius=radius;
   }
   public void javaAreas()
   {
       float PI = 3.14f;
       if(circleRadius<=0)
       {
           System.out.println (“error!”);
       }
       else
       {
          System.out.println (PI*circleRadius*circleRadius);
       }      
    }
在 C++ 程序中访问 Circle 类中的 private 私有成员变量 circleRadius ,并设置它的值,同时调用 Java 方法 javaAreas() 。在函数 Java_com_testJni_Circle_cAreas() 中加入如下代码:
jclass circle;
       jmethodID AreasID;
       jfieldID radiusID;
jint newRadius=5;
       circle = env->GetObjectClass(newCircle);//get current class
       radiusID=env->GetFieldID(circle,"circleRadius","I");//get field ID
       env->SetIntField(newCircle,radiusID,newRadius);//set field value
       AreasID=env->GetMethodID(circle,"javaAreas","()V");//get method ID
       env->CallVoidMethod(newCircle,AreasID,NULL);//invoking method
在 C++ 代码中,创建、检查及更新 Java 对象,首先要得到该类,然后再根据类得到其成员的 ID ,最后根据该类的对象, ID 号调用成员变量或者成员方法。
得到类,有两个 API 函数,分别为 FindClass() 和 GetObjectClass(); 后者顾名思义用于已经明确知道其对象,然后根据对象找类。前者用于得到没有实例对象的类。这里也可以改成 circle = env-> FidnClass("com/testJni/Circle"); 其中包的分隔符用字符 " /" 代替。如果已知一个类,也可以在 C++ 代码中创建该类对象,其 JNI 函数为 NewObject(); 示例代码如下:
jclass      circle =env->FindClass("com/testJni/ Circle ");
jmethodID circleID=env->GetMethodID(circle,"<init>","()V");// 得到构造函数的 ID
jobject newException=env->NewObject(circle, circleID,NULL);
得到成员变量的 ID ,根据其在 Java 代码中声明的类型不同而不同。具体分为两大类:非 static 型和 static 型,分别对应 GetFieldID() 和 GetStaticFieldID() 。同时也可以获得和设置成员变量的值,根据其声明的 type 而变化,获得其值的 API 函数为: Get type Field () 和 GetStatic type Field () ;与之相对应的设置其值的函数为 Set type Field () 和 SetStatic type Field () ;在本例中,成员变量 circleRadius 声明成 int 型,则对应的函数分别为 GetIntField() 和 SetIntField();
其实 JNI API 函数名是很有规律的,从上面已窥全貌。获得成员方法的 ID 也是同样的分类方法。具体为 GetMethodID() 和 GetStaticMethodID() 。调用成员方法跟获得成员变量的值相类似,也根据其方法返回值的 type 不同而不同,分别为 Call type Method () 和 CallStatic type Method () 。对于返回值为 void 的类型,其相应 JNI 函数为 CallVoidMethod() ;
以上获得成员 ID 函数的形参均一致。第一个参数为 jclass ,第二个参数为成员变量或方法,第三个参数为该成员的签名 ( 签名可参见表一 ) 。但调用或设置成员变量或方法时,第一个参数为实例对象 ( 即 jobject) ,其余形参与上面相同。
特别要注意的是得到构造方法的 ID 时,第二个参数不遵循上面的原则,为 jmethodID constructorID = env->GetMethodID(jclass, "<init>"," 函数签名 ");
从上面代码中可以看出,在 C++ 中可以访问 java 程序 private 类型的变量,严重破坏了类的封装原则。从而可以看出其不安全性。
五、       异常处理
本地化方法稳定性非常差,调用任何一个 JNI 函数都会出错,为了程序的健壮性,非常有必要在本地化方法中加入异常处理。我们继续修改上面的类。
我们声明一个异常类,其代码如下:
package com.testJni;
import com.testJni.*;
public class RadiusIllegal extends Exception
{
    protected String MSG="error!";
    public RadiusIllegal(String message)
    {
        MSG=message;
    }
    public void print()
    {
       System.out.println(MSG);
    }
}
同时也修改 Circle.java 中的方法 , 加入异常处理。
    public void javaAreas() throws RadiusIllegal    // 修改 javaAreas() ,加入异常处理
   {
       float PI = 3.14f;
       if(circleRadius<=0)
       {
           throw new RadiusIllegal("warning:radius is illegal!");
       }
       else
       {
            System.out.println (PI*circleRadius*circleRadius);
       }      
   }
   public native void cAreas(int radius) throws RadiusIllegal;    // 修改 cAreas () ,加入异常处理
   修改 C++ 代码中的函数,加入异常处理,实现 Java 和 C++ 互抛异常,并进行异常处理。
   JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jobject newCircle,jint radius)
{
       // 此处省略部分代码
       radiusIllegal=env->FindClass("com/testJni/RadiusIllegal");//get the exception class
       if((exception=env->ExceptionOccurred())!=NULL)
       {
              cout<<"errors in com_testJni_RadiusIllegal"<<endl;
              env->ExceptionClear();
       }    
       // 此处省略部分代码
       env->CallVoidMethod(newCircle,AreasID,NULL);//invoking
       if((exception=env->ExceptionOccurred())!=NULL)
       {
              if(env->IsInstanceOf(exception,radiusIllegal)==JNI_TRUE)
              {                  
                     cout<<"errors in java method"<<endl;
                     env->ExceptionClear();
              }
              else
              {
                     cout<<"errors in invoking javaAreas() method of Circle"<<endl;
                     env->ExceptionClear();
              }
        }   
        if(radius<=0)
        {
              env->ThrowNew(radiusIllegal,"errors in C function!");//throw exception
         return ;
        }
        else
        {
              // 此处为调用计算圆面积的 DLL
        }
}
在本地化方法 (C++) 中,可以自己处理异常,也可以重新抛出异常,让 Java 程序来捕获该异常,进行相关处理。
如果调用 JNI 函数发生异常,不及时进行处理,再次调用其他 JNI 函数时,可能会使 JVM 崩溃 (crash),
大多数 JNI 函数都具有此特性。可以调用函数 ExceptionOccurred() 来判断是否发生了异常。该函数返回 jthrowable 的实例对象,如本例 if((exception=env->ExceptionOccurred())!=NULL) 就用来判断是否发生了异常。当要判断具体是哪个异常发生时,可以用 IsInstanceOf() 来进行测试,此函数非彼 IsInstanceOf(Java 语言中的 IsInstanceOf) 。在上面的代码中,我们在本地化方法中给 circleRadius 设置了一非法值,然后调用方法 javaAreas(), 此时 java 代码会抛出异常,在本地化方法中进行捕获,然后用 IsInstanceOf() 来进行测试是否发生了 RadiusIllegal 类型的异常,以便进行相关处理。在调用其他 JNI 函数之前,应当首先清除异常,其函数为 ExceptionClear() 。
如果是 C++ 的程序发生异常,则可以用 JNI API 函数 ThrowNew() 抛出该异常。但此时本地化方法并不返回退出,直到该程序执行完毕。所以当在本地化方法中发生异常时,应该人为的退出,及时进行处理,避免程序崩溃。函数 ThrowNew() 中第一个参数为 jclass 的类,第二个参数为附加信息,用来描述异常信息。
如果要知道异常发生的详细信息,或者对程序进行调试时,可以用函数 ExceptionDescribe() 来显示异常栈里面的内容。
 
六、       MFC程序中嵌入Java虚拟机
可能大家天天都在用 Eclipse 和 Jbulider 这两款优秀的 IDE 进行程序开发,可能还不知道他们的可执行文件不到 100KB 大小,甚则连一副图片都可能比他们大。其实隐藏在他们背后的技术是 JNI ,可执行文件只是去启动 Java 程序,所以也只有那么小。
我们只需要在 MFC 程序中创建一个 JVM ,然后基于这个 JVM 调用 Java 的方法,启动 Java 程序,就可以模拟出 Eclipse 和 Jbulider 的那种效果,使 java 程序更专业。其实要实现这种效果,用上面的方法足有够有。创建 JVM, 只需包含相应的类库,设置相关的属性。
首先进行环境设置 , 在 VC 环境的 tools-->options-->Directories 下的 Library files 选项中包含其创建 JVM 的库文件 jvm.lib ,该库文件位于 JDK / lib 目录下,如图 6 所示:
图六 库文件路径设置
然后,在环境变量 path 中设置 jvm.dll 的路径。该 Dll      位于 jdk/jre/bin/server 目录或 jdk/jre/bin/client 目录下。注意:一定不要将 jvm.dll 和 jvm.lib 拷贝到你应用程序路径下 ,这样会引起 JVM 初始化失败。因为 Java 虚拟机是以相对路径来寻找和调用用到的库文件和其他相关文件。
接下来,我们在 MFC 程序 ( 该程序请到《程序员》杂志频道下载 ) 中进行创建 JVM 初始化工作。示例代码如下:
       JNIEnv *env;
       JavaVM *jvm;
       jint res;
       jclass cls;
       jmethodID mid;      
       JavaVMInitArgs vm_args;
       JavaVMOption options[3];
       memset(&vm_args, 0, sizeof(vm_args));    
     // 进行初始化工作       
       options[0].optionString = "-Djava.compiler=NONE";
       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); // 创建 JVM
       if (res < 0)
       {
              MessageBox( "Can't create Java VM","Error",MB_OK|MB_ICONERROR);
              exit(1);
       }
       cls = env->FindClass("prog");
       if(env->ExceptionOccurred()!=NULL)
       {
              MessageBox( "Can't find Prog class!","Error",MB_OK|MB_ICONERROR);
              exit(1);
       }
       mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V");
       if(env->ExceptionOccurred()!=NULL)
       {
              MessageBox("Can't find Prog.main!","Error",MB_OK|MB_ICONERROR);
              exit(1);
       }
       env->CallStaticVoidMethod( cls, mid, NULL); // 调用 Java 程序 main() 方法,启动 Java 程序
       if(env->ExceptionOccurred()!=NULL)
       {
              MessageBox( "Fatal Error!","Error",MB_OK|MB_ICONERROR);
              exit(1);
       }
       jvm->DestroyJavaVM();// 释放 JVM 资源
程序首先进行 JVM 初始化设置。我们观察 jni.h 文件关于 JavaVMOption 和 JavaVMInitArgs 的定义
typedef struct JavaVMOption {
    char *optionString;
    void *extraInfo;
} JavaVMOption;
 
typedef struct JavaVMInitArgs {
    jint version;
    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;
结构体 JavaVMInitArgs 中有四个参数,我们在程序中都得必须设置。其中版本号一定要设置正确,不同的版本有不同的设置方法,关于版本 1.1 和 1.2 的设置方法参看 sun 公司的文档,这里只给出版本 1.4 的设置方法。第二个参数表示 JavaVMOption 结构体变量的维数,这里设置为三维,其中 options[0].optionString = "-Djava.compiler=NONE"; 表示 disable JIT;options[1].optionString = "-Djava.class.path=."; 表示你所调用 Java 程序的 Class 文件的路径,这里设置为该 exe 应用程序的根路径 ( 最后一个字符 "." 表示根路径 ) ; options[2].optionString = "-verbose:jni"; 用于跟踪运行时的信息 。第三个参数是一个 JavaVMOption 的指针变量。第四个参数意思我们可以参看帮助文档的解释 If ignoreUnrecognized is JNI_FALSE , JNI_CreateJavaVM returns JNI_ERR as soon as it encounters any unrecognized option strings 。
初始化完毕后,就可以调用创建 JVM 的函数 jint JNICALL JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args); 如果返回值小于 0 表示创建 JVM 失败。最可能的原因就是 jvm.dll 和 jvm.lib 设置错误。
如果在运行的过程中找不到 java 程序的类,那么就是 -Djava.class.path 设置错误。只要 JVM 创建成功,就可以根据上面的方法调用 java 程序。最后当程序结束后,调用函数 DestroyJavaVM() 摧毁 JVM ,释放资源。
七、       附录
利用 JNI 函数,我们可以从本地化方法的内部与 JVM 打交道。正如在前面的例子中所看到的那样,每个本地化方法中都会接收一个特殊的自变量作为自己的第一个参数: JNIEnv ――它是指向类型为 JNIEnv_ 的一个特殊 JNI 数据结构的指针。 JNI 数据结构的一个元素是指向由 JVM 生成的一个指针的数组;该数组的每个元素都是指向一个 JNI 函数的指针。可以从本地化方法的内部对 JNI 函数的调用。 第二个参数会根据 Java 类中本地方法的定义不同而不同,如果是定义为 static 方法,类型会是 jclass ,表示对特定 Class 对象的引用,如果是非 static 方法,类型是 jobject ,表示当前对象的引用,相当于 ” this ” 。 可以说这两个变量是本地化方法返回 JAVA 的大门。
注意: 在本地化方法中生成的 Dll 不具备到处运行的特性,而具有 ” 牵一发而动全身 ” 的特点。只要包名一改变,那么你所有的工作就得重新做一遍。原因就是当用 javah 生成头文件时,函数名的生成规则为 Java[ _ 包名 ]_ 类名 _ 方法名 [ _ 函数签名 ] ;当你的包名改变时,生成的函数名也跟着改变了,那么你再次调用以前编写的 Dll 时,会抛出异常。
 
八、       参考文献
1.         《 Java 编程思想》 Bruce Eckel 机械工业出版社
2.         《 Java2 核心技术 卷 2 》(第 6 版) Cay S.Horstmann,Gary Cornell 机械工业出版社
3.         《高级 Java2 大学教程》 ( 英文版 ) Harvey M.Deitel ,Paul J.Deitel,Sean E.Santry 电子工业出版社
4.         《 windows 核心编程》 Jeffrey Richter 机械工业出版社
5.            http://www.jguru.com
6.                    sun 公司文档
 
如对本文有任何疑问和异议,欢迎与作者探讨: normalnotebook@126.com
 
注:本文最初发表在 2004 年《开发高手》第 12 期上。
 
JNI与Android NDK开发实践:深入“Hello-jni-ndk“ Java Native Interface(JNI)是Java编程语言的标准编程接口,它允许Java代码和其他语言编写的代码进行交互,尤其用于调用C、C++等语言编写的本地方法。JNI是连接Java应用和本地系统服务(如操作系统API)的桥梁,它提供了一套丰富的API,让Java程序能够扩展和提高性能。在Java类中,你可以通过使用native关键字来声明那些需要在本地代码中实现的方法。这些方法被称为“native方法”。 阅读详情

相关推荐

Android平台串口通信JNI开发实践教程

Java Native Interface (JNI) 是一种编程框架,允许Java代码和其他语言编写的代码进行交互,尤其是与C和C++代码的交互。JNI是Java平台的一部分,它为Java虚拟机(JVM)提供了接口,使Java程序能够调用本地应用程序接口(API)和库。它对于那些需要执行高性能计算、访问遗留代码库或利用特定硬件/操作系统特性的应用程序至关重要。

weixin_42181686的博客 1375

使用C/C++实现Java的Native方法接口(JNI)(6)多种JNI数据类型代码实例

在Java程序中使用native接口(JNI)调用外部的使用C/C++实现的函数

O_1CxH的博客 492

cmake跨平台sdk开发和Jni 接口设计及应用

1:cmake 跨平台sdk 开发快速入门,写一个CMakeLists.txt 可以生成windows 和 ubuntu16.04 和 安卓三个平台的sdk(动态库) 2:java的jni 技术,java 和c/c++ 的数据交互(包括基本类型和数组类型和类的相互传递)和相互调用 3:学会写java测试demo, 以及学会写安卓测试demo,测试自己所写的sdk 能否调用正常

JNI开发实践

1 创建一个java类,定义native方法 public static native boolean login(String name,String pw); 2 Build–&gt;make project编译java代码 编译后,会在module目录下的生成.class文件,目录如图一 注意: .class文件的存放地址根据自己build–intermediates的具体情况而定 3 ...

IT烟酒僧的博客 328

Java技术(JNI)实践

很小的一个JNI调用实例,并且介绍一些JNI调用的一些常见问题的解决办法。

JNI设计实践之路,pdf

JNI设计实践之路,pdfJNI设计实践之路,pdfJNI设计实践之路,pdf

JNI 从入门到实践,万字爆肝详解

到这里,JNI 的知识就讲完了,你可以按照学习路线图来看。下一篇,我们开始讲 Android NDK 开发。关注我,带你建立核心竞争力,我们下次见。参考资料《JNI 编程指南》JNI 提示—— Android 官方文档Java 原生接口规范 —— Java 官方文档深入理解 Android:卷 1(第 2 章 · 深入理解 JNI)—— 邓凡平 著深入理解 Android:Java 虚拟机 ART(第 11 章 · ART 中的 JNI)—— 邓凡平 著。

chuyouyinghe的专栏 3014

Python实战:用Py2neo轻松玩转Neo4j图数据库(附完整CRUD示例)

本文通过构建武侠知识图谱的实战案例,详细介绍了如何使用Python的Py2neo库对Neo4j图数据库进行CRUD操作。内容涵盖环境搭建、节点与关系创建、Cypher查询、数据更新删除及事务管理,帮助开发者直观高效地处理复杂关联数据,实现从入门到进阶的完整交互流程。

weixin_29034963的博客 185

JNI开发之JNI实践

在上一篇文章中介绍了JNI开发的环境搭建,这篇文章将用两个实例来介绍JNI开发。JNI开发大致可以分为两类:一类是Java调用本地代码方法;另外一类是本地方法访问Java成员。接下来将分别介绍这两种情况。 一、Java代码调用本地方法 在JNI原理那篇文章中,介绍了Java代码调用本地方法的一般步骤。接下来将以实际的例子来描述Java代码调用本地方法的过程。在这个例子中,在Java代码中调用本

chewbee的专栏 767

从CCD数据上报看wxHOOK的封号风险与规避

本文深入分析了微信CCD数据上报机制对wxHOOK的检测逻辑与封号风险,揭示了调试器路径暴露、非白名单DLL加载和内存校验失败等关键风险点。通过实战案例和规避方案,帮助开发者理解如何安全使用wxHOOK技术,避免封号风险,包括内存镜像欺骗、内核级隐蔽和硬件辅助方案等高级技巧。

weixin_27875131的博客 634

低代码Agent开发框架使用指南(二)—Coze平台核心功能概览

本篇分享系统介绍了字节跳动推出的低代码智能体开发平台Coze。Coze以免费易用、多模型支持和便捷的跨平台发布为核心优势,显著降低了智能体开发门槛。文章详细解析了平台的六大核心功能:扩展能力边界的插件系统、增强认知的知识库、结构化数据管理的数据库、可视化图像处理的图像流、实现复杂逻辑编排的工作流,以及处理复杂任务的多智能体协作机制,全面展现了Coze平台的强大能力。

weixin_42782643的博客 1358

从桥梁到生态:JNI在跨语言开发中的设计哲学与工程实践

本文深入探讨JNI(Java Native Interface)在跨语言开发中的设计哲学与工程实践,解析其如何平衡Java的平台无关性与C/C++的高性能优势。文章涵盖JNI架构设计、性能优化、异常处理及现代工具链集成,为开发者提供构建可持续跨语言系统的实用指南。

tequila的博客 986

S4 HANA 1809+折旧过账(AFAB/AFABN)凭证行数据存储逻辑变迁:从BSEG到ACDOCA的深度解析

本文深度解析了S4 HANA 1809+版本中折旧过账(AFAB/AFABN)凭证行数据存储逻辑的变迁,从传统的BSEG表迁移到ACDOCA表。文章详细探讨了这一变更的技术背景、官方设计意图及实际影响,包括数据存储量减小和系统性能提升等优势。同时提供了升级项目的应对策略和最佳实践建议,帮助用户顺利过渡到新的数据存储模型。

weixin_28731223的博客 294

转: JNI设计实践之路

JNI设计实践之路 作者:杨小华   一、      前言 本文为在 32 位 Windows 平台上实现 Java 本地方法提供了实用的示例、步骤和准则。本文中的示例使用 Sun公司的 Java Development Kit (JDK) 版本 1.4.2。 用 C ++语言编写的本地代码是用 Microsoft Visual C++  6.0编译器编译生成。规定在Java程序中funct

taoshengyang的专栏 1129

Cadence DSP 算子开发实战:从入门到精通

本文详细介绍了Cadence DSP算子开发的实战技巧,从入门基础到高级优化。内容涵盖DSP架构特性、内存管理、分块计算策略、SIMD指令优化等核心知识,并提供了性能调优和调试的实用技巧,帮助开发者快速掌握Cadence DSP开发精髓,提升图像处理等计算密集型任务的开发效率。

weixin_29278805的博客 437

STC8H开发(十六): GPIO模拟SPI驱动Ci24R1实现低成本无线遥控

本文详细介绍了如何使用STC8H单片机通过GPIO模拟SPI驱动Ci24R1无线模块,实现低成本无线遥控方案。内容涵盖硬件设计要点、软件驱动开发技巧及实战优化经验,特别适合消费电子产品开发者参考。Ci24R1凭借其高性价比和双模协议支持,成为替代传统nRF24L01+的理想选择。

weixin_30378623的博客 352

my-tv项目架构揭秘:从Kotlin到JNI的跨层设计实践

my-tv是一款电视直播应用,采用Kotlin作为主要开发语言,结合C++实现的JNI(Java Native Interface)模块,构建了一个跨层次的应用架构。该项目通过模块化设计,实现了从UI交互到网络请求、从数据处理到音视频播放的完整功能链。 应用的整体架构可分为四个主要层次:表现层(UI Layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data Ac...

gitblog_00524的博客 729

SerDes学习笔记:DFE均衡架构示例(USB3.1、PCIe3.0\4.0)

本文探讨了DFE均衡器架构在不同数据接口中的应用特性。研究表明,抽头数量随数据速率提升而增加,1-16抽头架构较为常见。通过PCIeGen3/4实例分析,对比了单抽头、双抽头和15抽头DFE的性能差异,证明多抽头结构能通过精细调节有效提升噪声容限和降低误码率。文章详细阐述了DFE工作原理,包括其通过反馈信号对输入信号的补偿机制,以及不同抽头对消除码间干扰的作用。实验采用8Gbps速率(125ps/UI)进行波形仿真,直观展示了均衡处理前后的信号变化,为理解信道均衡技术提供了重要参考。

一只豌豆象的博客 519

Android的JNI开发全面介绍与最佳实践

JNI全称是Java Native Interface,为Java本地接口,是连接Java层与Native层的桥梁。在Android进行JNI开发时,可能会遇到couldn't find "xxx.so"问题,或者内存泄漏问题,或者令人头疼的JNI底层崩溃问题。Java层如何调用Native方法?Java方法的参数如何传递给Native层?而Native层又如何反射调用Java方法?这些问题在本文将得到答案,带着问题去阅读会事半功倍,接下来我们开始全方位介绍与最佳代码实践。

徐福记456 3万+

FastAPI 高级特性:同异步路由函数、依赖注入与后台任务

在现代 Web 开发中,高效的性能和简洁的代码结构是至关重要的。FastAPI 作为一个现代、快速(高性能)的 Web 框架,凭借其对异步编程的原生支持和直观的依赖注入系统,迅速成为开发者的热门选择。在本文中,将深入探讨 `FastAPI` 中的几个核心概念:同异步函数的区别、`Depends` 依赖以及后台任务。

qq_43629857的博客 204

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

JNI详解---从不懂到理解

这里只是目录,请看链接:https://blog.youkuaiyun.com/hui12581/article/details/44832651 Chap1:JNI完全手册... 3 Chap2:JNI-百度百科... 11 Chap 3:javah命令帮助信息... 16 Chap 4:用javah产生一个.h文件... 17 Chap5:jni教程(very very good) 19 Ch...

hu626626的博客 564

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

chap 20:使用 Java Native Interface 的最佳实践

JNI 的发展 JNI 自从 JDK 1.1发行版以来一直是 Java平台的一部分,并且在 JDK 1.2发行版中得到了扩展。JDK 1.0发行版包含一个早期的本机方法接口,但是未明确分隔本机代码和 Java代码。在这个接口中,本机代码可以直接进入 JVM结构,因此无法跨 JVM实现、平台或者甚至各种 JDK版本进行移植。使用 JDK 1.0模型升级含有大量本机代码的应用程序,以及开

zvivi521的专栏 503

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

JNI与Android NDK详解:原理、调用过程与实践示例

本文系统介绍了JNI(Java Native Interface)和Android NDK的核心概念与应用。JNI作为Java与本地代码(C/C++)的桥梁,主要用于代码保护、性能优化、复用现有库和系统级访问。文章详细解析了JNI调用流程,包括环境配置、接口设计、本地方法实现和构建配置,并提供了图像处理项目的实践示例。同时总结了内存管理、性能优化和调试技巧等常见问题解决方案,强调应合理使用这些技术以平衡性能与维护成本。通过JNI和NDK,开发者可以安全高效地集成本地代码,但需注意仅在必要时采用并确保规范开发

xingfuyisheng的专栏 1714
上一篇: GCC内嵌汇编语言
下一篇: 对虚拟内存的经典解释
normalnotebook
博客等级 码龄24年 647粉丝 38原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值