JNI(Java Native Interface, java本地接口),是一种编程框架,用于java虚拟机中的java程序与本地应用或者库相互调用,本地应用一般指的是C,C++或者汇编等语言编写的,并且被编译为本机硬件和操作系统的程序。

设计的目的

有些事情Java无法处理时,JNI允许程序员用其他编程语言来解决,例如,Java标准库不支持的平台相关功能或者程序库。也用于改造已存在的用其它语言写的程序,供Java程序调用。许多基于JNI的标准库提供了很多功能给程序员使用,例如文件I/O、音频相关的功能。当然,也有各种高性能的程序,以及平台相关的API实现,允许所有Java应用程序安全并且平台独立地使用这些功能。

JNI框架允许Native方法调用Java对象,就像Java程序访问Native对象一样方便。Native方法可以创建Java对象,读取这些对象,并调用Java对象执行某些方法。当然Native方法也可以读取由Java程序自身创建的对象,并调用这些对象的方法。

注意事项

1、在使用JNI的过程中,可能因为某些微小的BUG,对整个JVM造成很难重现和调试的错误。

2、依赖于JNI的应用失去了Java的平台移植性(一种解决办法是为每个平台编写专门的JNI代码,然后在Java代码中,根据操作系统加载正确的JNI代码)。

3、JNI框架并没有对 non-JVM 内存提供自动垃圾回收机制,Native代码(如汇编语言)分配的内存和资源,需要其自身负责进行显式的释放。

4、LinuxSolaris平台,如果Native代码将自身注册为信号处理器(signal handler),就会拦截发给JVM的信号。可以使用 责任链模式 让 Native代码更好地与JVM进行交互。

5、Windows平台上,在SEH try/catch块中可以将结构化异常处理(SEH)用来包装Native代码,以捕获机器(CPU/FPU)生成的软中断(例如:空指针异常、被除数为0等),将这些中断在传播到JVM(中的Java代码)之前进行处理,以免造成未捕获的异常。

6、NewStringUTF、GetStringUTFLength、GetStringUTFChars、ReleaseStringUTFChars与 GetStringUTFRegion等编码函数处理的是一种修改的UTF-8,[3],实际上是一种不同的编码,某些字符并不是标准的UTF-8。 null字符(U+0000)以及不在Unicode字符平面映射中的字符(codepoints 大于等于 U+10000 的字符,例如UTF-16中的代理对 surrogate pairs),在修改的UTF-8中的编码都有所不同。 许多程序错误地使用了这些函数,将标准UTF-8字符串传入或传出这些函数,实际上应该使用修改后的编码。程序应当先使用NewString、GetStringLength、GetStringChars、ReleaseStringChars、GetStringRegion、GetStringCritical与ReleaseStringCritical等函数,这些函数在小尾序机器上使用UTF-16LE编码,在大尾序机器上使用UTF-16BE编码,然后再通过程序将 UTF-16转换为 UTF-8。

7、JNI在某些情况下可能带来很大的开销和性能损失:

  • 调用 JNI 方法是很笨重的操作,特别是在多次重复调用的情况下。
  • Native 方法不会被 JVM 内联,也不会被 即时编译 优化 ,因为方法已经被编译过了。
  • Java 数组可能会被拷贝一份,以传递给 native 方法,执行完之后再拷贝回去. 其开销与数组的长度是线性相关的。
  • 如果传递一个对象给方法,或者需要一个回调,那么 Native 方法可能会自己调用JVM。 访问Java对象的属性、方法和类型时,Native代码需要类似反射的东西。签名由字符串指定,通常从JVM中查询。这非常缓慢并且容易出错。
  • Java 中的字符串(String) 也是对象,有 length 属性,并且是编码过的. 读取或者创建字符串都需要一次时间复杂度为 O(n) 的复制操作.

JNIEnv*

JNI环境指针(JNIEnv*)作为每个映射为Java方法的本地幔数的第一个参数,使得本地幔数可以与JNI环境交互。这个JNI界面指针可以存储,但仅在当前线程中有效。其它线程必须首先调用AttachCurrentThread()把自身附加到虚拟机以获得JNI界面指针。一旦附加,本地线程运行就类似执行本地幔数的正常Java线程。本地线程直到执行DetachCurrentThread()把自身脱离虚拟机。

