寫個編譯器,把C++代碼編譯到JVM的位元組碼可不可行?
感覺優點還是挺多的。自由(可以用指針),而且一次編譯處處運行(包括所有支持JVM的嵌入式平台,而不需要交叉編譯)。就是GC不知道怎麼去處理。
題主開了個好腦洞 233
在知乎或者很多其它問答網站(爆棧站也不例外)問這種問題被噴的可能性極其大,因為:- 實際有這種使用場景的人是極小眾;
- 這種做法真的也只有很窄的應用場景…而且也沒辦法跟原生C++的性能比;
- 很多人根本沒仔細想清楚也沒有足夠知識儲備就開噴了。
別怕被噴。至少在知乎問題自身還不能被投反對,大不了被別人投關閉了而已(逃
上面的(1)和(2)是毫無爭議的事實。當然有實際性能實驗的數據更好,這裡就偷懶只定性說了。題主要是有愛的話可以自己試試用下面提到的一些方案來測試一下。題主問把C++源碼編譯為Java位元組碼之後在JVM上運行是否可行。單純說技術上是否可以實現的話,當然是可以實現的——至少到某種頗為可用的程度上可以實現。
Java位元組碼是圖靈完備的,可以表達用圖靈機可計算的運算。在這個意義上說它肯定是可以表達所有C++可以表達的運算的,只是不一定能直接表達——但是肯定可以模擬出來。(總會有人拿偏門的功能,例如說並不在C或C++標準里的內聯彙編之類的功能為例來說肯定不能被編譯到Java位元組碼。那種評論可以先不管。)而真正讓人困惑的是把C++程序放在JVM上運行的動機——把C++寫的程序放JVM上有啥好處,不然 why are you 弄啥嘞?
===============================================
現成方案
這種腦洞顯然不是題主最先開的。早就有前人們實踐過了,挑幾個稍微新一點的例子來說:- NestedVM:通過GCC把C、C++、Fortran等語言編譯到Java位元組碼。它是基於GCC的MIPS後端,在它的基礎上改造出Java位元組碼後端的。項目已經多年沒有更新,最後一個版本是2009年發布的、基於GCC 3.3.6的。
- Cibyl:同樣基於改造GCC的MIPS後端來把C語言編譯到Java位元組碼。跟NestedVM有不少相似之處。
- LLJVM:一個LLVM的Java位元組碼後端以及配套的運行時庫,可以支持諸如C語言在JVM上的運行。
- Proteus Compile System:一個LLVM的Java位元組碼後端及運行時庫,可以支持C/C++。
- Renjin.org | Introducing GCC-Bridge: A C/Fortran compiler targeting the JVM
上面的例子都是直接編譯到Java位元組碼的。那些可以支持C但是沒有說支持C++的環境,再不濟也可以進一步用類似CFront的方式把C++給lower到C之後再進一步編譯下去。
再舉一組例子是雖然也在JVM上運行,但並不直接編譯到Java位元組碼,而是編譯到某種AST之後通過partial evaluation來運行時編譯到機器碼的。它們都是基於Truffle / Graal的實現:- TruffleC:在Truffle框架上實現的C語言運行環境。它有一個應用場景是給RubyTruffle加速C extension的性能:Very High Performance C Extensions For JRuby+Truffle
- Sulong:在Truffle框架實現的LLVM IR運行環境。通過Sulong,可以運行基於LLVM實現的C、C++、Fortran之類的各種語言。
上面都是一些非常實在的、至少當項目還在活躍期的時候相當可用的例子。
而接下來我要幫題主進一步擴展腦洞,想想其它「好玩的」情況。跟JVM相似的高級語言虛擬機,還有Flash VM(AVM2 / Tamarin)和各家的現代JavaScript引擎。
那麼能不能在Flash VM上運行C、C++程序呢?可以的,通過Adobe Alchemy(項目現在叫做FlasCC)。該項目有開源版叫做CrossBridge:Cross compile your C/C++ games to run in Flash Player
於是,把Alchemy的思路移植到JVM上,這個絕對是可行的。但還有更現成的開腦洞的辦法:把Alchemy編譯出來的Flash程序,直接放在JVM上跑。例如說通過Mozilla Shumway來直接運行SWF文件。請跳個傳送門:把Flash遊戲發布到Web上 探討下可行路徑? - RednaxelaFX 的回答那麼能不能在JavaScript引擎上運行C、C++程序呢?可以的,有若干把C、C++編譯到JavaScript或者Web Assembly的途徑,其中最出名的一個是Emscripten,而另一個有趣的新晉選擇是Cheerp。大家或許都見過用Emscripten編譯的DOOM了,可行性是杠杠的。
於是,把它們的思路移植到JVM上,這個絕對也是可行的。但還有更現成的開腦洞的辦法:把Emscripten或Cheerp編譯出來的JavaScript程序,直接放在JVM上跑。例如說通過Oracle JDK 8 / OpenJDK 8自帶的Nashorn JavaScript引擎來運行,又或者是用更老的Mozilla Rhino來運行。最後再舉一組極度腦洞大開的例子。
有一個用純Java寫的x86 PC模擬器,JPC。它可以啟動並運行諸如Windows 95和某些Linux版本。顯然無論是C還是C++寫的程序,只要能用常規的編譯器編譯到在JPC支持的OS上運行的話,它就可以在JPC上運行…
同一系列更腦洞的:Fabrice Bellard大大用純JavaScript寫過一個x86 PC模擬器——Javascript PC Emulator,可以啟動並運行一個定製的Linux。在demo帶的Linux鏡像里還帶有Fabrice大大寫的TCC編譯器。這個模擬器也可以在JVM上的JavaScript引擎上跑,所以…===============================================
插曲:C++/CLI 與 .NET / CLR?
在微軟的.NET平台上,我們也可以看到C++的身影。不過在CLR上可以運行的不是原生C++,而是適配到.NET上的變種:C++/CLI(嗯還有其前身的Managed C++,不是MC++已經黑歷史了,就不討論了)。
CLR所實現的虛擬指令集,CLI VES的指令集——Common Intermediate Langauge(CIL,也叫MSIL),在設計之初就考慮到要兼容unmanaged code的執行,所以位元組碼指令集中包含了一個專門用於支持unmanaged code功能的子集,例如說裸內存訪問 / 指針操作,通過裸函數指針的間接函數調用,等等。C++/CLI則充分利用了這一子集而得以直接高效地運行在CLR上,讓程序員可以用很自然的C++語法寫出managed與unmanaged混搭的程序。
但當然C++/CLI也為了遷就CLR而做了一些功能上的限制,例如說ref class的實例不能夠stack allocate或者是以值的形式聲明為別的類的成員;又例如說ref class不能用bitfield。如果一定要直接使用原生的C++庫的話,偶爾會遇上些邊角問題。
另外,在只允許verified code的配置下,使用了unsafe功能的C++/CLI程序是不能運行的。這也算是個限制吧。雖說JVM與.NET的CLR是有不少相似之處,在許多方面它們都是在一個級別上的東西,但是在對C++的原生支持上,.NET CLR顯然是遠遠走在JVM之前的。
放倆傳送門:- .NET CLR怎麼保證執行正確的unsafe代碼不掛掉? - RednaxelaFX 的回答
- C#能否被編譯成Java位元組碼? - RednaxelaFX 的回答
===============================================
有啥好處?
然而把C或C++寫的程序放在JVM上運行的好處有啥嘞?
在上面列舉的現成方案中,對我來說最有說服力的是TruffleC + RubyTruffle的例子。
RubyTruffle是一個非常高性能的Ruby實現(嗯,除去啟動開銷外…啟動開銷在Substrate VM上的版本會好很多),但如果它不能完美支持CRuby的眾多C擴展的話那對社區來說還是不夠有說服力。而TruffleC則大開腦洞解決了這個問題:有C語言的源碼的C擴展,可以通過TruffleC來跟RubyTruffle跑在一起,而且通過對CRuby的C擴展API做特化實現,TruffleC + RubyTruffle可以使得該系統上運行的Ruby代碼在JIT編譯的時候可以穿透C擴展API的邊界一直內聯到C擴展的一側,消除C擴展邊界上的開銷,達到遠高於原生CRuby的高性能。而Cibyl的作者創建這個項目的目的也很明確:他希望能把一些以前用C寫的遊戲啊啥的移植到支持J2ME的平台上。這種有明確目的的項目,就會為了這具體的目的來做對應的實現,有的放矢。這樣的結果即便性能比不上原生的高,但它可以在只允許客戶自己部署J2ME程序的環境上使用豐富的C程序/庫。
上面提到的Alchemy、Emscripten的初衷也很相似:一個帶有沙箱機制的運行時環境,希望能夠更充分利用現成的各種程序/庫,包括用C、C++寫的庫。特別是在移植老遊戲的場景很流行。但這種思路要用在Java SE上,從實用角度上說感覺說服力不足。
Flash與JavaScript都是很流行的客戶端平台。Flash雖然現在比較沒落了,但就在幾年前它都還很輝煌。在一些安全性要求高的地方可能不允許Flash程序使用native擴展,又或者一個Flash程序想儘可能簡單地跨平台可運行,這些條件下在Flash程序里要想用C或C++寫的庫,FlasCC就是一個很現實的選擇。而JavaScript在瀏覽器里的話則沒有標準的native擴展介面,想用native庫最現實的做法就是編譯到JavaScript來運行。那麼Java SE呢?Java SE也曾經在瀏覽器里輝煌過一小段時間——以Java Applet的形式嵌入到網頁里,給網頁提供動態交互功能。但這市場很快就被後來居上的Flash給完全吞噬了。而一個不以Applet形式運行的Java程序,其實想通過標準的Java Native Interface去使用現成的native庫也不是什麼難事,並沒有足夠動力一定要把native庫自身給編譯成Java位元組碼跑在JVM裡面。說到要把native程序跑在沙箱里…不同層面的解決方案實在多如牛毛啊。
有開系統虛擬機的;
有開系統容器隔離的;有在上述兩者之間的;有在應用層面構建沙箱的;…其中NaCl / PNaCl就是一種在應用層面構建沙箱,以接近原生應用性能運行的解決方案。如果是為了Java帶的沙箱而把C++寫的程序編譯到JVM上運行,大可不必啊。題主或者其他同好有想到什麼好的動機來支持把C++編譯到Java位元組碼的需求的話,我洗耳恭聽 ^_^
===============================================
關於實現的一點討論
就挑幾個小點來討論一下在JVM上跑C++程序會涉及的問題或者「非問題」。
1. 內存怎麼辦?
最常見、直觀的解決辦法就是暴力解法:用一個大byte數組來模擬「native memory」。
其中,最簡單的做法就是用一個Java層面的byte[]來充當「native memory」來給上面跑的C / C++程序用。這樣C / C++的指針則表現為對這個數組的下標(index)。GC要是移動了這個數組怎麼辦?沒關係啊,「指針」只是下標而不是裸的地址,不受GC影響。Alchemy在Flash上實現C/C++程序的native heap就是這麼實現的。而不想把這個「native memory」放在JVM的GC堆里的話,也可以用DirectByteBuffer系API來在真正的native memory里申請一大塊內存來模擬JVM上運行的C / C++程序的native memory。這樣C / C++的指針就表現為對這個DirectByteBuffer的offset,其實跟數組下標也沒啥兩樣。有一大塊byte數組,想怎麼安排裡面的數據的內存布局那都是隨便搞。沒啥模擬不了的。
有兩點需要注意的是:一來,在實現的時候要注意多位元組數據訪問的原子性。JVM只認自己看到的Java層面的類型,如果它看到的是一個byte[]對象它就會按照byte的規則來處理其中的元素訪問的原子性。如果有程序在byte[]上面模擬別的寬度的數據,例如用4個byte模擬一個int,那要需要這個訪問是原子的則需要自己想辦法。二來,這種通過Java層面的byte數組模擬出來的「native memory」跟在JVM外真正自由的native memory不能簡單地互操作。這是下面一點要提到的了。2. C/C++程序之間的互操作?
這種在JVM上運行的C/C++程序,要跟同一個JVM通過JNI訪問的C/C++庫交互的話,是…比較麻煩的。基本上兩者雖然都是C/C++寫的,但卻像是運行在兩個世界裡一樣。
就像同樣是C++寫的程序,編譯給不同OS上的、指針寬度不同的二進位程序之間不能直接互操作;就算是同一個OS上同樣的指針寬度,遵循不同ABI的編譯器編譯出來的結果也不能互操作。在JVM上運行的C/C++程序無法直接跟同一JVM通過JNI訪問的C/C++庫互操作也是一樣的道理。
我以前試過好幾次想用Java寫一個非常簡單的教學用JVM,不要自舉,只要做一個在宿主JVM上跑的解釋器就行。其中一個很糾結的地方就是決定要不要自己實現內存訪問 / GC / 對象布局 / ClassLoader之類的功能。這些東西一旦自己實現了,就跟在JVM上模擬「native memory」一樣,要跟外面通過JNI實現的功能交互就變得麻煩了。想實現個Hello World還得跟System.out.println()打交道呢,而這個println()的底下是什麼?很可能是一個用C寫的函數:(jdk8u/jdk8u/jdk: 141beb4d854d src/solaris/native/java/io/io_util_md.c),然而我的教學用JVM上的Java程序要寫個Hello World我都得費事去做適配,實在麻煩。
3. 基於(操作數)棧的位元組碼的表達能力?
關於在JVM上實現C/C++寫的程序的運行,我見過最無厘頭的觀點是:Java位元組碼是基於棧的,所以不適於實現C/C++。然而這根本不是問題。
基於(操作數)棧的虛擬指令集,與基於(虛擬)寄存器的虛擬指令集,其實主要差別在於表達式的臨時值的訪問方式不同。僅此而已。
對這點感興趣的同學請跳傳送門:寄存器分配問題? - RednaxelaFX 的回答大家用過或者見過IBM XL C/C++編譯器,又或者是HP的aCC編譯器不?它們倆在編譯器前端到優化器、優化器到後端之間傳遞程序用的IR——WCode與UCode2,無獨有偶,都是基於(操作數)棧的。在對操作數棧的使用上,兩者跟Java位元組碼都頗為相似。
這在新一代編譯器里當然算不上是流行的IR設計了,但這種設計在現在還活著的成熟產品里還能看到,也算是能反映出這是個能實現功能的設計了吧。===============================================
有啥漏了寫的或者是評論區有啥好玩的話題,以後再補充上來。以上~
的確有優點,不過不是題主總結的這種。。。我碰到過的有兩類需求吧:
1 有很多現成的C++代碼,但是因為種種原因你需要在jvm平台執行,而又因為種種原因你沒法把它包成native,必須純java byte code
這種需求理論上會碰到,但是解決方法卻不一定是將C++編譯為java位元組碼了,在我看來你拿java重構C++這些項目都來得快2 利用這個做法做調試和保護,比如你的C++代碼運行著崩了,如果自己一行行跟進debug太費事,而且很多時候越界並不是立即崩,而是寫亂,那麼如果我們有個很安全的環境去跑C++,一旦有問題就traceback打出來,不是省時間嗎
嗯,這個比較誘人,不過實際上很多debugger都是這麼乾的,弄個虛擬的環境讓你的C++跑,而且C++貌似也有自己的虛擬機解釋執行的實現,做得比jvm更好,更定製化,因為C++特性是java的超集,java代碼能很直白轉為C++,但是C++某些特性(指針亂指到數組中間,別名引用等)用java就有點麻煩,當然你可以弄個超大byte數組模擬進程內存,但這樣一來也不好檢查越界什麼的了最後再吐個槽,真要做的話,轉jvm byte code不如轉java源碼,然後扔給jdk,(逃C++本來就很跨平台,編譯成JVM的位元組碼的話,你在Windows上會遇到麻煩,就不跨平台了(逃
怎麼自由也成了一個優點,c++里的指針才是真·自由好嗎?
gc的問題可大了,和指針撞上了。哪怕c#里想用指針都得先fix,不然一gc,數據一移動,指針就廢啦。
更別說跨平台這個不切實際的事情了,c和c++可能會有很多底層相關的代碼呢。java里是直接提供功能(類、方法)給上層用的,jvm來擦屁股,現在你想把c++編譯過去,這問題留給編譯器解決?光是要實現c++那麼多語言特性,這編譯器就不好做了啊。我倒感覺這東西是可行的,既然C++本身大部分都是編譯時行為,包括RAII本身的構造和析構的代碼插入,異常的stackunwind之類,那麼可以把C++編譯出來的機器碼翻譯到jvm byte code嘛,不使用GC,不使用對象,完全的機器碼到位元組碼變換,模板也是實例化之後的代碼,繞過了jvm殘廢的泛型限制,繞過了GC的不確定性,jvm層面只能看到基本類型和raw memory塊
你想通過編譯到C++來避免你的C++代碼因為資源管理出錯而出翔,這是不可能的不管如何總是要有程序被編譯成cpu可以直接執行的代碼,jvm位元組碼一次編譯所有平台執行,可是jvm本身可不是一次編譯全平台運行的。除非有一天所有的機器採用同一套指令集架構。至於c++可不可以編譯成jvm位元組碼,要看jvm的指令集是否能完成c++所需的特性。不過不是有c++.net么,應該類似把
安卓NDK早就把所有Linux原生C/C++程序載入到Java API了
異端,不好好寫Java代碼的C++程序員?如果編譯原理好好學了,就不會問可不可行的問題,主要是想的太多。先不說其它的,你既然已經知道GC了,可以先好好看看《The Garbage Collection Handbook (豆瓣)》,然而你說要編譯到JVM的位元組碼,是不是應該先讀完JVM規範再來問呢?
推薦閱讀:
※python 的 tuple 是不是冗餘設計?
※C++中int A::*a里的指針a是什麼?
※C/C++ 里指針聲明為什麼通常不寫成 int* ptr 而通常寫成 int *ptr ?
※c語言b++<15是b和15比,還是b+1和15比?
TAG:Java虛擬機JVM | 編譯原理 | CC | 編譯 | 中間表示編譯原理 |