原文地址:Manual Swift: Understanding the Swift/Objective-C Build Pipeline
Xcode 是如何將 Swift 和 Obj-C 編譯到一起的?
如果你沒有 xcodebuild
的話,應該要怎么做?
我們來看看“編譯到一起”兩種不同的方式:
- Obj-C 使用 Swift
- Swift 使用 Obj-C
今天,我們將會使用 Swift 的風格來看待 Obj-C,目的是讓你對這些處理過程的線索有個大致理解。改天我們再挖掘這些線索在實際過程中是如何做到的。
Obj-C 代碼是如何使用其他 Obj-C 代碼的?
首先,我們來看看Obj-C 代碼是如何使用其他 Obj-C 代碼的。顯然,使用 Swift 構建的 Obj-C 建立在此之上,所以大部分繁雜的工作實際上都發生在這個處理過程中。這也意味着,一旦你搞明白了,就對 Obj-C 如何使用 Swift 理解了 90%!
逐個編譯,再鏈接全部
總的來說,構建一個 Obj-C 代碼使用其他 Obj-C 代碼的可執行文件,有兩步處理:
-
編譯(Compile):每個文件都會被編譯成一個目標文件:A.m -> A.o
-
鏈接(Link):所有的目標文件都被鏈接器合並成一個可執行文件:A.o, B.o, … -> MyApp
頭文件承諾,編譯器信任,鏈接器驗證
編譯和組合(鏈接)的步驟依賴於來自頭文件的信息。
頭文件承諾
頭文件對 API 們做出承諾。就像如下代碼:
1
|
NSString *NSTemporaryDirectory();
|
表示:
相信我!這兒會有個叫
NSTemporaryDirectory
的方法,如果你沒有用任何參數調用它,它會返回一個NSString *
。
一個這樣的接口聲明:
1
2
3
|
@interface Something: NSObject
- (
BOOL)makeItSo: (NSError **)outError;
@end
|
表示:
聽我的:當你需要的時候,會有一個名為
Something
的NSObject
類型的類。這個類的所有方法?就像這個接口聲明的那樣,盡管相信我吧!
編譯器信任
編譯器照着這些頭文件說的做,並針對他們的承諾檢查了它們所提供的實現文件。
假如碰到:
1
|
NSTemporaryDirectory(updatedTemporaryDirectoryPath);
|
它會大發脾氣道:
你這什么意思,給這東西傳個參數!而且會有一個返回值,使勁吹吧!難道還會有人想要有返回值?!
(編譯器非常容易激動,這就是他們如何對工作所需的細節表現出的極度關注。盡管如此,這些行為並沒有任何意義。)
如果所有東西都檢查通過,編譯器將會保持安靜,完成其工作,並留下從實現代碼翻譯而來的目標代碼后離開。
(這個經過翻譯的目標代碼會被嵌入額外假設,這些額外的假設基於像這些承諾:如何給函數傳遞參數──傳到堆棧上?寄存器中?向量寄存器中?以及從哪里讀出返回值?但這里牽涉到需要描述什么是 ABI
,所以我們下次再討論這些。)
目標代碼完全相信,頭文件所承諾的函數定義在后面確實會存在!編譯器輸出代碼,說:“嘿,去調用這個函數吧,雖然我不知道它在哪兒,但有一些頭文件承諾它會在那里,所以我只是在這里做一下信任。”
結果便是,目標代碼帶着一堆未定義的引用。所有這些未定義的引用都是關於,什么東西在哪里、有多少個參數,是什么類型的參數與返回值的假設。
編譯器對這些頭文件有很大的信心,是嗎?
旁白:
有多少未定義的引用?你自己看!
選擇一個對象文件A.o
並運行nm -u A.o
。輸出將會是列出該文件所引用的所有未定義名稱。nm
是一個工具,它以人類可讀的方式格式化對象文件所引用的名稱的表格,稱為符號表。(nm 是 NaMes,懂了嗎?),它也可以用來過濾列表,就如這里的-u
要求它只列出未定義的名字。
鏈接器驗證
編譯器總結說,“嘿,東西都在這兒了,一些頭文件承諾的!”
鏈接器則會說,“把錢拿出來看看”。他們把所有的目標文件堆在一起,並處理了那些未定義的引用。如果有東西沒有檢查成功,他們會翻桌子,丟出錯誤,並拒絕繼續執行:
1
2
3
4
5
6
|
> cc trust-me.m
Undefined symbols for architecture x86_64:
"_thisWillTotallyBeThere", referenced from:
_main in trust-me-c9e7ba.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
|
所有的目標文件都被送往鏈接器,鏈接器連接它們的外部名稱並檢查所有的東西是否如實被定義了。如果是的話,鏈接器會吐出一個可執行文件。
當然,還有更多的東西。我們沒有觸及模塊或動態鏈接(frameworks!dylibs!dlopen!)。如果你覺得不夠詳細,可以看看Advanced Mac OS X Programming。
從 Obj-C 使用 Swift
呼,這真是有夠消遣的了。幸運的是,我們差不多完成了。
我們來讓 Swift 變得看起來像 Obj-C
Obj-C 的編譯依賴於頭文件和目標文件。但Swift不需要頭文件,而且你可能也從來沒有碰到過 Swift 的目標文件。那 Xcode 要如何解決這個問題?
當然是給每個 Swift 文件生成單獨的頭文件了目標文件啦!
每個 Swift 文件都會被編譯為一個目標文件和一個供 Obj-C 使用的頭文件: A.swift -> A.o, A.h
這告訴了我們,運行普通的 Obj-C 構建和鏈接編譯管道需要什么東西:
- 編譯每個.m文件的橋接頭文件
- 將所有的目標文件(Swift 和 Obj-C)連接成一個可執行文件
復數橋接頭文件
因為該頭文件在 Obj-C 和特定的swift文件的世界之間架起了橋梁,所以它被稱為橋接頭文件。
現在,你可能會遇到這種情況,“你正在添加 Obj-C 文件!到一個 Swift 的項目!你想要一個橋接頭文件?”之前在 Xcode 中的提過。這也只是一個橋接頭文件,而不是一堆(很多個!每個 Swift 文件一個)橋接頭文件。一座橋連接一個堤岸到另一個堤岸,而單數橋接頭文件則從 Swift 架往 Obj-C 大陸,復數橋接頭文件則從 Obj-C 大陸通往 Swift 之地。
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// ===[ CallMeFromObjC.swift ]===
// 你必須 import Foundation 使其能夠調用 Obj-C.
import Foundation
// 一個類若要對 Obj-C 可見則必須繼承自 NSObject
// (如果你想要寫一個根類,則必須在 Obj-C 上寫)
public class CallMeFromObjC: NSObject {
// 公開你想在 Objc-C 上使用的 API
public var name: String
public init(name: String) {
self.name = name
}
public func speak() {
print("\(self)'s name is: \(name)")
}
}
|
用編譯管道運行下面的粗糙調用:
1
2
3
4
5
6
7
8
9
|
install -d build
xcrun -sdk macosx10.12 \
swift -frontend -c -primary-file CallMeFromObjC.swift \
-sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/ \
-module-name Bridgette \
-emit-module-path build/Bridgette.swiftmodule \
-emit-objc-header-path build/CallMeFromObjC.h \
-enable-testing -enable-objc-interop -parse-as-library \
-o build/CallMeFromObjC.o
|
下面則是它生成的橋接頭文件的核心代碼:
1
2
3
4
5
6
7
8
|
SWIFT_CLASS(
"_TtC9Bridgette14CallMeFromObjC")
@interface CallMeFromObjC : NSObject
@property (nonatomic, copy) NSString * _Nonnull name;
- (
nonnull instancetype)initWithName:(NSString * _Nonnull)name OBJC_DESIGNATED_INITIALIZER;
- (
void)speak;
- (
nonnull instancetype)init SWIFT_UNAVAILABLE;
@end
|
(為了方便起見省略了大堆頂部的定義,你可以在這個 gits 里查看完整細節。看他們如何處理警告信息也是很有趣。)
怎樣導入?
編譯和鏈接 Obj-C 用於 Swift,也意味着編譯 Swift 並將其與其他 Swift 鏈接起來。當編譯器為swift文件生成一個目標文件時,它也需要大量的信任。
在這種情況下,項目中的其他 Swift 文件允許編譯器的外部定義。對,Swift 文件本身就是有效的頭文件!
從其它文件導入命名到模塊中在源碼里是隱式的:當你在 A.swift 中編寫代碼時,你可以自由使用 B.swift 中定義的類型B,並且你只需認為它是可用的就行。如果這是 Obj-C,那就好像你項目中的每個 .m 文件都會自動獲得一系列你項目中的每個.h文件的 #imports
。
盡管如此,Swift 代碼並沒有 #imports
來命名特定文件,以便將其編譯為單個 .swift 文件。因此,不是將模塊中的所有文件都在 .swift 文件列入,而是將該列表移至編譯器調用:當你編譯單個 Swift 文件時,編譯器調用會把該模塊中別的 Swift 文件都列出來。
好消息是,Xcode 正為你編寫着這些編譯器調用,是吧?
總結
Obj-C 編譯過程是對提供了為映射步驟帶出確實上下文的頭文件的映射和合並。映射到目標文件;合並為可執行文件。
用 Obj-C 使用 Swift 是為了讓 Swift 看起來像 Obj-C,所以普通的 Obj-C 構建管道就能發揮其魔力。為了讓 Swift 看起來像 Obj-C,每個 Swift 文件都會被編譯成相應的頭文件和目標文件。
這是一個高層次的概覽。要真正理解正在發生的事情,我們需要仔細研究 Xcode 的構建日志以了解所有這些細節。但這又是改天的工作了。
http://blog.joyingx.me/2018/03/27/理解-Swift-Objective-C-的構建管道/