资讯专栏INFORMATION COLUMN

Java 外部函数接口:JNI, JNA, JNR

pubdreamcc / 1422人阅读

摘要:我们知道,发起函数调用,需要构造一个栈帧。构造栈帧的具体实现细节的选择,被称为调用惯例。要想完成这个函数调用逻辑,就要运行时构造栈帧,生成参数压栈和清理堆栈的工作。目前,几乎支持全部常见的架构。

原文:http://nullwy.me/2018/01/java...
如果觉得我的文章对你有用,请随意赞赏
遇到的问题

前段时间开发的时候,遇到一个问题,就是如何用 Java 实现 chdir?网上搜索一番,发现了 JNR-POSIX 项目 [stackoverflow ]。俗话说,好记性不如烂笔头。现在将涉及到的相关知识点总结成笔记。

其实针对 Java 实现 chdir 问题,官方 20 多年前就存在对应的 bug,即 JDK-4045688 "Add chdir or equivalent notion of changing working directory"。这个 bug 在 1997.04 创建,目前的状态是 Won"t Fix(不予解决),理由大致是,若实现与操作系统一样的进程级别的 chdir,将影响 JVM 上的全部线程,这样引入了可变(mutable)的全局状态,这与 Java 的安全性优先原则冲突,现在添加全局可变的进程状态,已经太迟了,对不变性(immutability)的支持才是 Java 要实现的特性。

chdir 是平台相关的操作系统接口,POSIX 下对应的 API 为 int chdir(const char *path);,而 Windows 下对应的 API 为 BOOL WINAPI SetCurrentDirectory(_In_ LPCTSTR lpPathName);,另外 Windows 下也可以使用 MSVCRT 中 API 的 int _chdir(const char *dirname);(MSVCRT 下内部实现其实就是调用 SetCurrentDirectory [reactos ] )。

Java 设计理念是跨平台,"write once, run anywhere"。很平台相关的 API,虽然各个平台都有自己的类似的实现,但存在会差异。除了多数常见功能,Java 并没有对全部操作系统接口提供完整支持,比如很多 POSIX API。除了 chdir,另外一个典型的例子是,在 Java 9 以前 JDK 获取进程 id 一直没有简洁的方法 [stackoverflow ],最新发布的 Java 9 中的 JEP 102(Process API Updates)才增强了进程 API。获取进程 id 可以使用以下方式 [javadoc ]:

long pid = ProcessHandle.current().pid();

