Jimmy Chen

A Programmer

(译文) JNI编程指南与规范 第二章 开始编程

第二章 开始

  本章将引导你了解如何使用Java本地接口。我们将编写一个Java应用程序调用一个C函数来答应“Hello World!”。

2.1 概述

  图2.1表明使用JDK或者Java SDK 2发行版编写一个调用C函数来打印“Hello World!”的Java应用程序的过程。这个过程有以下步骤组成:

《(译文) JNI编程指南与规范 第二章 开始编程》

  • 创建申明了本地方法的类(HelloWorld.java)
  • 使用javac去编译这个HelloWorld源文件,得到一个HelloWorld.class的class文件。javac编译工具是JDK或者Java 2 SDK发行版中提供的。
  • 使用javah -jni去生成包含本地方法函数原型的C头文件(HelloWorld.h)。javah工具也是在JDK或者Java 2 SDK发行版中带有的。
  • 编写本地方法的C实现(HelloWorld.c)
  • 将C实现编译成一个本地库helloWorld.dll或者libHelloWorld.so,使用主机环境中可用的C编译器和连接器
  • 使用Java运行时解析器运行hello world程序。类文件(HelloWorld.class)和本机库(HelloWorld.dll或者HelloWorld.so)都在运行时加载。

本章的剩余部分将详细讲解这些步骤。

2.2 定义本地方法

  首先用Java编程语言编写以下程序。这个程序定义了一个包含本地方法print,类名为HelloWorld的类

class HelloWorld {
    private native void print();
    
    public static void main(String[] args) {
        new HelloWorld().print();
    }
    
    static {
        System.loadLibrary("HelloWorld");
    }
}

  HelloWorld类定义从打印本地方法的声明开始。之后是实例化HelloWorld类并调用此示例的print本地方法。类定义最后一部分是一个静态初始化器,它加载包含本地print方法的本地库。

  定义一个本地方法例如print和使用Java编程语言定义一个常规的方法存在两个不同的地方。一个本地方法声明必须包含native修饰符。native修饰符表明该方法由其他编程语言实现。此外本地方法声明以分号终结(语句终结符),因为在这个类中没有实现这个本地方法。我们会在独立的c文件中完成print方法的编写。

  在本地方法print能够被调用前,实现了print方法的本地库必须被加载。在这个例子中,我们在HelloWorld类的静态初始化块中将本地库加载进来。Java虚拟机在调用HelloWorld类的任何方法前先运行静态初始化块的代码,因此可以肯定在本地方法print被调用前,本地库就已经被加载了。

  我们定义了一个可以运行HelloWorld类的main方法。HelloWorld.main方法以与常规方法相同的方式调用print本地方法。

  System.loadLibrary使用库名字,找到与库名字相关的本地库,并将本地库加载到应用程序中。我们会在本书的后面部分讨论准确的加载过程。现在只需要记住,为了使System.loadLibrary(“HelloWorld”)能够成功,我们需要在win32系统上创建一个HelloWorld.dll文件,在Solaris系统中创建一个libHelloWorld.so文件。

2.3 编译HelloWorld类

  在你完成HelloWorld类的编写,将源码保存到一个名为HelloWorld.java的文件中。使用JDK或者Java SDK 2中带有的javac工具进行编译:

javac HelloWorld.java

  这条指令会在当前目录中产生一个HelloWorld.class文件。

2.4 创建本地方法的头文件

  接下来我们会使用javah工具来生成一个JNI类型的头文件,这个文件在后面使用C语言完成本地方法时是非常有用的。执行javah的指令如下:

javah -jni HelloWorld

  头文件的名字是类名并在其后面加上”.h”结尾。上面的指令会生成一个名字为HelloWorld.h的文件。在这里我们不会列出这个头文件的内容。这个文件的最重要的部分是Java_HelloWorld_print函数原型,它是实现了HelloWorld.print方法的C函数:

JNIEXPORT void JNICALL Java_HelloWorld_print
  (JNIEnv *, jobject);

  现在先忽略JNIEXPORT和JNICALL宏。你可能有注意到本地方法的C实现接受两个参数,尽管相应本地方法的定义(指HelloWorld.java中的定义)却没有接受任何参数。每一个本地方法实现的第一个参数是一个JNIEnv接口指针。第二个参数是引用HelloWorld对象本身,类似于C++的this指针。在本书的后面我们会讨论如何使用JNIEnv接口指针和jobject参数,但是在这个例子将忽略这两个参数。

2.5 编写本地方法实现

  使用javah来生成的JNI类型头文件能够帮助你使用C/C++来完成本地方法的实现。你编写的函数必须遵循生成的头文件中的函数原型。在C文件HelloWorld.c中,你可以按照下面的代码来实现HelloWorld.print方法。

