Swift 已經發布一年多了,蘋果承諾將在 2015 年底開源 Swift。這是非常棒的一件事情,但是我們現在可以在 Android 設備上運行 Swift 嗎?
這都是由 Chris Lattner 設計的,很容易就可以發現 Swift 的編譯器是基於 LLVM 構建的。LLVM 是個編譯器基礎設施,利用了了一個可重定向編譯器的有趣概念。
也就是說,不是生成特定架構的機器代碼,LLVM 為一個虛擬機生成彙編代碼,然後轉換成中間代碼,適配架構需要的實際代碼。
模塊化的設計非常的好,因為允許高度代碼復用(前端和後端的共享優化)。更多關於 LLVM 的資料請看這裡。
在這一點上,你可能會想:
如果 LLVM 已經夠模塊化,那麼我們是否可以使用一個不同的後端,生成二進位代碼,適配 OS X,iOS 或者是 Android?
假設是可以的,我們來看看如何實現。
手動構建 Swift 代碼如果使用 Xcode,系統會自動完成這些。我們現在需要手動編譯和連接一個簡單的 Swift "Hello world" :
// hello.swiftprint("Hello, world!");
構建對象文件:
$ $SDK/usr/bin/swiftc -emit-object hello.swift
hello.o 裡面到底有什麼:
$ nm hello_swift.o U __TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS U __TFSs27_allocateUninitializedArrayurFBwTGSaq__Bp_ U __TFSs5printFTGSaP__9separatorSS10terminatorSS_T_ U __TIFSs5printFTGSaP__9separatorSS10terminatorSS_T_A0_ U __TIFSs5printFTGSaP__9separatorSS10terminatorSS_T_A1_0000000000000140 S __TMLP_0000000000000100 S __TMaP_ U __TMdSS U __TZvOSs7Process11_unsafeArgvGVSs20UnsafeMutablePointerGS0_VSs4Int8__ U __TZvOSs7Process5_argcVSs5Int32 U _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func6 U _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token60000000000000000 T _main U _swift_getExistentialTypeMetadata U _swift_once
看吧,這非常有趣。Swift mangles symbols 看起來明顯有點像 C++。事實上,print 函數並沒有成為 _print symbol ,但是成為了更複雜的 symbol 的 __TFSs5printFTGSaP__9separatorSS10terminatorSS_T_ 列表。
同時也要求其他 symbols,主要是為了處理字符串轉換和內存處理。
無論如何,所有這些 symbols 已經在 libswiftCore.dylib 定義,也出現在 $SDK。我們現在要把這些信息給 linker:
$ ld -arch x86_64 -o hello hello.o -L$SDK/usr/lib/swift/macosx -lSystem -lswiftCore$ DYLD_LIBRARY_PATH=$SDK/usr/lib/swift/macosx ./helloHello, world!
是的,這個方法是可行的。
適配 Android現在最大的問題是 SwiftCore 庫缺失。現在蘋果已經為 iOS,OS X 和 Watch OS 都提供了一個。但是,很明顯,並沒有提供 Android 版本。
但是,不是所有 Swift 代碼都要求 SwiftCore 庫,跟不是所有 C++ 代碼都要求 STL 一樣。所以只要使用 Swift 的子集,不需要 SwiftCore 的那部分,這問題就算解決了。
為了演示,我們先來一個簡單的:
// add.swiftfunc addTwoNumbers(first: UInt8, second: UInt8) -> UInt8 { return first + second}
所以這過程基本分為 3 個步驟:
讓 Swift 編譯器生成一些 LLVM-IR
使用 LLVM 從中間表示的代碼生成 ARM ELF
使用 Android NDK 來生成一個二進位代碼,連接到已生成的對象文件
在之前的步驟中,當運行 swiftc hello.swift,Swift 編譯器實際在幹兩件事情:
從 Swift 代碼中生成 LLVM 中間表示代碼
轉換 IR 為一些 x86_64 機器代碼,打包為一個 Mach-O 文件
這個實際上是非常常用的事例,所以編譯器可以一次性做完這些。但是我們想要生成一些 ARM ELF 文件 (在 Android 上使用的二進位格式文件)。
$SDK/usr/bin/swiftc -parse-as-library # We don't need a "main" function -target armv7-apple-ios9.0 -emit-ir add.swift | grep -v "^!" # Filter-out iOS metadata > add.ll
注意:我們需要添加 "grep" 過濾器來移除一些 iOS 特定的元數據(Swift 編譯器加進去的) 。
2. 從 LLVM-IR 中生成一個對象文件在這點上,我們需要 Android NDK。非常幸運的是已經包括了一個 LLVM 工具鏈,我們可以利用 llc (LLVM static compiler) :
$NDK/toolchains/llvm-3.5/prebuilt/darwin-x86_64/bin/llc -mtriple=armv7-none-linux-androideabi -filetype=obj add.ll
非常棒,所以我們已經構建了一個 ARM ELF 對象文件!
3. 打包一個 Android 應用的對象文件我們需要從 Java 中調用它,所以需要一個 JNI bridge。這使用 C 來編寫非常簡單:
// jni-bridge.c// Let's work around Swift symbol mangling#define SWIFT_ADD _TF3add13addTwoNumbersFTVSs5UInt86secondS0__S0_uint8_t SWIFT_ADD(uint8_t, uint8_t);jstring jni_bridge(JNIEnv * env, jobject thiz ) { uint8_t a = 123; uint8_t b = 45; uint8_t c = SWIFT_ADD(a,b); char result[255]; sprintf(result, "The result is %d", c); return (*env)->NewStringUTF(env, result);}
最後,我們需要打包所有,變成一個共享庫:
$NDK_GCC/bin/arm-linux-androideabi-ld add.o jni_bridge.o -shared # Build a shared library -lc # We'll need the libc -L$NDK/platforms/android-13/arch-arm/usr/lib
就是這樣!我們需要打包,在一個 Android 應用中分享對象文件,然後運行:
這非常有趣,但是並沒有什麼用:
最後,很重要的一點,這個示例已經放到了 GitHub。
via romain.goyet.com