首页 > Linux操作系统 > Linux操作系统 > 跨语言编程的探索 | 龙蜥技术
跨语言编程是现代程序语言中非常重要的一个方向,也被广泛应用于复杂系统的设计与实现中。本文是 GIAC 2021(全球互联网架构大会) 中关于 Alibaba FFI — “跨语言编程的探索”主题分享的内容整理。两位分享人董登辉和顾天晓分别是龙蜥社区 Java SIG(Reliability,availability and serviceability)负责人和核心人员。
背景
前言
无疑,Java 是目前工业界最流行的应用编程语言之一。除了主流实现上(OpenJDK Hotspot)不俗的性能表现和成熟的研发生态(Spring 等)外,其成功的背后离不开语言本身较低(相比于 C/C++)的学习门槛。一个初学者可以利用现有的项目脚手架快速地搭建出一个初具规模的应用程序,但也因此许多 Java 程序员对程序底层的执行原理并不熟悉。本文将探讨一个在大部分 Java 相关的研发工作中不太涉及到的技术 — 跨语言编程。
回想起多年前第一次用 Java 在控制台上打印出 “Hello World” 时,出于好奇便翻阅 JDK 源码想一探实现的究竟 (在 C 语言中我们可以使用 printf 函数,而 printf 在具体实现上又依赖操作系统的接口),再一次次跳转后最终停留在了一个“看不到实现”的 native 方法上,额,然后就没有然后了。
我想有不少 Java 初级程序员对 native 方法的调用机制仍一知半解,毕竟在大部分研发工作中,我们直接实现一个自定义 native 方法的需求并不多,简单来说 native 方法是 Java 进行跨语言调用的接口,这也是 Java Native Interface 规范的一部分。

Java 跨语言编程技术的应用场景
常见场景
在介绍 Java 跨语言编程技术之前,首先简单地分析下实际编程过程中需要使用跨语言编程技术的场景,在这里我罗列了以下四个场景:
1、依赖字节码不支持的能力
换个角度看,目前标准的字节码提供了哪些能力呢?根据 Spec 规范,已有的字节码可以实现创建 Java 对象、访问对象字段和方法、常规计算(加减乘除与或非等)、比较、跳转以及异常、锁操作等等,但是像前言中提到的打印字符串到控制台这样的高阶功能,字节码并不直接支持,此外,像获取当前时间,分配堆外内存以及图形渲染等等字节码也都不支持,我们很难写出一个纯粹的 Java 方法(组合这些字节码)来实现这类能力,因为这些功能往往需要和系统资源产生交互。在这些场景下,我们就需要使用跨语言编程技术通过其他语言的实现来集成这些功能。
2、使用系统级语言(C、C++、Assembly)实现系统的关键路径
不需要显示释放对象是 Java 语言学习门槛低的原因之一,但因此也引入了 GC 的机制来清理程序中不再需要的对象。在主流 JVM 实现中,GC 会使得应用整体暂停,影响系统整体性能,包括响应与吞吐。
所以相对于 C/C++ ,Java 虽然减轻了程序员的研发负担,提高了产品研发效率,但也引入了运行时的开销。(Software engineering is largely the art of balancing competing trade-offs. )
当系统关键路径上的核心部分(比如一些复杂算法)使用 Java 实现会出现性能不稳定的情况下,可以尝试使用相对底层的编程语言来实现这部分逻辑,以达到性能稳定以及较低的资源消耗目的。
3、其他语言实现调用 Java
这个场景可能给大部分 Java 程序员的第一感觉是几乎没有遇到过,但事实上我们几乎每天都在经历这样的场景。
举个例子:通过 java <Main-Class> 跑一个 Java 程序就会经过 C 语言调用 Java 语言的过程,后文还会对此做提及。
4、历史遗留库
公司内部或者开源实现中存在一些 C/C++ 写的高性能库,用 Java 重写以及后期维护的成本非常大。当 Java 应用需要使用这些库提供的能力时,我们需要借助跨语言编程技术来复用。
Alibaba Grape
再简单谈谈阿里内部的一个场景:Alibaba Grape 项目,也是在跨语言编程技术方向上与我们团队合作的第一个业务方。
Grape 本身是一个并行图计算框架的开源项目(相关论文获得了 ACM SIGMOD Best Paper Award),主要使用 C++ 编写,工程实现上应用了大量的模板特性。对 Grape 项目感兴趣的同学可以参考 Github 上的相关文档,这里不再详细介绍。
该项目在内部应用中,存在很多使用 Java 作为主要编程语言的业务方,因此需要开发人员把 Grape 库封装成 Java SDK 供上层的 Java 应用调用。在实践过程中,遇到的两个显著问题:
为了解决这些问题,两个团队展开了合作,Alibaba FFI 项目正式开始演进,该项目的实现目前也主要针对 Java 调用 C++ 场景。

