跨语言编程的探索 | 龙蜥技术
跨语言编程是现代程序语言中非常重要的一个方向,也被广泛应用于复杂系统的设计与实现中。本文是 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 应用调用。在实践过程中,遇到的两个显著问题:
封装 SDK 的工作非常繁琐,尤其对于像 Grape 这样依赖模板的库,在初期基于手动的封装操作经常出错 运行时性能远低于 C++ 应用
System.out.println("hello ffi");
private native void writeBytes(byte b[], int off, int len, boolean append) throws
IOException;
static native void myHelloFFI();
/* 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
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);
那么 JNI 到底是什么呢?以下是我的理解。
首先,JNI 是 Java 跨语言访问的接口规范,主要面向 C、C++、Assembly(为什么没有其他语言?我个人认为是由于这几种语言在当时规范设计之初足以覆盖绝大部分场景) 规范本身考虑了主流虚拟机的实现(hotspot),但本身不和任何具体的实现绑定,换句话说,Java 程序中跨语言编程的部分理论上可以跑在任何实现这个规范的虚拟机上 规范定义了其他语言如何访问 Java 对象、类、方法、异常,如何启动虚拟机,也定义了Java 程序如何调用其他语言(C、C++、Assembly) 在具体使用和实际运行效果的表现用一句话总结:Powerful, but slow, hard to use, and error-prone
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
}
}
@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);
}
}
一套 Java 注解和类型 包含一个注解处理器,用于生成胶水代码 运行时支持
实现 bitcode 到 bytecode 的翻译,打破 Java 方法与 Native 函数的边界 基于FFI的纯 Java 接口定义,底层依赖 LLVM,通过 FFI 访问 LLVM 的 C++ 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);
}
#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)
}
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);
}
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
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
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++ 实现 基于 Aibaba FFI 的 Java 实现,但是关闭 LLVM4JNI,JNI 的额外开销没有任何消除 基于 Alibaba FFI 的 Java 实现,同时开启 LLVM4JNI,一些 native 方法的额外开销被消除
欢迎更多开发者加入Java语言与虚拟机SIG:
网址:https://openanolis.cn/sig/java
邮件列表: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