把当前线程附加到虚拟机并获取JNI界面指针:

JNIEnv *env;
(*g_vm)->AttachCurrentThread (g_vm, (void **) &env, NULL);

当前线程脱离虚拟机:

(*g_vm)->DetachCurrentThread (g_vm);

JNI如何工作

JNI框架中,native方法一般在独立的.c或者.cpp文件中实现,当JVM调用这些函数,就传递一个JNIEnv指针,一个jobject的指针,任何在Java方法中声明的Java参数。一个JNI函数看起来类似这样:

// .h头文件中的函数声明
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello
  (JNIEnv *, jclass, jstring);
// 对应的java文件声明
package com.study.jnilearn;

public class HelloWorld {
    public static native String sayHello(String name);}

JNIEnv指针的作用,包含了与JVM交互的函数和访问java对象的函数,例如本地数组转换成java数组的JNI函数,本地字符串转换成java字符串的JNI函数,实例化对象,抛出异常等,基本上java可以做到的事情,JNIEnv也可以。

本地数据类型与Java数据类型可以互相映射。对于复合数据类型,如对象,数组,字符串,就必须用JNIEnv中的方法来显示地转换。

第二个参数jclass引用了一个java对象(包含了native方法)。

类型映射

下表是java(JNI)与本地代码之间的数据类型映射:

本地类型java语言的类型描述类型签名(signature)
unsigned charjbooleanunsigned 8 bitsZ
signed charjbytesigned 8 bitsB
unsigned shortjcharunsigned 16 bitsC
shortjshortsigned 16 bitsS
longjintsigned 32 bitsI
long long__int64jlongsigned 64 bitsJ
floatjfloat32 bitsF
doublejdouble64 bitsD
voidV

 方法签名模板:"L fully-qualified-class"  ,由改名字指明的类。