相比其他语言,Pyhon 和 Ruby,对操作系统相关的接口都有更多的原生支持。Pyhon 和 Ruby 实现的相关 API 基本上都带有 POSIX 风格。比如上文提到,chdirgetpid,在 Pyhon 和 Ruby 下对应的 API 为:Pyhon 的 os 模块 os.chdir(path) 和 os.getpid();Ruby 的 Dir 类的 [Dir.chdir( [ string] )](https://ruby-doc.org/core-2.2... 类方法和 Process 类的 Process.pid 类属性。Python 解释器的 chdir 对应源码为 posixmodule.c#L2611,Ruby 解释器的 chdir 对应源码为 dir.c#L848 和 win32.c#L6741。

JNI 实现 getpid

Java 下要想实现本地方法调用,需要通过 JNI。关于 JNI 的介绍,可以参阅“Java核心技术,卷II:高级特性,第9版2013”的“第12章 本地方法”,或者读当年 Sun 公司 JNI 设计者 Sheng Liang(梁胜)写的“Java Native Interface: Programmer"s Guide and Specification”。本文只给出实现 getpid 的一个简单示例。

首先使用 Maven 创建一个简单的脚手架:

mvn archetype:generate     
  -DgroupId=com.test       
  -DartifactId=jni-jnr     
  -DpackageName=com.test   
  -DinteractiveMode=false  

com.test 包下添加 GetPidJni 类:

package com.test;

public class GetPidJni {
    public static native long getpid();

    static {
        System.loadLibrary("getpidjni");
    }

    public static void main(String[] args) {
        System.out.println(getpid());
    }
}

javac 编译代码 GetPidJNI.java,然后用 javah 生成 JNI 头文件:

$ mkdir -p target/classes
$ javac src/main/java/com/test/GetPidJni.java -d "target/classes"
$ javah -cp "target/classes" com.test.GetPidJni

生成的 JNI 头文件 com_test_GetPidJni.h,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_test_GetPidJni */

#ifndef _Included_com_test_GetPidJni
#define _Included_com_test_GetPidJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_test_GetPidJni
 * Method:    getpid
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_com_test_GetPidJni_getpid
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

现在有了头文件声明,但还没有实现,手动敲入 com_test_GetPidJni.c

#include "com_test_GetPidJni.h"

JNIEXPORT jlong JNICALL
Java_com_test_GetPidJni_getpid (JNIEnv * env, jclass c) {
    return getpid();
}

编译 com_test_GetPidJni.c,生成 libgetpidjni.dylib

$ gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o libgetpidjni.dylib com_test_GetPidJni.c

生成的 libgetpidjni.dylib,就是 GetPidJni.java 代码中的 System.loadLibrary("getpidjni");,需要加载的 lib。

现在运行 GetPidJni 类,就能正确获取 pid:

$ java -Djava.library.path=`pwd` -cp "target/classes" com.test.GetPidJni

JNI 的问题是,胶水代码(黏合 Java 和 C 库的代码)需要程序员手动书写,对不熟悉 C/C++ 的同学是很大的挑战。

JNA 实现 getpid

JNA(Java Native Access, wiki, github, javadoc, mvn),提供了相对 JNI 更加简洁的调用本地方法的方式。除了 Java 代码外,不再需要额外的胶水代码。这个项目最早可以追溯到 Sun 公司 JNI 设计者 Sheng Liang 在 1999 年 JavaOne 上的分享。2006 年 11月,Todd Fast (也来自 Sun 公司) 首次将 JNA 发布到 dev.java.net 上。Todd Fast 在发布时提到,自己在这个项目上已经断断续续开发并完善了 6-7 年时间,项目刚刚在 JDK 5 上重构和重设计过,还可能有很多缺陷或缺点,希望其他人能浏览代码并参与进来。Timothy Wall 在 2007 年 2 月重启了这项目,引入了很多重要功能,添加了 Linux 和 OSX 支持(原本只在 Win32 上测试过),加强了 lib 的可用性(而非仅仅基本功能可用) [ref ]。

看下示例代码:

import com.sun.jna.Library;
import com.sun.jna.Native;

public class GetPidJNA {

    public interface LibC extends Library {
        long getpid();
    }

    public static void main(String[] args) {
        LibC libc = Native.loadLibrary("c", LibC.class);
        System.out.println(libc.getpid());
    }
}
JNR 实现 getpid

最初,JRuby 的核心开发者 Charles Nutter 在实现 Ruby 的 POSIX 集成时就使用了 JNA [ref ]。但过了一段时候后,开始开发 JNR(Java Native Runtime, github, mvn) 替代 JNA。Charles Nutter 在介绍 JNR 的 slides 中阐述了原因:

Why Not JNA?
- Preprocessor constants?
- Standard API sets out of the box
- C callbacks?
- Performance?!?

即,(1) 预处理器的常量支持(通过 jnr-constants 解决);(2) 开箱即用的标准 API(作者实现了 jnr-posix, jnr-x86asm, jnr-enxio, jnr-unixsocket);(3) C 回调 callback 支持;(4) 性能(提升 8-10 倍)。

使用 JNR-FFI(github, mvn)实现 getpid,示例代码:

import jnr.ffi.LibraryLoader;

public class GetPidJnr {

    public interface LibC {
        long getpid();
    }

    public static void main(String[] args) {
        LibC libc = LibraryLoader.create(LibC.class).load("c");
        System.out.println(libc.getpid());
    }
}

使用 JNR-POSIX(github, mvn)实现 chdirgetpid,示例代码:

import jnr.posix.POSIX;
import jnr.posix.POSIXFactory;

public class GetPidJnrPosix {

    private static POSIX posix = POSIXFactory.getPOSIX();

    public static void main(String[] args) {
        System.out.println(posix.getcwd());
        posix.chdir("..");
        System.out.println(posix.getcwd());
        System.out.println(posix.getpid());
    }
}
JMH 性能比较

性能测试代码为 BenchmarkFFI.java(github),测试结果如下:

# JMH version: 1.19
# VM version: JDK 1.8.0_144, VM 25.144-b01

Benchmark                          Mode  Cnt      Score      Error   Units
BenchmarkFFI.testGetPidJna        thrpt   10   8225.209 ±  206.829  ops/ms
BenchmarkFFI.testGetPidJnaDirect  thrpt   10  10257.505 ±  736.135  ops/ms
BenchmarkFFI.testGetPidJni        thrpt   10  77852.899 ± 3167.101  ops/ms
BenchmarkFFI.testGetPidJnr        thrpt   10  58261.657 ± 5187.550  ops/ms

即:JNI > JNR > JNA (Direct Mapping) > JNA (Interface Mapping)。相对 JNI 的实现性能,其他三种方式,从大到小的性能百分比依次为:74.8% (JNR), 13.2% (JnaDirect), 10.6% (JNA)。在博主电脑上测试,JNR 相比 JNA 将近快了 6-7 倍(JNR 作者 Charles Nutter 针对 getpid 的测试结果是 JNR 比 JNA 快 8-10 倍 [twitter slides ])。

实现原理 JNA 源码简析

先来看下 JNA,JNA 官方文档 FunctionalDescription.md,对其实现原理有很好的阐述。这里将从源码角度分析实现的核心逻辑。

回顾下代码,我们现实定义了接口 LibC,然后通过 Native.loadLibrary("c", LibC.class) 获取了接口实现。这一步是怎么做到的呢?翻下源码 Native.java#L547 就知道,其实是通过动态代理(dynamic proxy)实现的。使用动态代理需要实现 InvocationHandler 接口,这个接口的实现在 JNA 源码中是类 com.sun.jna.Library.Handler。示例中的 LibC 接口定义的全部方法,将全部分派到 Handler 的 invoke 方法下。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

然后根据返回参数的不同,分派到 Native 类的,invokeXxx 本地方法:

/**
 * Call the native function.
 *
 * @param function  Present to prevent the GC to collect the Function object
 *                  prematurely
 * @param fp        function pointer
 * @param callFlags calling convention to be used
 * @param args      Arguments to pass to the native function
 *
 * @return The value returned by the target native function
 */
static native int invokeInt(Function function, long fp, int callFlags, Object[] args);

static native long invokeLong(Function function, long fp, int callFlags, Object[] args);

static native Object invokeObject(Function function, long fp, int callFlags, Object[] args);
...

比如,long getpid() 会被分派到 invokeLong,而 int chmod(String filename, int mode) 会被分派到 invokeInt。invokeXxx 本地方法参数:

参数 Function function,记录了 lib 信息、函数名称、函数指针地址、调用惯例等元信息;

参数 long fp,即函数指针地址,函数指针地址通过 Native#findSymbol()获得(底层是 Linux API dlsym 或 Windows API GetProcAddress )。

参数 int callFlags,即调用约定,对应 cdecl 或 stdcall。

参数 int callFlags,即函数入参,若无参数,args 大小为 0,若有多个参数,原本的入参被从左到右依次保存到 args 数组中。

再来看下 invokeXxx 本地方法的实现 dispatch.c#L2122(invokeIntinvokeLong 实现源码类似):

/*
 * Class:     com_sun_jna_Native
 * Method:    invokeInt
 * Signature: (Lcom/sun/jna/Function;JI[Ljava/lang/Object;)I
 */
JNIEXPORT jint JNICALL
Java_com_sun_jna_Native_invokeInt(JNIEnv *env, jclass UNUSED(cls), 
                                  jobject UNUSED(function), jlong fp, jint callconv,
                                  jobjectArray arr)
{
    ffi_arg result;
    dispatch(env, L2A(fp), callconv, arr, &ffi_type_sint32, &result);
    return (jint)result;
}

即,全部 invokeXxx 本地方法统一被分派到 dispatch 函数 dispatch.c#L439:

static void
dispatch(JNIEnv *env, void* func, jint flags, jobjectArray args,
ffi_type *return_type, void *presult)

这个 dispatch 函数是全部逻辑的核心,实现最终的本地函数调用。

我们知道,发起函数调用,需要构造一个栈帧(stack frame)。构造栈帧,涉及到参数压栈次序(参数从左到右压入还是从右到左压入)和清理栈帧(调用者清理还是被调用者清理)等实现细节问题。不同的编译器在不同的 CPU 架构下有不同的选择。构造栈帧的具体实现细节的选择,被称为调用惯例(calling convention)。按照调用惯例构造整个栈帧,这个过程由编译器在编译阶段完成的。比如要想发起 sum(2, 3) 这个函数调用,编译器可能会生成如下等价汇编代码:

; 调用者清理堆栈(caller clean-up),参数从右到左压入栈
push 3
push 2
call _sum      ; 将返回地址压入栈, 同时 sum 的地址装入 eip
add  esp, 8    ; 清理堆栈, 两个参数占用 8 字节

dispatch 函数是,需要调用的函数指针地址、输入参数和返回参数,全部是运行时确定。要想完成这个函数调用逻辑,就要运行时构造栈帧,生成参数压栈和清理堆栈的工作。JNA 3.0 之前,实现运行时构造栈帧的逻辑的对应代码 dispatch_i386.c、dispatch_ppc.c 和 dispatch_sparc.s,分别实现 Intel x86、PowerPC 和 Sparc 三种 CPU 架构。

运行时函数调用,这个问题其实是一个一般性的通用问题。早在 1996 年 10 月,Cygnus Solutions 的工程师 Anthony Green 等人就开发了 libffi(home, wiki, github, doc),解决的正是这个问题。目前,libffi 几乎支持全部常见的 CPU 架构。于是,从 JNA 3.0 开始,摒弃了原先手动构造栈帧的做法,把 libffi 集成进了 JNA。

直接映射(Direct Mapping)
https://docs.oracle.com/javas...
http://www.chiark.greenend.or...

JNR 源码简析

JNR 底层同样也是依赖 libffi,参见 jffi。但 JNR 相比 JNA 性能更好,做了很有优化。比较重要的点是,JNA 使用动态代理生成实现类,而 JNR 使用 ASM 字节码操作库生成直接实现类,去除了每次调用本地方法时额外的动态代理的逻辑。使用 ASM 生成实现类,对应的代码为 AsmLibraryLoader.java。其他细节,限于文档不全,本人精力有限,不再展开。

Java 9 的 getpid 实现

Java 9 以前 JDK 获取进程 id 没有简洁的方法,最新发布的 Java 9 中的 JEP 102(Process API Updates)增强了进程 API。进程 id 可以使用以下方式 [javadoc ]

long pid = ProcessHandle.current().pid();

翻阅实现源码,可以看到对应的实现就是 JNI 调用:

jdk/src/java.base/share/classes/java/lang/ProcessHandleImpl [src ]

/**
* Return the pid of the current process.
*
* @return the pid of the  current process
*/
private static native long getCurrentPid0();

*nix 平台下实现为:

jdk/src/java.base/unix/native/libjava/ProcessHandleImpl_unix.c [src ]

/*
 * Class:     java_lang_ProcessHandleImpl
 * Method:    getCurrentPid0
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_java_lang_ProcessHandleImpl_getCurrentPid0(JNIEnv *env, jclass clazz) {
    pid_t pid = getpid();
    return (jlong) pid;
}

Windows 平台下实现为:

jdk/src/java.base/windows/native/libjava/ProcessHandleImpl_win.c [src ]

/*
 * Returns the pid of the caller.
 *
 * Class:     java_lang_ProcessHandleImpl
 * Method:    getCurrentPid0
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_java_lang_ProcessHandleImpl_getCurrentPid0(JNIEnv *env, jclass clazz) {
    DWORD  pid = GetCurrentProcessId();
    return (jlong)pid;
}
参考资料

Changing the current working directory in Java? https://stackoverflow.com/q/8...

How can a Java program get its own process ID? http://stackoverflow.com/q/35842

Java核心技术,卷II:高级特性,第9版2013:第12章 本地方法,豆瓣

Java Native Interface: Programmer"s Guide and Specification, Sheng Liang (wiki,linkedin,msa), 1999,豆瓣:作者梁胜,中国科技大学少年班83级,并拥有耶鲁大学计算机博士学位(1990-1996),目前 Rancher Labs 创始人兼 CEO [ref ]

2013-07 Charles Nutter: Java Native Runtime http://www.oracle.com/technet...

JEP 191: Foreign Function Interface http://openjdk.java.net/jeps/191 作者是Charles Nutter

2014-03 Java 外部函数接口 http://www.infoq.com/cn/news/...

2005-08 Brian Goetz:用动态代理进行修饰 https://www.ibm.com/developer...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71846.html

相关文章

  • java如何获取hdd序列号

    摘要:序在里头如何获取硬盘的序列号呢,这里涉及了跨平台的问题,不同的操作系统的查看命令不一样,那么里头如何去适配呢。这里使用了这个项目来获取。使用的是的方式而不是的形式来进行本地调用的。获取方法,,,和之间的区别是什么,它们的调用效率怎么排名 序 在java里头如何获取硬盘的序列号呢,这里涉及了跨平台的问题,不同的操作系统的查看命令不一样,那么java里头如何去适配呢。这里使用了oshi这个...

    jzman 评论0 收藏0
  • 一个简单的JNA使用例子

    摘要:提供了这个技术来实现调用和程序,但实现起来比较麻烦,所以后来公司在的基础上实现了一个框架使用这个框架可以减轻程序员的负担,使得调用和容易很多。 使用JAVA语言开发程序比较高效,但有时对于一些性能要求高的系统,核心功能可能是用C或者C++语言编写的,这时需要用到JAVA的跨语言调用功能。JAVA提供了JNI这个技术来实现调用C和C++程序,但JNI实现起来比较麻烦,所以后来SUN公司在...

    winterdawn 评论0 收藏0
  • 在运行期通过反射了解JVM内部机制

    摘要:我们找到了许多有趣的工具和组件用来检测状态的各个方面,其中一个就是在运行期通过反射了解内部机制。由于包含多种的实现,就是供具体实现比如必须继承的抽象类。调试器框架是可扩展的,这意味着可以通过继承这个抽象类来使用另一个调试器。 在日常工作中,我们都习惯直接使用或者通过框架使用反射。在没有反射相关硬编码知识的情况下,这是Java和Scala编程中使用的类库与我们的代码之间进行交互的一种主要...

    crossea 评论0 收藏0
  • Java调用dll文件

    摘要:目录创建创建项目与工具项目与工具步骤与代码步骤与代码使用调用使用调用项目与工具项目与工具步骤与代码步骤与代码实际效果实际效果参考链接参考链接创建项目与工具步骤与代码使用创建动态链接库项目设置项目名与项目 目录 1 C++创建dll 1.1 项目与工具 1.2 步骤与代码 2 Java使用JN...

    Jeff 评论0 收藏0
  • Head First JNA

    摘要:与动态链接库配套的,会有相应的头文件,来声明动态链接库中对外暴露的方法。结构体映射结构体映射类编写类,继承,表示这个一个结构体。声明字段与,并且设置访问属性为。计算机状态结构体结构体指针结构体具体的值至此,功能完成。 问题描述 虚拟化项目,需要用到Java调用原生代码的技术,我们使用的是开源库JNA(Java Native Access)。 Native(C/C++)代码,编译生成动态...

    YPHP 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<