漫谈 JNI

引子

我们首先来看一枚业界新闻:

北京时间 4 月 6 日,据国外媒体报道,TIOBE 网站发布了最新的《四月份编程语言排名》,下面让我们一睹为快:

时隔 4 年之后,C 语言荣登本月榜单中位居榜首,近十年来 C 语言的使用率一直稳定在 15%-20%之间。这一次由于长期霸占第一宝座的 Java 略显颓势,从而给了 C 语言超过的机会。

随着 JVM(Java 虚拟机)可以支持越来越多的语言,Java 的强大势力正在逐渐被瓜分。以 JavaFX 脚本为例,该语言的排名已经迅速蹿升至 22 位。

此外,Objective-C 和谷歌 Go 语言依然是上升势头最明显的编程语言,显示出新语言的旺盛生命力。

TIOBE 开发语言排行榜每月更新一次,依据的指数是基于世界范围内的资深软件工程师和第三方供应商,包括谷歌、微软等巨头公司均参与统计,其结果作为当前业内程序开发语言的流行使用程度的有效指标。

我们看到了 Java 语言生命力的旺盛,特别是在 Java
2 之后的版本,也就是 JDK5(Tiger) 发布之后,Java 语言的应用更是爆炸式的增长,时值互联网飞速成长迅猛发展之际,Java 语言与生俱来的跨平台特性以及良好且可控的安全性成为了互联网技术企业首选的开发语言。借着 Sun 公司当年在服务器市场上强劲的表现,Java 语言顿时成为 TIOBE 排行榜上的新贵。

Java 应用如此成熟与广泛,在 Google.com 上搜索 Java 得到的结果条数是“Results 1 – 10 of about 196,000,000 for  Java”,搜索 C++得到的结果条数是“ Results 1 – 10 of about 53,300,000 for C++
”,那么我们再来看看关于 JNI 的搜索结果“ Results 1 – 10 of about 1,980,000 for Java Native Interface
”和 Java 2D 做一个对比“ Results 1 – 10 of about 2,710,000 for Java 2D”。从这些结果中我们能看出一些东西,Java
Native Interface(JNI) 相较于 Java 中并不常用的 Java 2D API 的关注程度还低,也就是说 JNI 其实跟 Java 主体上的流行半毛钱关系都没有,那么 JNI 为什么要存在呢,JNI 又能给我们带来什么呢?

JNI 带来了什么

Java 语言的成功很大程度上得于 API 的丰富和设计的良好,Java API 为广大开发人员节省了大量的时间提高了开发效率,使得项目的交付更为敏捷。但是 API 注定存在其局限性,那么我们在什么时候需要使用到 JNI 呢?

1.         所开发的应用程序要使用到与平台相关的属性,而 Java 标准类库不支持对这些属性的处理;

2.         已经拥有了用其他编程语言实现的应用程序或库,希望用 Java 直接调用这些实现;

3.         程序的某个模块对运行的时间效率要求很高,从而希望用较低级的语言 (如汇编) 来实现,同时希望在 Java 应用程序中使用这个模块。

从目前市场上对 JNI 应用的案例来看,最为主要的应用场景一般是源于上面的第二点原因,在已有类库的基础上进行开发平台的迁移。近来比较热门的 Android 移动终端 OS 平台就是基于 JNI 技术来实现的,本质上 Android 的核是 Linux 内核的子集,基于该内核使用 Java 平台进行了良好的封装,提供给开发人员一个高可用性的 SDK
Platform。开发人员只需了解 Java 平台的特性,根据 API
Documentation 就完全可以开始编码了,并不需要知道其内部的实现机制,着实是为开发人员谋福利啊,当然此举更是从根本上劫持了诸多原本不属于该开发阵营中的开发人员,任何熟悉 Java 平台开发的开发人员顿时全成为了 Android 平台开发的潜在用户。技术也是可以带来市场的。

这就是 JNI 给我们带来的,借助 Java 语言的跨平台特性,以及开发阵营的强大,各大企业和机构基于已有的类库和程序进行二次封装,不仅能延续产品的持续发展,还能扩大二次开发人员的阵营,而不落于 IT 技术发展的潮流。说了这么多废话,那么如何使用 JNI 技术来进行二次封装呢?

Hello JNI World

1.         编写并编译 Java 代码

使用称手的工具 (IDE) 编写 Java 代码,例如保存在” E:\JavaWorkspace\Laputa\com\laputa\jni\test\HelloJNIWorld.java”

package com.laputa.jni.test

public class HelloJNIWorld{

public native void sayHello();

public static void main(String[] args){

HelloJNIWorld hello = new HelloJNIWorld();

hello.sayHello();

}

}

进入” E:\JavaWorkspace\Laputa\”, 使用命令” javac com\laputa\jni\test\HelloJNIWorld.java”编译该文件,由于未指定编译结果路径,生成的 class 文件将保存在源文件同一目录下,得到”
e:\JavaWorkspace\Laputa\com\laputa\jni\test\HelloJNIWorld.class”。

2.         生成 JNI 头文件

