Swift 已经发布一年多了,苹果承诺将在 2015 年底开源 Swift。这是非常棒的一件事情,但是我们现在可以在 Android 设备上运行 Swift 吗?
Swift 编译器
这都是由 Chris Lattner 设计的,很容易就可以发现 Swift 的编译器是基于 LLVM 构建的。LLVM 是个编译器基础设施,利用了了一个可重定向编译器的有趣概念。
也就是说,不是生成特定架构的机器代码,LLVM 为一个虚拟机生成汇编代码,然后转换成中间代码,适配架构需要的实际代码。
模块化的设计非常的好,因为允许高度代码复用(前端和后端的共享优化)。更多关于 LLVM 的资料请看这里。
适配不同的机器
在这一点上,你可能会想:
如果 LLVM 已经够模块化,那么我们是否可以使用一个不同的后端,生成二进制代码,适配 OS X,iOS 或者是 Android?
假设是可以的,我们来看看如何实现。
手动构建 Swift 代码
如果使用 Xcode,系统会自动完成这些。我们现在需要手动编译和连接一个简单的 Swift "Hello world" :
1
|
// hello.swiftprint("Hello, world!"); |
构建对象文件:
1
|
$ $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_token6 0000000000000000 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:
1
2
3
4
5
6
|
$ ld -arch x86_64 -o hello hello.o -L$SDK /usr/lib/swift/macosx -lSystem -lswiftCore $ DYLD_LIBRARY_PATH=$SDK /usr/lib/swift/macosx . /hello Hello, world! |
是的,这个方法是可行的。
适配 Android
现在最大的问题是 SwiftCore 库缺失。现在苹果已经为 iOS,OS X 和 Watch OS 都提供了一个。但是,很明显,并没有提供 Android 版本。
但是,不是所有 Swift 代码都要求 SwiftCore 库,跟不是所有 C++ 代码都要求 STL 一样。所以只要使用 Swift 的子集,不需要 SwiftCore 的那部分,这问题就算解决了。
为了演示,我们先来一个简单的:
1
2
|
// add.swiftfunc addTwoNumbers(first: UInt8, second: UInt8) -> UInt8 { return first + second} |
所以这过程基本分为 3 个步骤:
-
让 Swift 编译器生成一些 LLVM-IR
-
使用 LLVM 从中间表示的代码生成 ARM ELF
-
使用 Android NDK 来生成一个二进制代码,连接到已生成的对象文件
1. 让 Swift 编译器生成一些 LLVM-IR
在之前的步骤中,当运行 swiftc hello.swift
,Swift 编译器实际在干两件事情:
-
从 Swift 代码中生成 LLVM 中间表示代码
-
转换 IR 为一些 x86_64 机器代码,打包为一个 Mach-O 文件
这个实际上是非常常用的事例,所以编译器可以一次性做完这些。但是我们想要生成一些 ARM ELF 文件 (在 Android 上使用的二进制格式文件)。
1
2
3
4
5
6
7
|
$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) :
1
2
3
4
|
$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 来编写非常简单:
1
2
3
4
5
6
7
8
9
|
// 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);} |
最后,我们需要打包所有,变成一个共享库:
1
2
3
4
5
6
|
$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 应用中分享对象文件,然后运行:
总结
这非常有趣,但是并没有什么用:
-
一般来讲,NDK 只是对一小部分的应用有意义,所以情况的 Google 反对使用 NDK 编写整个 Android 应用。
-
而且,因为我们缺失 SwiftCore 库,所以有了一定的限制,只适用于一小部分的 Swift 子集。
最后,很重要的一点,这个示例已经放到了 GitHub。
via romain.goyet.com