#include <jni.h>
#include <stdio.h>
#include <HelloWorld.h>

JNIEXPORT void JNICALL Java_HelloWorld_print
  (JNIEnv *env, jobject obj)
{
    printf("Hello World!\n");
    return ;
}

  这个本地方法的实现是非常简单的。它使用printf函数来显示字符串“Hello World!”,然后就返回。就像前面提到的,JNIEnv指针和obj对象引用都忽略了。

  这个C程序包含三个头文件:

  • jni.h:此头文件提供本地代码调用JNI函数所需的信息。 编写本机方法时,必须始终将此文件包含在C或C源文件中。
  • stdio.h:上面的代码片段还包括了stdio.h因为它使用printf函数
  • HelloWorld.h:通过javah工具生成的头文件。它包含Java_HelloWorld_print的函数原型。

2.6 编译C源码并生成一个本地库

  请记住,你在文件HelloWorld.java文件中创建一个HelloWorld类时,包含一条将本地库加载到程序中的代码:

System.loadLibrary("HelloWorld");

  现在所有需要的C源码都已经编写完成了,现在你需要编译HelloWorld.c文件并创建一个本地库。

  不同的操作系统提供不同的方式去创建本地库。在Solaris上,下述命令能够创建一个名为libHelloWorld.so的动态库。

cc -G -I/java/include -I/java/include/solaris HelloWorld.c -o libHelloWorld.so

-G编译选项表明让C编译器生成一个动态库而不是常规的Solaris可执行文件。在win32系统中,下面的指令使用Microsoft Visual C++编译器创建的动态链接库(DLL)HelloWold.dll

cl -Ic:\java\include -Ic:\java\include\win32 -MD -LD HelloWorld.c -FeHelloWorld.dll

-MD编译选项表明HelloWorld.dll和win32多线程C库链接。-LD编译选项表明C编译器产生一个DLL文件而不是常规的Win32可执行文件。当然不管在Win32还是Solaris系统,你都需要在你的电脑上设置好头文件的包含路径。

博主注:我使用的编译环境是Ubuntu 16.04版本,JDK版本是openjdk 1.8,使用上面的指令是不行的,下面是我使用的编译指令

cc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -I. -fPIC -shared HelloWorld.c -o libHelloWorld.so

当然你也可以使用gcc,其中,JAVA_HOME是我配置到.bashrc中的路径:

export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

大家可以按照各自的JAVA_HOME进行配置,这样就能够编译成功了。

2.7 运行程序

  到这里,运行该程序的两个主件都已经准备好了。类文件(HelloWor.class)调用一个本地方法,本地库(HelloWorld.dll)实现这个本地方法。因为HelloWorld类包含它自己的main方法,在Solaris和Win32上,可以通过如下方法执行这个程序:

java HelloWorld

你能够看到程序输出如下信息:

Hello World!

  为了让你的程序能够正确的运行,正确设置好本地库的路径是非常重要的。本地库路径是一系列文件目录,当Java虚拟机加载本地库时会搜索这些路径。如果你没有正确设置本地库路径,你会看到如下类似的错误log:

java.lang.UnsatisfiedLinkError: no HelloWorld in library path 
    at java.lang.Runtime.loadLibrary(Runtime.java)
    at java.lang.System.loadLibrary(System.java) 
    at HelloWorld.main(HelloWorld.java)

  需要确保本地库在设置的本地库路径的其中一个目录中。如果你在Solaris系统上运行,LD_LIBRARY_PATH环境变量是用来设置本地库路径的。确保该环境变量的路径包含动态库libHelloWorld.so文件所在的目录。如果libHelloWorld.so文件在当前目录,在标准shell或者KornShell中,你可以通过如下两条指令来设置LD_LIBRARY_PATH环境变量

LD_LIBRARY_PATH=. 
export LD_LIBRARY_PATH

在C Shell中的等价指令如下:

setenv LD_LIBRARY_PATH .

  如果你是在Windows 95或者Windows NT上运行,确保HelloWorld.dll在当前目录,或者其所在的目录已经列在PATH环境变量中。

  在Java 2 SDK 1.2发行版中,你可以像下面的指令一样,通过java命令行来指定本地库的的路径:

java -Djava.library.path=. HelloWorld

-D命令行选项设置了一个Java平台属性。将java.library.path设置为“.”,“.”表明Java虚拟机会在当前路径中搜索本地库。

博主注:博主运行HelloWorld使用的指令是:

java -Djava.library.path=. HelloWorld

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: