跨語言程式設計的探索 | 龍蜥技術
跨語言程式設計是現代程式語言中非常重要的一個方向,也被廣泛應用於複雜系統的設計與實現中。本文是 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++ 應用
為了解決這些問題,兩個團隊展開了合作,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 #endifc. 實現標頭檔案中的函式,在這裡我們直接使用 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);
-
首先,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 } }遺憾的是,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 會將執行過程中的熱點方法編譯成可執行程式碼,使得這些方法可以直接執行(避免瞭解釋位元組碼執行)。在編譯過程中應用了許多優化技術,內聯便是其中最重要的優化之一。簡單來說,內聯是把被呼叫方法的執行邏輯嵌入到呼叫者的邏輯中,這樣不僅可以消除方法呼叫帶來的開銷,同時能夠進行更多的程式優化。
概覽
Alibaba FFI 專案致力於解決 Java 跨語言程式設計中遇到的問題,從整體上看專案分為以下兩個模組:
-
一套 Java 註解和型別 -
包含一個註解處理器,用於生成膠水程式碼 -
執行時支援
-
實現 bitcode 到 bytecode 的翻譯,打破 Java 方法與 Native 函式的邊界 -
基於FFI的純 Java 介面定義,底層依賴 LLVM,通過 FFI 訪問 LLVM 的 C++ API
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 方法的呼叫。
|
|
|
|
|
|
|
|
-
純粹的 C++ 實現 -
基於 Aibaba FFI 的 Java 實現,但是關閉 LLVM4JNI,JNI 的額外開銷沒有任何消除 -
基於 Alibaba FFI 的 Java 實現,同時開啟 LLVM4JNI,一些 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、飛騰、海光、兆芯、鯤鵬晶片。
歡迎下載:
加入我們,一起打造面向未來的開源作業系統!
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70004278/viewspace-2795511/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Battleship程式設計語言與技術BAT程式設計
- 技術門檻高?來看 Intel 機密計算技術在龍蜥社群的實踐 | 龍蜥技術Intel
- Java技術分享之函數語言程式設計!Java函數程式設計
- Java技術分享之函數語言程式設計Java函數程式設計
- 1024程式設計師節開幕,龍蜥多位技術專家參與演講程式設計師
- 致敬 hacker :盤點記憶體虛擬化探索之路|龍蜥技術記憶體
- Python函數語言程式設計術語大全Python函數程式設計
- 輕量易部署!Coolbpf 釋出不依賴 Clang 的指令碼化程式設計特性 lwcb | 龍蜥技術指令碼程式設計
- JavaScript函數語言程式設計之為什麼要函數語言程式設計(非嚴謹技術層面的扯淡)JavaScript函數程式設計
- 助力Koordinator雲原生單機混部,龍蜥混部技術提升CPU利用率達60%|龍蜥技術
- 龍蜥利器:系統運維工具 SysAK的雲上應用效能診斷 | 龍蜥技術運維
- 萬里資料庫加入龍蜥社群,打造基於“龍蜥+GreatSQL”的開源技術底座資料庫SQL
- 探索 Web API:SpeechSynthesis 與文字語言轉換技術WebAPI
- ‘程式語言‘ ’程式設計工具’程式設計
- 程式語言設計,程式設計哲學程式設計
- 作業系統遷移難?Alibaba Cloud Linux 支援跨版本升級 | 龍蜥技術作業系統CloudLinux
- 載入速度提升 15%,關於 Python 啟動加速探索與實踐的解析 | 龍蜥技術Python
- 直播回顧:隱私計算的關鍵技術以及行業應用技巧 | 龍蜥技術行業
- eBPF 雙子座:天使 or 惡魔?| 龍蜥技術eBPF
- 龍蜥開源Plugsched:首次實現 Linux kernel 排程器熱升級 | 龍蜥技術Linux
- 龍蜥社群高效能儲存技術 SIG 11 月運營回顧 | 龍蜥 SIG
- 龍蜥開源核心追蹤利器 Surftrace:協議包解析效率提升 10 倍! | 龍蜥技術協議
- js函數語言程式設計術語總結 - 持續更新JS函數程式設計
- c語言程式設計題C語言程式設計
- 函數語言程式設計函數程式設計
- JAVA語言程式設計思想Java程式設計
- RAC的函數語言程式設計函數程式設計
- 龍蜥LoongArch架構研發全揭秘,龍芯開闢龍騰計劃技術合作新正規化架構
- Scala 函數語言程式設計(一) 什麼是函數語言程式設計?函數程式設計
- R語言程式設計藝術 第2章 向量(上)R語言程式設計
- Inspur KOS 龍蜥衍生版面向智慧新媒體轉型的探索與實踐 | 龍蜥案例
- 微機原理與介面技術-第四章-組合語言程式設計組合語言程式設計
- Java如何呼叫C語言程式,JNI技術JavaC語言
- 基於 Coolbpf 的應用可觀測實踐 | 龍蜥技術
- SysOM 案例解析:消失的記憶體都去哪了 !| 龍蜥技術記憶體
- 函數語言程式設計-鏈式程式設計RAC函數程式設計
- .NET併發程式設計-函數語言程式設計程式設計函數
- 量化/合約跟單/系統程式設計開發/策略交易開發技術/Python程式語言程式設計Python