例如,签名"Ljava/lang/String;"是类java.lang.String。带前缀[的签名表示该类型的数组,如[I表示整型数组。void签名使用V代码。

这些类型是可以互换的,如jint也可使用 int,不需任何类型转换。

但是,Java字符串、数组与本地字符串、数组是不同的。如果在使用char *代替了jstring,程序可能会导致JVM崩溃。

JNI开发流程

1、编写声明了native方法的java类,并编译成class文件。

package jni.study;

import java.util.List;

public class HelloWorld {
    native void func1();
    native String func2();
    native void func3(String str);
    native String func4(String str);
    native void func5(List list);
    native List func6();
    native static String test(String str);

    public static void main(String[] args) {
        test("Hello World !");
    }
    static {
        System.loadLibrary("libuntitled5");
    }
}
D:\workspace\jni_study>javac src/main/java/jni/study/HelloWorld.java

 

 2、通过class文件生成.h头文件

D:\workspace\jni_study>javah -jni -classpath src/main/java -d src/main/java/lib jni.study.HelloWorld.class
// -jni 参数表示将class中native方法生成jni规则的函数
// -classpath 参数表示class文件所在目录
// -d 参数表示生成的.h文件的位置

这里需要注意  ,java9开始,javah命令就不存在了。 报:bash: javah: command not found。javahYou use the javah tool to generate C header and source files from a Java class.https://docs.oracle.com/javase/9/tools/javah.htm#JSWOR687

 java16为例,只需要执行命令:

D:\workspace\jni_study>javac -h ./src/main/java/c src/main/java/jni/study/HelloWorld.java

 

 3、通过头文件中声明的方法,编写c或者cpp文件实现

打开clion,创建c库项目,库类型选择动态库shared

 删除生成的c和h文件,将java生成的头文件复制过来,并创建同名源文件

 引入头文件后,因为clion环境中没有

#include <jni.h>

 需要引入jdk中提供的头文件依赖。

clion依赖的c编译器(C:\MinGW\mingw64\x86_64-w64-mingw32\include)中,添加两个文件:C:\Java\jdk-16\include\jni.h 和 C:\Java\jdk-16\include\win32\jni_md.h

编写CMakeLists.txt文件,并重新加载

add_library(untitled5 SHARED jni_study_HelloWorld.h jni_study_HelloWorld.c)
//加载过程
"D:\app\Intellij\CLion 2020.3.3\bin\cmake\win\bin\cmake.exe" -DCMAKE_BUILD_TYPE=Debug -G "CodeBlocks - MinGW Makefiles" D:\workspace\untitled5
-- Configuring done
-- Generating done
-- Build files have been written to: D:/workspace/untitled5/cmake-build-debug

[Finished]

头文件内容:

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

#ifndef _Included_jni_study_HelloWorld
#define _Included_jni_study_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     jni_study_HelloWorld
 * Method:    func1
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_jni_study_HelloWorld_func1
  (JNIEnv *, jobject);

/*
 * Class:     jni_study_HelloWorld
 * Method:    func2
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_study_HelloWorld_func2
  (JNIEnv *, jobject);

/*
 * Class:     jni_study_HelloWorld
 * Method:    func3
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_jni_study_HelloWorld_func3
  (JNIEnv *, jobject, jstring);

/*
 * Class:     jni_study_HelloWorld
 * Method:    func4
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_study_HelloWorld_func4
  (JNIEnv *, jobject, jstring);

/*
 * Class:     jni_study_HelloWorld
 * Method:    func5
 * Signature: (Ljava/util/List;)V
 */
JNIEXPORT void JNICALL Java_jni_study_HelloWorld_func5
  (JNIEnv *, jobject, jobject);

/*
 * Class:     jni_study_HelloWorld
 * Method:    func6
 * Signature: ()Ljava/util/List;
 */
JNIEXPORT jobject JNICALL Java_jni_study_HelloWorld_func6
  (JNIEnv *, jobject);

/*
 * Class:     jni_study_HelloWorld
 * Method:    test
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_study_HelloWorld_test
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

 源文件内容:

//
// Created by Administrator on 2021/12/4.
//

#include "jni_study_HelloWorld.h"

#ifdef __cplusplus
extern "C"
{
#endif

/*
 * Class:     jni_study_HelloWorld
 * Method:    func1
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_jni_study_HelloWorld_func1
        (JNIEnv *jniEnv, jobject jobject1){}

/*
 * Class:     jni_study_HelloWorld
 * Method:    func2
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_study_HelloWorld_func2
        (JNIEnv *jniEnv, jobject jobject1){
    return NULL;
}

/*
 * Class:     jni_study_HelloWorld
 * Method:    func3
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_jni_study_HelloWorld_func3
        (JNIEnv *jniEnv, jobject jobject1, jstring jstring1){}

/*
 * Class:     jni_study_HelloWorld
 * Method:    func4
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_study_HelloWorld_func4
        (JNIEnv *jniEnv, jobject jobject1, jstring jstring1){
    return NULL;
}

/*
 * Class:     jni_study_HelloWorld
 * Method:    func5
 * Signature: (Ljava/util/List;)V
 */
JNIEXPORT void JNICALL Java_jni_study_HelloWorld_func5
        (JNIEnv *jniEnv, jobject jobject1, jobject jobject2){}

/*
 * Class:     jni_study_HelloWorld
 * Method:    func6
 * Signature: ()Ljava/util/List;
 */
JNIEXPORT jobject JNICALL Java_jni_study_HelloWorld_func6
        (JNIEnv *jniEnv, jobject jobject1) {
    return NULL;
}

/*
 * Class:     jni_study_HelloWorld
 * Method:    test
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_study_HelloWorld_test
        (JNIEnv *jniEnv, jclass jclass1, jstring jstring1) {
    const char *c_str = (*jniEnv)->GetStringUTFChars(jniEnv, jstring1, 0);
    if (c_str == NULL) {
        return NULL;
    }
    printf("c output : %s\n", c_str);
    (*jniEnv)->ReleaseStringUTFChars(jniEnv, jstring1, c_str);
    return NULL;
}


#ifdef __cplusplus
}
#endif

4、编译c文件,生成dll文件

5、java中引用dll文件,并执行

将dll文件复制到java项目中,并在启动配置中加入参数:

-Djava.library.path=./src/main/java/lib

 执行后:

c output : Hello World !

参考文献:

https://zh.wikipedia.org/wiki/JNIhttps://zh.wikipedia.org/wiki/JNI

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