System.out.println("hello ffi");
通过 System.out 我们可以快速地实现控制台输出功能,我相信会有不少好奇的同学会关心这个调用到底是如何实现输出功能的,翻阅源码后,我们最终会看见这样一个 native 方法:private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;
static native void myHelloFFI();b. 通过 javah 或者 javac -h (JDK 10)命令生成后续步骤依赖的头文件(该头文件可以被 C 或者 C++ 程序使用)
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloFFI */
#ifndef _Included_HelloFFI
#define _Included_HelloFFI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloFFI
* Method: myHelloFFI
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloFFI_myHelloFFI
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
c. 实现头文件中的函数,在这里我们直接使用 printf 函数在控制台输出 “hello ffi”JNIEXPORT void JNICALL Java_HelloFFI_myHelloFFI
(JNIEnv * env, jclass c) {
printf("hello ffi");
}
// Init jvm ...// Get method idjmethodID mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");/* Invoke */(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

下面是 JNR 官方给出的示例。首先创建 LibC 接口封装目标 C 函数,然后调用 LibraryLoader 的 API 创建 LibC 的具体实例,最后通过接口完成调用:public class HelloWorld {
public interface LibC { // A representation of libC in Java
int puts(String s); // mapping of the puts function, in C `int puts(const char *s);`
}
public static void main(String[] args) {
LibC libc = LibraryLoader.create(LibC.class).load("c"); // load the "c" library into the libc variable
libc.puts("Hello World!"); // prints "Hello World!" to console
}
}
遗憾的是,JNA 和 JNR 对 C++ 的支持并不友好,因此在调用 C++ 库的场景中使用受限。@Platform(include="<vector>")
public class VectorTest {
@Name("std::vector<std::vector<void*> >")
public static class PointerVectorVector extends Pointer {
static { Loader.load(); }
public PointerVectorVector() { allocate(); }
public PointerVectorVector(long n) { allocate(n); }
public PointerVectorVector(Pointer p) { super(p); } // this = (vector<vector<void*> >*)p
/**
other methods ....
*/
public native @Index void resize(long i, long n); // (*this)[i].resize(n)
public native @Index Pointer get(long i, long j); // return (*this)[i][j]
public native void put(long i, long j, Pointer p); // (*this)[i][j] = p
}
public static void main(String[] args) {
PointerVectorVector v = new PointerVectorVector(13);
v.resize(0, 42); // v[0].resize(42)
Pointer p = new Pointer() { { address = 0xDEADBEEFL; } };
v.put(0, 0, p); // v[0][0] = p
PointerVectorVector v2 = new PointerVectorVector().put(v);
Pointer p2 = v2.get(0).get(); // p2 = *(&v[0][0])
System.out.println(v2.size() + " " + v2.size(0) + " " + p2);
v2.at(42);
}
}
JNI 的开销
内联
JVM 高性能的最核心原因是内置了强大的及时编译器(Just in time,简称 JIT)。JIT 会将运行过程中的热点方法编译成可执行代码,使得这些方法可以直接运行(避免了解释字节码执行)。在编译过程中应用了许多优化技术,内联便是其中最重要的优化之一。简单来说,内联是把被调用方法的执行逻辑嵌入到调用者的逻辑中,这样不仅可以消除方法调用带来的开销,同时能够进行更多的程序优化。

根据 JNI 规范,JVM 首先需要把 JNIEnv* 放入第一个参数寄存器(rdi)中,然后把剩下的几个参数包括 this(receiver)分别放入相应的寄存器中。为了让这一过程尽可能地快, hotspot 内部会根据方法签名动态生成转换过程的高效 stub。

