作者:老九—技术大黍
社交:知乎
公众号:老九学堂
特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权
前言
在《The Java™ Native Interface Programmer’s Guide and Specification》一书中这样描述JNI是什么:
计科系出身的我来参考翻译一下,请大家不要当成标准,如果有不足之处,请大家指正和补充。
当Java平台被部署到主机环境的最顶端处时,它就可以让Java应用程序与使用其他编程语言(比如C/C++)实现的本地代码协同工作。程序员已经适应了使用传统的C/C++语言与Java平台一起构建应用,因为Java应用程序已经与C/C++代码协同开发很多年了,我们Java应用可以直接使用C/C++已经固有的代码来解决Java应用中的问题,从而不需要我们Java程序再使用Java代码来重写一次。
JNI是我们可以使用的Java平台提供的高级功能,因为这个功能可以让我们使用其它编程语言来书写功能性代码,并用被Java应用程序调用。JNI作为Java虚拟实现的一个组成部分,它提供一个双向接口(two-way interface)方式允许Java应用调用本地代码,同样本地代码与可以调用Java应用的代码。图1.1描述了JNI在虚拟机中的角色:
JNI就是Java平台设计来给我们第三方应用开发时,当我们需要让Java应用程序与其他编程语言协同混混编的接口,它是一个双向接口:
- Java应用通过JNI呼叫其它编程语言的代码(最常见的是C/C++,理论上可以是任何其他的编程语言)
- 其它编程语言也可以通过JNI呼叫Java应用程序的功能
解读
使用纯Java来开发对于程序员是一件非常美好的事情。但是,Java语法始终有一个缺点就是它的运行效率相比C/C++不够高,即使Java已经更新到Java 17版本了,它还是不可能和C/C++一样快,原因很简单:Java代码必须通过Java虚拟机进程才能执行!而C/C++代码是直接让本地操作系统运行的代码!
C/C++应用程序本身就是与Java虚拟机同等的进程存在。换名话说:除非Java没有虚拟机,直接被操作系统运行,它才能和C/C++应用一样快。当然,这个假设是不成立的。
为解决彻底解决运行效率,那么我们可让Java直接呼叫C的函数,以便解决效率,这种编程叫JNI编程。
相对于Java在沙箱(虚拟机)中运行,C/C++的代码直接OS中运行,所以叫本地码。
1996年Java第一版提供的纯Java写的字符串加密库运行效率不高,所以只有通过C的函数加密才能解决效率。
比如,我原来制作的《凤舞三国》手机游戏,我们使用C书写的哈夫曼加/解密算法,安装在Android机上就需要通过Java代码使用JNI来调用这个功能。原因两个:
- 安全--如果我们使用Java来书写,那么谁都可以轻松的反编译.class字节码文件
- 高效--Java的代码本身运行效率没有C代码高
另外,Java提供的网络阻塞式IO流,因为是用纯Java写的,所以在非常强调运行效率的环境下,也是达不到要求,因此,这部分功能也需要调用本地码来解决。当然JNI的缺点就是因为使用两种语言开发,那么意味着本地码必须支持不同的OS运行环境。比如,在windows运行,那么本地码必须支持win32环境;在Linux运行,那么本地码支持Linux环境;在Unix运行,那么本地码必须支持Unix环境。
因此,如果我们在win32环境开发,那么必须针对各平台进行重新调试,这是非常麻烦的事情。这也就是为什么使用Cocos2d-x的C++引擎开发跨平台(Android和iOS)手机游戏最麻烦的原因。
注意:JNI是Java Native Interface的缩写。
理论讲完了,我们下面来进行代码实操。
第一个JNI应用
一般我们JNI编码是呼叫C的函数,当然也可以呼叫C++的成员函数。下面我们演示Java类呼叫C函数。
Java代码
书写一个HelloNative.java源文件
/**
功能:书写一个Java类用来演示JNI编程-呼叫C函数
作者:技术大黍
*/
public class HelloNative{
public static native void greeting();
}
本地方法可以使用static来修饰,当然也可以不使用。使用static是因为不处理参数传递。
生存头文件
接着我们在命令中输入下面的两条命令
javac *.java
javah HelloNative
生成一个HelloNative.h头文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloNative */
#ifndef _Included_HelloNative
#define _Included_HelloNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloNative
* Method: greeting
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloNative_greeting
(JNIEnv *env, jclass clazz);
#ifdef __cplusplus
}
#endif
#endif
解释
C函数头文件实现
接着我们需要编写一个HelloNative.c源文件,定义一个C函数给Java虚拟机来呼叫。因为是写给虚拟机,所以要遵守以下规则:
- 必须使用Java全方法名,如果有包,那么还加上包名。比如com.yy.HelloNative.greeting
- 每个单词之间使用下划线分隔_,比如Java_HelloNative_greeting或者Java_com_yy_HelloNative_greeting_
- 如果类包含ASCII码字符’_’,’$’或者数字,或者大于’\u007F’的Unicode编码值,那么使用_0xxxx十六进制表示
注意:如果有重载的本地方法,那么必须用两个下划线来表示。比如重载的Java_HelloNative_greeting__
/**
功能:书写一个HelloNative.h的实现
*/
#include "HelloNative.h"
#include "stdio.h"
JNIEXPORT void JNICALL Java_HelloNative_greeting
(JNIEnv *env, jclass clazz){
printf("Hello Native World!\n");
}
C++头文件实现
如果是C++那么,书写一个HelloNative.cpp文件如下
/*
功能:书写一个C++的实现
*/
#include "HelloNative.h"
#include <iostream>
using namespace std;
extern "C"
JNIEXPORT void JNICALL Java_HelloNative_greeting
(JNIEnv *env, jclass clazz){
cout << "Hello Native World!" << endl;
}
编译动态链接库
编译有两种方式:
- 安装gcc/g++编程器(在 www.mingw-w64.org/doku.php 下载安装)
- 安装Visual Studio 2019
GCC编译
在控制台中输入下面的命令
gcc -Wl,--add-stdcall-alias -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o hello.dll HelloNative.c
如果一切正常,那么会生成一个HelloNative.dll文件
CL编程
先全文搜索到cl.exe编译器命令位置,然后把它添加到PATH中去。
在控制台中输入下面的命令
cl -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32" -LD HelloNative.c -FeHelloNative.dll
如果我们的操作系统是64位话,那么Java调用时链接不成功的,因为它是一个32位编译命令。会报下面的错误信息:
解决编译过程问题
要解决32位与64位不兼容的问题,我们有两种方式:
- 安装64位gcc编译器
- 调试VS2019环境 ,让它生成64位dll文件
书写一个HelloNativeTest.java源文件
/**
功能:书写一个程序入口用来测试JNI编译效果
作者:技术大黍
*/
public class HelloNativeTest{
public static void main(String[] args){
HelloNative.greeting();
}
//静态语句块保证在创建类对象之前加载dll文件
static{
System.loadLibrary("Hello");
}
}
如果一切正常,那么我们会看到下面的这个效果
掌握JNI编程
类型转换
当C和Java之间传递函数的参数和返回值时,我们Java实际上是没有原始数值类型的。因此,在进行参数转义说明。
在jni.h头文件定义了这些类型转换,并且该头文件定义JNI _FALSE=0和JNI_TRUE=1两个常量。
在Java中字符串是一个UTF-16编码序列,也就等于C中非空结束的字符序列,因此两种语言的字符完成不一样。
JNI提供两组函数转换字符串转换问题:
- 一个是UTF-8的字符串序列
- 一个是UTF-16值数组,也就是jchar数组
Java的String对应JNI的jstring类型。例如
如果JNI文件的C++代码,那么JNIEvn类有一个内联函数NewStringUTF可以直接呼叫:
该函数返回一个新的字符串jstring对象,它使用函数GetStringUTFChars来实现。 我们知道Java虚拟机要垃圾叫回收对象,因此,我们的字符串使用完成之后,我们必须呼叫函数ReleaseStringUTFChars来对字符对象进行垃圾回收。
访问对象属性
前面我们访问类的静态方法,以及参数和返回值。接下来我们讨论访问Java对象属性。
接着需要在cpp中这样实现
编码签名
C/C++访问java对象的属性和方法时,必须有识别码,也就是编码签名。比如上面“D”就是double的意思
- 如果是访问java的数组,比如字符串数组
- 如果访问float[][]二维数组
- 如果访问一个方法带两个int参数 ,并且返回一个int
- 如果访问一个方法带一个字符串参数,返回void
注意:表达式以分号结尾,参数之间没有间隔。
如果构造方法,比如Employee(java.lang.String,java.util.Date)
注意:D和Ljava/util/Date;之间没有间隔。在编码签名 ,包名的.都被/替换掉。
V的意思是返回类型为void,虽然Java和构造方法没有返回值,但是编码签名要求使用V给虚拟机一个数字签名来识别。
使用javap命令和参加-s来生成Java类的编码签名,javap –s –private Employee
如果一切正常,那么显示如下效果
访问静态属性
访问静态属性有点类似访问非静态属性,我们同样使用GetStaticFieldID等函数。
- 因为没有对象,所以使用FindClass来类的引用
- 使用类而不是使用对象实例来访问静态属性
比如,我们访问System.out静态属性
总结
JNI编程一般的教课书都不会讲,因为它是需要与其它编程语言一起混合编程,这对于初级的Java程序员来说是一个灾难:说好的我们Java程序是“software craftsman”(纯软件程序员)的嘛?怎么又要咱们学习C/C++啊!。这不是骗人吗?
但是,如果我们要成为高级Java程序员,那么肯定是要学会与其它语言一起混合编程的。因为一种编程语言不可能解决所有的应用问题,这个在实际开发中是非常见的事情。
我们希望通过这篇文章可以让大家快速理解Java平台的JNI功能,以及了解怎样进行JNI开发,以及时和快速解决工作中与C/C++程序员一起协同编程的问题。这样也不会被C/C++程序员瞧不起,找到我们Java程序员应有的尊严。
我们可大喊一声:为了德玛!
最后
记得给大黍❤️关注+点赞+收藏+评论+转发❤️
作者:老九学堂—技术大黍
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。