笔者之前一直在探索Objective-C语言的底层原理,有天突然想到既然iOS未来的编程语言是Swift(其实国外目前已是,国内因大型app的历史包袱过重),我为什么不去研究Swift语言的底层原理,而在一个注定被抛弃的语言上继续挖掘呢。加上目前所在团队也在开始对Swift语言进行尝试,所以探索Swift语言的底层原理明显各方面的价值更大。
说干就干,开始阅读一份项目源码的第一步,是先让这个项目能够通过你自己编译通过并且顺利跑起来,这一点尤其重要。而Swift源码工程比较复杂,搭建起一个调试环境并不容易。因此能自己本地将工程编译运行起来并且可调试对于我们理解和学习Swift底层的原理可以说是不能绕过的前提。这篇的目的就是帮助读者尽快达到本地可调试的状态。
网上资料现状
目前我们在网上能搜到的这方面的资料非常少,而且几乎都存在以下几个问题
- Swift版本非最新版本:对于我们来说继续研究老版本的Swift代码肯定没有去研究最新版的Swift代码有价值。
- 各种编译问题:有一些资料按照其步骤会遇到各种编译问题导致无法正常运行,但是并没有全面的解决方案。
- 无法调试:部分资料能调试SwiftRuntime,但却无法调试SwiftCore,而这两个对于研究Swift底层同样重要。
- 官方方案跑不起来:官方文档的编译方案笔者尝试了很多次都没有办法正常的Run起来,如果有同学解决了请指导我。
明确目标
对于现在的我们,最有价值的内容应该是我们工作中经常用到的内容,比如Foundation中的Array的底层实现,比如Swift中Runtime的底层实现,而像LLVM虚拟机
或者Swift编译器
的底层实现,对于我们来说还不用太着急.
因此我们的目标很简单
- 把 Swift 目前最新的稳定版本(5.8.1)本地编译运行起来,并且可以达到SwiftRuntime和SwiftCore本地可调试
盘它
Disk Space Prepare
整个Swift工程以及其编译后的DerivedData
缓存差不多需要64G
的磁盘空间,建议在开始前清理本地磁盘,空出至少70G的空间以便存放工程相关文件。
Dependency Install
Swift的本地部署过程中,会依赖cmake
,ninja
,sccache
,因此我们需要先把这些依赖安装好。
ruby
$ brew install cmake ninja sccache
Clone&Checkout
首先,我们在我们准备存放我们Swift源码的地方创建一个文件夹,然后cd
到我们的目录:
shell
$ mkdir swift-project
$ cd swift-project
然后,我们在刚才新建的目录中执行如下命令拉取对应的 Swift 源码,并cd
到源码目录:
shell
$ git clone --branch swift-5.8.1-RELEASE git@github.com:apple/swift.git
$ cd swift
最后,拉取源码后还须拉取依赖:
shell
$ ./utils/update-checkout --tag swift-5.8.1-RELEASE --clone
这一步会比较耗时,主要是拉取llvm-project
非常耗时,它有将近4G的大小,可以耐心等待下。当我们看到下面这样的输出时就说明成功把代码盘下来了。
-
扩展阅读
- 如果你想跑其他的Swift版本,或者想跑和你Xcode对应版本的Swift,那么可以通过如下命令来查看
csszixun@zixundeMBP swift % xcrun swift -version swift-driver version: 1.75.2 Apple Swift version 5.8.1 (swiftlang-5.8.0.124.5 clang-1403.0.22.11.100) Target: arm64-apple-macosx13.0
Edit Code
这个时候如果我们直接去执行Swift的build-script
脚本会报如下错误:
从报错来看问题出在cxxshim-OSX-ARM64
的脚本里,原因就是cxxshim
试图创建模块目录导致cxxshim
,我们只需要将 swift/stdlib/public/Cxx/cxxshim/CMakeLists.txt
此文件中的如下命令删除即可。
Build Swift
源码修改完毕后我们开始编译,这里我们需要使用Swift的官方脚本,也就是在util
目录下的build-script
脚本。以下是我使用的命令:
scss
$ utils/build-script \
--release-debuginfo --debug-swift-stdlib \
--xcode --skip-ios --skip-watchos --skip-tvos \
--skip-early-swiftsyntax --skip-build-benchmarks \
--swift-darwin-supported-archs="$(uname -m)"
这个脚本也会比较耗时,因为有这么多子工程需要编译,大家可以一边跑笔者一边给大家解释下各个参数的意思:
--xcode
: 使用CMake
的Xcode生成器,编译完成后会生成一个Swift.xcodeproj
的工程。--release-debuginfo
: 编译出带有RelWithDebInfo
环境变量的工程,类似于Debug
和Release
模式,RelWithDebInfo
会优化一部分,但同时保留调试信息。(相比debug编译速度快非常多)--debug-swift-stdlib
: 编译带有调试信息的 Swift标准库,如果想调试 Swift编译器,可以使用--debug-swift
--skip-build-benchmarks
: 表明在构建过程中跳过编译和构建基准测试的步骤。--skip-early-swiftsyntax
: 表示跳过earlyswiftsyntax
, 这个不加会编译出错。--swift-darwin-supported-archs "$(uname -m)"
: 编译需要的架构,$(uname -m)
命令用于获取机器架构环境,例如笔者机器获取的结果为arm64
--sccache
: 编译缓存工具,可以提高下一次编译的速度-skip-ios --skip-tvos --skip-watchos
: 跳过相应平台,这里只编译macOS
平台。--bootstrapping=off
: 跳过迭代构建过程,直接用现有的编译器构建目标编译器,从而节省编译时间。
可能你会遇到error: using unsupported Xcode version
的错误,解决方法很简单,输入如下命令即可
ini
export SKIP_XCODE_VERSION_CHECK=1
这样build就不会检测xcode版本了,然后我们继续执行上面build-script
命令。
当我们看到如下界面的时候说明我们编译成功了:
编译成功之后,我们会在目录 swift-project
下得到一个build
文件夹,里面就是我们 build 的产物:
Run Swift
接下来,在上面生成的build
目录中我们找到swift-macos-arm64
文件夹下的Swift工程,双击打开Swift.xcodeproj
来打开我们的Swift源码工程。打开后,我们可以看到如下弹框。这里网上很多文章都叫你点击Automatically Create Schemes
来自动生成Schemes,笔者建议是点击Manually Manage Schemes
因为Automatically Create Schemes
会生成一堆scheme,Swift工程本身就已经很大了,再生成这么多scheme会非常卡,我们只要创建我们自己需要的就好。点击Manually Manage Schemes
后会弹出如下弹框:
没关系,直接Close
即可。接下来,咱们需要创建一个用来运行验证代码的Target,在 Swift.xcodeproj
工程里面,我们点击 TARGETS
下面的 +
新建一个调试的 target,我们选择 macOS 的Command Line Tool
来创建
Product Name
按自己喜好取个喜欢的名字就行(笔者的名字叫Swiftabc)。接下来咱们为 target 引入依赖,如下图引入ALL_BUILD
:
然后确认一下Build Setting
下Hardened Runtime
是否已经关闭:
Hardened Runtime
是Apple的一种安全保护策略,具体可以查阅Hardened Runtime | Apple Developer Documentation。
接下来我们如下图点击New Scheme...
来创建我们需要编译的Scheme:
在如下弹框中选择我们创建的Target(Swiftabc),然后点击OK
:
然后我们点击如下图的Edit Scheme...
来编辑我们需要变更的内容:
在如下图的弹框中我们将 Target Scheme
的 Build Configuration
修改为ReWithDebInfo
完成了配置,接下去就让我们Run
一下看看,不出意外的话这个时候就要出意外了,我们会遇到如下的错误:
这里当时纠结了笔者很久,苦苦寻觅得不到解法,直到发现这个Options.h
的文件在llvm
的工程里,抱着试一试编译llvm
工程的心态编译了一把llvm
解决了这个问题(而且发现Swift工程依赖很多llvm编译产物,必须先编译llvm),llvm
工程在如下目录:
双击打开后和Swift
工程一样,我们选择Manually Manage Schemes
:
然后单击弹框中的Close
按钮:
接下来通过New Scheme...
新建我们需要的Scheme:
这里我们选择All_BUILD
Scheme,这个Scheme可以把LLVM工程的库全部编译一遍
记得和Swift工程一样,把Build Configuration
改成RelWithDebInfo
:
选择好之后,我们开始Build LLVM的工程,LLVM的工程非常大,编译比较久,建议打一把王者后回来看看是否编译完成。当你打完王者回来后会发现不出意外的话这个时候又要出意外了:
一开始笔者想了各种办法去把这个被时代抛弃的i386
架构干掉,但是发现怎么干都干不掉,如果有同学知道怎么干掉请指导我。回到这个问题本身,其实不需要解决,因为在编译i386
架构之前,我发现我们需要的arm64
的架构已经编译出来了,所以不解决也没关系。
因此,我们可以回到Swift
工程继续编译大计,继续等待我们的编译成果,不出意外的话又又又要出意外了,我们会得到如下的报错:
这是因为工程找不到cmark,这个问题很好解决,还记得我们通过脚本生成的工程吗,有一个就是cmark:
打开cmark-gfm.xcodeproj
工程,选择All_BUILD
:
同样的,通过Edit Scheme...
把Build Configuration
改成RelWithDebInfo
然后Build一把
不出意外的话这次没有出意外,顺利Build Success。继续回到Swift
工程继续编译大计。这一波,贼稳,不出意外的话漂亮的Build Success
会弹在我们的XCode界面上。那怎么验证我们现在是否可以debug Swift的源码呢?
我们先打一波断点,搜索HeapObject.cpp
文件,然后在文件的第141行打一个断点然后重新Run
一把:
完美,此时我们已经具备了Swift Runtime
的debug环境。接下来我们在main.swift
文件中添加我们想测试的代码。
css
import Foundation
var arr = Array(1...10)
var arr2 = arr.filter { a in
a > 5
}
print(arr2)
然后我们在ArrayType.swift
文件中找到filter
函数,然后打上断点
但是当我们Run起来的时候会发现,这个断点没有进来,怎么肥四!这里笔者纠结了很久,也尝试了网上的办法将build-script
脚本中的--debug-swift-stdlib
参数替换成--debug-swift
,依旧不行。而且--debug-swift
会把包括编译器,LLVM等所有的host tool
的符号全部编译链接进来,慢的一批,编译一次可以打3到4把王者荣耀。除非哪天咱们要去看编译器的源码,不然珍爱生命,远离--debug-swift
。
我们回到XCode,发现控制台有如下输出:
ini
warning: Swiftabc was compiled with optimization - stepping may behave oddly; variables may not be available.
大概意思就是我们试图在使用编译器优化后的代码进行调试,这会让我们的调试变得很奇怪。有了这个线索,我们去看看Sequence.swift
文件中filter
的源码:
swift
@inlinable
public __consuming func filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> [Element] {
return try _filter(isIncluded)
}
方法的正上方有个注解@inlinable
,学过C++
的同学应该知道了,这不就是内敛函数么,Swift的@inlinable
和C++类似,内敛会把我们的方法体复制在调用的时候在调用函数上展开。我们要调试源码就要把这个给关掉。
- 在 Xcode 的 "Build Settings" -> "Swift Compiler - Code Generation" -> "Optimization Level" 设置成 "No Optimization"
- 在 Xcode 的 "Build Settings" -> "Apple Compiler - Code Generation" -> "Optimization Level" 设置成 "None"
然后我们重新Run
一把,可以发现我们完美的进入了filter
函数:
这个时候,我们一般都会想看看_filter
函数里的内容,我们点击step into
,但是却发现进来的却是ContiguousArray
的init
方法
又怎么肥四!我们先找到_filter
函数
swift
@_transparent
public func _filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> [Element] {
var result = ContiguousArray<Element>()
var iterator = self.makeIterator()
while let element = iterator.next() {
if try isIncluded(element) {
result.append(element)
}
}
return Array(result)
}
发现第一行var result = ContiguousArray<Element>()
好像就是初始化ContiguousArray
,难道这个函数又被内敛了。看注解并没有@inlinable
,而是@_transparent
。
我们在工程中全局搜@_transparent
,会搜到如下文档内容:
Semantically, @_transparent
means something like "treat this operation as
if it were a primitive operation". The name is meant to imply that both the
compiler and the compiled program will "see through" the operation to its
implementation.
This has several consequences:
- Any calls to a function marked
@_transparent
MUST be inlined prior to
doing dataflow-related diagnostics, even under -Onone
. This may be
necessary to catch dataflow errors.
- Because of this, a
@_transparent
function is implicitly inlinable, in
that changing its implementation most likely will not affect callers in
existing compiled binaries.
- Because of this, a public or
@usableFromInline
@_transparent
function
MUST only reference public symbols, and MUST not be optimized based on
knowledge of the module it's in. [The former is caught by checks in Sema.]
- Debug info SHOULD skip over the inlined operations when single-stepping
through the calling function.
This is all that @_transparent
means.
大概意思就是@_transparent
也是内敛,并且不会受我们前面设置的Optimization Level
影响.这里我目前找到的唯一方法就是把@_transparent
注释掉。
swift
// @_transparent
public func _filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> [Element] {
var result = ContiguousArray<Element>()
var iterator = self.makeIterator()
while let element = iterator.next() {
if try isIncluded(element) {
result.append(element)
}
}
return Array(result)
}
注释后,我们就可以完美的进入到_filter
函数啦
现在,我们完美的把当前最新的Swift版本5.8.1本地跑了起来,并且对于我们最有价值的SwiftRuntime
和SwiftCore
都可以调试。
One More Thing
当我要切换swift版本重新跑一遍需要怎么做?
-
清空缓存
-
command
+shift
+k
: 清空内存缓存 -
删除
DerivedData
-
-
删除
build
- 删除
swift-project/build
目录下所有内容
- 删除
-
切换版本
- 用脚本切换到你需要的tag,千万不要用
git checkout
,因为swift有很多子仓库需要同步切换
sqlutils/update-checkout --tag swift-XXX-RELEASE
- 用脚本切换到你需要的tag,千万不要用
-
Build&Run
- 按照这篇文档重新Build和Run
当我遇到问题解决不了上哪里搜/问?
-
Github issue: github.com/apple/swift...
-
Swift Forums: forums.swift.org/