概览
Alibaba FFI 项目致力于解决 Java 跨语言编程中遇到的问题,从整体上看项目分为以下两个模块:
FFI
类层次图(不完整)本质上 FFI 模块是通过注解处理器生成跨语言调用中需要的相关代码,用户仅需要依赖 FFI 的相关库(插件),并用 FFI 提供的 api 封装需要调用的目标函数即可。
示例
@FFIGen(library = "stdcxx-demo")
@CXXHead(system = {"vector", "string"})
@FFITypeAlias("std::vector")
@CXXTemplate(cxx="jint", java="Integer")
@CXXTemplate(cxx="jbyte", java="Byte")
public interface StdVector<E> extends CXXPointer {
@FFIFactory
interface Factory<E> {
StdVector<E> create();
}
long size();
@CXXOperator("[]") @CXXReference E get(long index);
@CXXOperator("[]") void set(long index, @CXXReference E value);
void push_back(@CXXValue E e);
long capacity();
void reserve(long size);
void resize(long size);
}
public class StdVector_cxx_0x6b0caae2 extends FFIPointerImpl implements StdVector<Byte> {
static {
FFITypeFactory.loadNativeLibrary(StdVector_cxx_0x6b0caae2.class, "stdcxx-demo");
}
public StdVector_cxx_0x6b0caae2(final long address) {
super(address);
}
public long capacity() {
return nativeCapacity(address);
}
public static native long nativeCapacity(long ptr);
...
public long size() {
return nativeSize(address);
}
public static native long nativeSize(long ptr);
}
JNI 的胶水代码:
#include <jni.h>
#include <new>
#include <vector>
#include <string>
#include "stdcxx_demo.h"
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT
jbyte JNICALL Java_com_alibaba_ffi_samples_StdVector_1cxx_10x6b0caae2_nativeGet(JNIEnv* env, jclass cls, jlong ptr, jlong arg0 /* index0 */) {
return (jbyte)((*reinterpret_cast<std::vector<jbyte>*>(ptr))[arg0]);
}
JNIEXPORT
jlong JNICALL Java_com_alibaba_ffi_samples_StdVector_1cxx_10x6b0caae2_nativeSize(JNIEnv* env, jclass cls, jlong ptr) {
return (jlong)(reinterpret_cast<std::vector<jbyte>*>(ptr)->size());
}
......
#ifdef __cplusplus
}
#endif
JNIEXPORT void JNICALL Java_Demo_crash(JNIEnv* env, jclass) {
void* addr = 0;
*(int*)addr = 0; // (Crash)
}
在第 3 行会出现内存访问越界的问题,如果不做特殊处理应用会 Crash。为了”隔离“这个问题,我们引入在保护机制,以下是 Linux 上的实现:PROTECTION_START // 生成胶水代码中插入宏
void* addr = 0;
*(int*)addr = 0;
PROTECTION_END // 宏
// 宏展开后的实现如下
// Pseudo code
// register signal handlers
signal(sig, signal_handler);
int sigsetjmp_rv;
sigsetjmp_rv = sigsetjmp(acquire_sigjmp_buf(), 1);
if (!sigsetjmp_rv) {
void* addr = 0;
*(int*)addr = 0;
}
release_sigjmp_buf();
// restore handler ...
if (sigsetjmp_rv) {
handle_crash(env, sigsetjmp_rv);
}
相关项目的对比
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LLVM4JNI

