關於具有二進制兼容性的應用程序二進制接口 GCJ 的說明
許多 Java 應用程序假定它們將只會由一個 Java 虛擬機解釋,而不是由一個像 GCJ 一樣與時俱進的編譯器進行本地編譯的。
這些 Java 應用程序可能假設自己很難在沒有經過重大源代碼修改情況下進行本地編譯的運行環境中運行。顯然,在讀取字節碼流並使用 ClassLoader.defineClass() 方法把它們載入為類的時候,這些應用程序含有它們自己的代碼。
可以看到那是一個怎樣的問題,因為沒有字節碼流是用於一個本地編譯了的類的。如果本地編譯一個使用 defaineClass() 的程序,當你要結束一個類,這個類嘗試為可能不曾以字節碼存在的別的類讀取一個字節碼流,這是無法實現的。更進一步,當你本地編譯一個類,指望它載入其他類的時候,過去默認的是相當於只把它作為一個本地編譯了的類給找出來。
考慮到這個問題,GCJ 4 有一個新的編譯模式,稱為間接發布,它使用新的具有二進制兼容性的應用程序二進制接口。如果你喜歡,你可以把舊的編譯模式當作“直接發布”。詳情請參閱 ftp://gcc.gnu.org/pub/gcc/summit/2004/GCJ%20New%20ABI.pdf
簡而言之,為了完成它的工作,GCJ 使用 gcj-dbtool(參閱 gcj-dbtool(1)),它產生一個稍後由 GCJ 使用的“。db”文件。想法是,首先使用 -findirect-dispatch 選項對你的 .jar 或 .class 文件(你打算本地編譯的應用程序依賴這些文件)進行本地編譯。然後使用 gcj-dbtool 把放在一個 .db 數據庫文件中的關於那些本地編譯了的類的信息存儲起來。
在你正在構建的程序的本地編譯過程中,當 GCJ 看到 defineClass() 准備被調用的時候,就把已經通過了的字節碼跟共享庫匹配(使用 .db 文件中的映射表),這些共享庫包含你已經本地編譯了的類。
從一個類到在 .db 文件中的編譯形式的映射,映射為一個類的簽名(一個加密校驗和)到一個共享庫。你可以把它看成像使用一種緩存的 JIT 一樣使用 GCJ.
還有其他關於具有二進制兼容性的應用程序二進制接口的重要細節,這些細節用於確定本地代碼遵循某些二進制兼容性規則。這對於讓本地代碼在處理 .class 文件時得以正確執行是個不錯的輔助作用。 :)
大部分典型的不使用自定義類加載器(classloaders)的 Java 應用程序不必為此擔心。具有二進制兼容性的應用程序二進制接口可以為你正在不依賴 gcj-dbtool 地構建中的代碼啟用(通過把 -findirect-dispatch 選項傳給 gcj )而生成一個二進制程序,它應該不理會對 libgcj 和其他依賴的類庫的修改而繼續工作(參閱 gcj(1))。另一方面,如果你使用舊的“C++ 應用程序二進制接口”風格(即不使用 -findirect-dispatch 選項)編譯一個應用程序,它會在對所依賴類庫的公共 API 的發生任何修改時馬上中斷 —— 就像 C++ 一樣。在過去,正如你可以想象的,這是一個重大的限制並且是更廣泛應用 GCJ 的一個障礙。
在啟用了二進制兼容性的本地編譯構建完成以後,對於外界來說,原始的應用程序仍然做著它以前一直在做的事 —— 例如,和往常一樣從一些沒有本地編譯過的 .jar 文件中載入字節碼。然而,defineClass() 過去一直在做的調用過程現在已經被改進了;它們首先嘗試找出所尋找的類的一個本地編譯形式(很可能現在在一個共享庫中),如果沒有找到本地編譯了的類的話,則回去解釋 .class 文件(很可能在一個 .jar 文件中)。
如果你希望能從本地代碼中調用解釋代碼,你需要在構建本地代碼時使用 -findirect-dispatch 選項。如果不使用間接發布,你的代碼將不能夠在需要時載入字節碼形式的類。如果在代碼中你從來沒有提及過“new ThatClassIWant();”的話,你可以私下實現這種需求,手動加載類,並使用工廠模式,但那會很快變得單調乏味。
從解釋代碼中調用本地代碼會工作得很好,無需關心應用程序二進制接口。對解釋代碼使用間接發布在定義上是缺省的。沒錯,為本地編譯而使用 -findirect-dispatch 選項應該是缺省的,但仍未如此 —— 大部分是因為它還沒完善。主要存在的障礙如下:
CNI,因為 C++ 編譯器仍不明白具有二進制兼容性的應用程序二進制接口
並且當被使用於源代碼編譯時(即從 .java 到 .class),程序缺陷(bug(s))會影響 -findirect-dispatch 選項。
當然, 目標是只要問題一解決就馬上把二進制兼容性作為應用程序二進制接口的缺省的特性。
還有一個來自具有二進制兼容性的應用程序二進制接口的性能打擊。Bryce McKinlay 完成的測試表明這與多數應用程序關系不大(<= 10%),但若讓它擁有更多數據會比較好。
編譯 JAR
重要:有些時候當 -findirect-dispatch 選項直接從 Java 源文件編譯為本地代碼的時候並不總是可以工作的。那種情況仍未完全實現(在 gcj 4.0.x 時),且不被支持。唯一被支持的帶有 -findirect-dispatch 選項的編譯方式就是在這裡說明了的方式。
第一步是在應用程序中編譯所有 JAR 文件。例如:
gcj -shared -findirect-dispatch -Wl,-Bsymbolic -fjni -fPIC myapp.jar -o myapp.jar.so
這將編譯所有包含了的類。注意,沒有要求設置類路徑(classpath);通過具有二進制兼容性的應用程序二進制接口,所有類在運行時被鏈接。當使用 -findirect-dispatch 選項時當前必須使用 -Wl,-Bsymbolic 選項。
設置數據庫
首先創建一個數據庫。
gcj-dbtool -n myapp.db
現在,添加所有編譯了的 jar 文件到數據庫,例如:
gcj-dbtool -a myapp.db myapp.jar myapp.jar.so
運行
現在可以運行你的應用程序了。用 gij 啟動它,正如你用 Java 命令啟動它一樣。然而,還要把 gij 指向你創建的 .db 文件:
gij ——cp myapp.jar -Dgnu.gcj.precompiled.db.path=myapp.db org.package.ClassName etc
注意:應用程序的 jar 文件仍然需要在 classpath 中。
便利方法
你有一堆 JAR 文件而又有點懶於手工完成所有的二進制兼容性編譯工作嗎?這裡有一份腳本,遍歷進入每個目錄,用 GCJ 處理每個 JAR 文件並把新的庫添加到數據庫文件中。
#!/bin/sh
# ${1} is the name of a GCJ database file
gcj-dbtool -n ${1}
for JAR_FILE in `find -iname "*.jar"`
do
echo "Compiling ${JAR_FILE} to native"
gcj -shared -findirect-dispatch -Wl,-Bsymbolic -fjni -fPIC -o ${JAR_FILE}.so ${JAR_FILE}
gcj-dbtool -a ${1} ${JAR_FILE} ${JAR_FILE}.so
done
以數據庫文件的名字調用這份腳本並稍等片刻。如果所有事情都完成了,這將會給你生成所有的本地庫以及一個滿載的數據庫,這些庫和數據庫現在可以用於使用 GCJ 來運行應用程序了。
范例
對一些現實的例子感興趣嗎?進入 《Classpath 用例》 頁面。在那裡你可以看到構建本地的 Eclipse 是多麼容易。
故障糾紛
1. 我如何區分代碼是運行於解釋模式還是本地模式呢?
在 gdb 下運行應用程序,在你的代碼中設置一個斷點。設置在被解釋的代碼中的斷點會完全不能工作(然而,某些斷點可能因為 gdb 的其他程序缺陷而工作失效,因此不要依賴它作為一種跡象),並且被解釋的代碼將以 _Jv_InterpMethod::run 或類似的形式出現在 gdb 的後台跟蹤中。
作為一種選擇,在 gij 4.0 中有一個奇怪現象可以利用。gij 中的棧跟蹤將顯示若干行“(Unknown Source)”,就是正在被解釋的代碼,而顯示庫名稱的就是被編譯的代碼。
注意:這個“(Unknown Source)”的奇怪現象將在 4.1 中去除。
2. 即使按照上面的說明去做,我的代碼仍運行在解釋模式中。為什麼呢?
在 gcc 4.0 中,。jar.so 文件由於某些原因而載入失敗時不會有沒有錯誤報告 —— 它們只是默默地被忽略。這個策略在以後可能會改變。
對於代碼不能正確運行於解釋模式有兩個很大可能的原因:
a)你忘記了使用 JNI(Java 本地接口)的 -fjni 編譯器選項編譯代碼。這種情況下虛擬機將在每當它嘗試載入與那些庫相關的類的時候,重復嘗試載入鏈接庫並失敗。(鏈接庫失敗是因為它嘗試以 CNI 形式鏈接,那是失敗的。)
或者
b)你的映射文件(例如 class.db)過時了,或者沒有在 gnu.gcj.compiled.db.path 中。
3. 當我在 gdb 下運行應用程序時,gdb 把我的庫一次又一次地載入很多次。
你大概忘記了在編譯時指定 -fjni 選項。請參閱上面 2.a 部分的內容。