使用 javah 命令生成头文件,”javah –jni com.laputa.jni.test.HelloWorld”,将会生成一个”
e:\JavaWorkspace\Laputa\com_laputa_jni_test_HelloJNIWorld.h”。在生成头文件的时候有两点需要注意,一是”javah –jni”
后面需要写类的全限定名 (自行查阅资料了解全限定名相关知识),并且执行该命令的当前目录下要能找到全限定名中的路径,也就是说执行命令的路径应该在包路径的上一层目录,这里我们将 com.laputa.jni.test 包保存在”E:\JavaWorkspace\Laputa”下,那么我们执行 javah 命令的路径也应该是”
E:\JavaWorkspace\Laputa”; 二是未指定生成头文件的目标目录时,默认将头文件生成在当前目录下。头文件的样式如下:

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

#include <jni.h>

/* Header for class com_laputa_jni_test_HelloJNIWorld */

#ifndef _Included_com_laputa_jni_test_HelloJNIWorld

#define _Included_com_laputa_jni_test_HelloJNIWorld

#ifdef __cplusplus

extern “C” {

#endif

/*

* Class:     com_laputa_jni_test_HelloJNIWorld

* Method:    sayHello

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_com_laputa_jni_test_HelloJNIWorld_sayHello

(JNIEnv *, jobject);

#ifdef __cplusplus

}

#endif

#endif

3.         实现头文件对应的方法

关于 JNI 头文件中的的类型以及相关的内容可以参考 Java Tutorial 中 Java Native
Interface 一章。现在我们就来实现刚才生成的头文件中的 sayHello 方法,至于方法名为何这么长,主要是因为 JNI 就是通过这样的机制来识别方法签名的,开发过程中需要注意的是,对于 Java 文件中的 native 方法最好不要使用重载,因为重载后的 native 方法生成的头文件中的方法将会非常之长,重载的方法名中将会包含其参数信息,例如在 HelloJNIWorld.java 中添加一个 public
native void sayHello(String str); 方法,其生成的头文件中对应方法将会是 (下面标红的文字):

/*

* Class:     com_laputa_jni_test_HelloJNIWorld

* Method:    sayHello

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

*/

JNIEXPORT void JNICALL Java_com_laputa_jni_test_HelloJNIWorld_sayHello__Ljava_lang_String_2

(JNIEnv *, jobject, jstring);

这会让我们在查看代码的时候迷惑,所以建议在需要重载的时候直接使用其他的名称,放弃重载为后期维护提供可读性更高的代码。

接下来就可以开始实现我们需要的方法了,例如我的 Java 代码中需要调用 sayHello 这个本地方法,那么我们新建一个对应的 cpp 文件,将方法签名拷贝至 cpp 文件中,实现该方法。

#include “com_laputa_jni_test_HelloJNIWorld.h”

#include <iostream>

using namespace
std;

/*

* Class:     com_laputa_jni_test_HelloJNIWorld

* Method:    sayHello

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_com_laputa_jni_test_HelloJNIWorld_sayHello

(
JNIEnv *, jobject)

{


std::cout << “Hello JNI World” << std::endl;

}

4.         编译链接库并添加至系统 Path 变量中

使用合适的编译器 (VC/GCC) 进行编译和链接,生成合适的链接库 (dll 或者 so 文件)。在编译过程中可能出现头文件包含出现错误的问题,主要是因为在生成的 JNI 头文件中有”#include
<jni.h>”语句,编译器会到系统路径下去查找外部头文件,需要我们在编译的 IDE 或者脚本中指定该文件所在的目录为编译时 Include 路径,另外”jni.h”文件中可能需要使用到与平台相关的一些文件,例如在 Windows 平台下需要使用到”jni_md.h”文件,该文件处于 JDK 安装目录的 include 目录下的 win32 路径下 (其他平台也处于 include 的相应目录下),需要将这个文件所在目录也添加到编译时 Include 路径中,保证编译通过。将该文件加入系统 Path 目录下 (不同的平台使用其特定方式进行添加,在非 Windows 平台下使用 export 命令设置名为 LD_LIBRARY_PATH 的路径参数值为库文件所在目录),以便 Java 程序能加载该库文件 (Java 加载库文件时,需要库文件在系统的 Path 下)。

5.         在 Java 程序中加载库文件,执行代码

在 Java 程序中使用 System.loadLibrary(String libName) 加载系统路径下的库文件 (忽略后缀名)。加载库文件的代码可以使用静态域进行加载,使得类在加载之初就加载库文件,并且之后在当前 JVM 实例中共享使用,不需重复加载库文件。在代码中添加一段代码:

static{

System.loadLibrary(“HelloJNIWorld”);

}

重新编译,执行代码,程序将在控制台输出“Hello JNI World”

以上就是 HelloWorld 的整个编码过程了,流程如下:

  1. 编写 Java 代码,声明本地方法|
  2. 生成 JNI 头文件|
  3. 根据头文件编写 JNI 实现代码
  4. 编译工程,生成链接库
  5. 执行程序,加载链接库,查看结果

这就是 JNI 编码和调用的整个流程和步骤了,以上是一个非常简单的例程,实际的产品设计中会有更多的一些技巧和设计原则在里面。