source
int v1 = i + j; int v2 = i - j; int v3 = i * j; int v4 = i / j; return v1 + v2 + v3 + v4;
bitcode
%5 = sdiv i32 %2, %3 %6 = add i32 %3, 2 %7 = mul i32 %6, %2 %8 = add nsw i32 %5, %7 ret i32 %8
bytecode
Code: stack=2, locals=6, args_size=3 0: iload_1 1: iload_2 2: idiv 3: istore_3 4: iload_2 5: ldc #193 // int 2 7: iadd 8: iload_1 9: imul 10: istore 5 12: iload_3 13: iload 5 15: iadd 16: ireturn
2、JNI Functions 的转换,目前已经支持 90+ 个。未来该功能会和fbjni等类似框架集成,打破Java和Native的代码边界,消除方法调用的额外开销。
source
jclass objectClass = env->FindClass(“java/util/List"); return env->IsInstanceOf(arg, objectClass);
bytecode
Code: stack=1, locals=3, args_size=2 0: ldc #205 // class java/util/List 2: astore_2 3: aload_1 4: instanceof #205 // class java/util/List 7: i2b 8: ireturn
3、C++ 对象访问。Alibaba FFI的另外一个好处是可以以面向对象的方式(C++是面向对象语言)来开发 Java off-heap 应用。当前基于Java的大数据平台大多需要支持 off-heap 的数据模块来减轻垃圾回收的压力,然而人工开发的 off-heap 模块需要小心仔细处理不同平台和架构的底层偏移和对齐,容易出错且耗时。通过 Aliabba FFI,我们可以采用 C++开发对象模型,再通过 Alibaba FFI 暴露给 Java 用户使用。
source
class Pointer {
public:
int _x;
int _y;
Pointer(): _x(0), _y(0) {}
const int x() { return _x; }
const int y() { return _y; }
};
JNIEXPORT
jint JNICALL Java_Pointer_1cxx_10x4b57d61d_nativeX(JNIEnv*, jclass, jlong ptr) {
return (jint)(reinterpret_cast<Pointer*>(ptr)->x());
}
JNIEXPORT
jint JNICALL Java_Pointer_1cxx_10x4b57d61d_nativeY(JNIEnv*, jclass, jlong ptr) {
return (jint)(reinterpret_cast<Pointer*>(ptr)->y());
}
bitcode
define i32 @Java_Pointer_1cxx_10x4b57d61d_nativeX %4 = inttoptr i64 %2 to %class.Pointer* %5 = getelementptr inbounds %class.Pointer, %class.Pointer* %4, i64 0, i32 0 %6 = load i32, i32* %5, align 4, !tbaa !3 ret i32 %6 define i32 @Java_Pointer_1cxx_10x4b57d61d_nativeY %4 = inttoptr i64 %2 to %class.Pointer* %5 = getelementptr inbounds %class.Pointer, %class.Pointer* %4, i64 0, i32 1 %6 = load i32, i32* %5, align 4, !tbaa !8 ret i32 %6
bytecode
public int y(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #36 // Field address:J 4: invokestatic #84 // Method nativeY:(J)I 7: ireturn LineNumberTable: line 70: 0 public static int nativeY(long); descriptor: (J)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=2, args_size=1 0: lload_0 1: ldc2_w #85 // long 4l 4: ladd 5: invokestatic #80 // Method com/alibaba/llvm4jni/runtime/JavaRuntime.getInt:(J)I 8: ireturn
JavaRuntime
public class JavaRuntime {
public static final Unsafe UNSAFE;
...
public static int getInt(long address) {
return UNSAFE.getInt(address);
}
...
}
在访问 C++ 对象的字段实现中,我们使用 Unsafe API 完成堆外内存的直接访问,从而避免了 Native 方法的调用。
|
|
|
|
|
|
|
|
|
|
|
|
这里我们以算法完成的时间(Job Time)为指标,将最终结果以 C++ 的计算时间为单位一做归一化处理。
结语
欢迎更多开发者加入Java语言与虚拟机SIG:
网址:
邮件列表: java-sig@lists.openanolis.cn
——完——
加入龙蜥社群
加入微信群:添加社区助理-龙蜥社区小龙(微信:openanolis_assis),备注【龙蜥】拉你入群;加入钉钉群:扫描下方钉钉群二维码。欢迎开发者/用户加入龙蜥社区(OpenAnolis)交流,共同推进龙蜥社区的发展,一起打造一个活跃的、健康的开源操作系统生态!
关于龙蜥社区
龙蜥社区 (OpenAnolis)是由 企事业单位、高等院校、科研单位、非营利性组织、个人等按照自愿、平等、开源、协作的基础上组成的非盈利性开源社区。龙蜥社区成立于2020年9月,旨在构建一个开源、中立、开放的Linux上游发行版社区及创新平台。
短期目标是开发龙蜥操作系统(Anolis OS)作为CentOS替代版,重新构建一个兼容国际Linux主流厂商发行版。中长期目标是探索打造一个面向未来的操作系统,建立统一的开源操作系统生态,孵化创新开源项目,繁荣开源生态。
龙蜥OS 8.4 已发布,支持x86_64和ARM64架构,完善适配Intel、飞腾、海光、兆芯、鲲鹏芯片。
欢迎下载:
https://openanolis.cn/download
加入我们,一起打造面向未来的开源操作系统!
https://openanolis.cn
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70004278/viewspace-2795511/ ,如需转载,请注明出处,否则将追究法律责任。