猫·仁波切

会研发的PM才是好OP.

Module System of Swift (简析 Swift 的模块系统)

声明: 转载注明我或者 SwiftChina, 请在方便的情况下情尽量告知. weibo

本文的发现基于个人研究。请尊重原创。

引子

你之所以认为 Swift 最像 Scala, 那是因为你还没学过 Rust. —- 猫·仁波切

Swift 中模块是什么?当写下 Swift 中一句 import Cocoa 的时候到底整了个什么玩意?官方 ibook 很含糊只是提了半页不到。

本文解决如下问题

  • 介绍 Swift 中两种可 import 的模块
  • 如何用 Swift 写一个可被其他 Swift 代码使用的模块
  • 分析 Swift 的标准库实现方式

第一部分 Clang 模块(系统模块)

Clang 模块是来自系统底层的模块,一般是 C/ObjC 的头文件。原始 API 通过它们暴露给 Swift ,编译时需要链接到对应的 Library。

例如 UIKitFoundation 模块,从这些模块 dump 出的定义来看,几乎是完全自动生成的。当然, Foundation 模块更像是自动生成 + 人工扩展(我是说其中的隐式类型转换定义、对 Swift 对象的扩展等,以及 @availability 禁用掉部分函数。)。相关函数声明可以从 我的 Github andelf/Defines-Swift 获得。

我可不觉得这些定义全部都是官方生成后给封装进去的。所以在整个 Xcode-6 beta2 目录树里进行了探索。

在 Xcode 目录寻找相关信息,最后目标锁定到了一个特殊的文件名 module.map

原来这个文件叫 Module map(这个名字还真是缺乏想象力),属于 llvm 的 Module 系统。本来是用来颠覆传统的 C/C++/Objc 中的 #include#import。最早在 2012 年 11 月的 LLVM DevMeeting 中由 Apple 的 Doug Gregor 提出 1。相关内容 CSDN 也有文章介绍,不过是直译版,没有提出自己见解 2

关于 llvm Module 系统

2012 年提出概念,所以其实这个东西已经很早就实现了 。简单说就是用树形的结构化描述来取代以往的平坦式 #include, 例如传统的 #include <stdio.h> 现在变成了 import std.io;, 逼格更高。主要好处有:

  • 语义上完整描述了一个框架的作用
  • 提高编译时可扩展性,只编译或 include 一次。避免头文件多次引用,只解析一次头文件甚至不需要解析(类似预编译头文件)
  • 减少碎片化,每个 module 只处理一次,环境的变化不会导致不一致
  • 对工具友好,工具(语言编译器)可以获取更多关于 module 的信息,比如链接库,比如语言是 C++ 还是 C
  • 等等

所以这么好的一个东西, Apple 作为 llvm 的主力,在它的下一代语言中采用几乎是一定的。

算了,我是个半路出家的,之前没接触过 iOS / MacOSX 开发,其实 2013 年的 WWDC, Apple 为 Objective-C 加入的 @import 语法就是它。可以认为,这是第一次这个 Module 系统得到应用。

module.map 文件

module.map 文件就是对一个框架,一个库的所有头文件的结构化描述。通过这个描述,桥接了新语言特性和老的头文件。默认文件名是 module.modulemapmodule.map 其实是为了兼容老标准,不过现在 Xcode 里的还都是这个文件名,相信以后会改成新名字。

文件的内容以 Module Map Language 描述,大概语法我从 llvm 官方文档 3 摘录一段,大家体会一下:

1
2
3
4
5
6
7
8
9
10
11
module MyLib {
  explicit module A {
    header "A.h"
    export *
  }

  explicit module B {
    header "B.h"
    export *
  }
}

类似上面的语法,描述了 MyLibMyLib.AMyLib.B 这样的模块结构。

官方文档 [^3] 中有更多相关内容,可以描述框架,描述系统头文件,控制导出的范围,描述依赖关系,链接参数等等。这里不多叙述,举个 libcurl 的例子:

1
2
3
4
5
module curl [system] [extern_c] {
    header "/usr/include/curl/curl.h"
    link "curl"    
    export *
}

将此 module.map 文件放入任意文件夹,通过 Xcode 选项或者命令行参数,添加路径到 import search path (swift 的 -I 参数)。 然后就可以在 Swift 代码里直接通过 import curl 导入所有的接口函数、结构体、常量等,(实测,发现 curl_easy_setopt 无法自动导入,看起来是声明语法太复杂导致)。甚至可以直接从 swift repl 调用,体验脚本语言解释器般的快感(因为我们已经指定了链接到 curl 库)。

Xcode 选项位于 Build Settings 下面的 Swift Compiler - Search Paths 。添加路劲即可。

再举个复杂点的 SDL2.framework 的例子,看看如何实现树形的模块结构,这个需要把 module.map 放到 .framework 目录里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
framework module SDL2 [system] {
  umbrella header "SDL.h"
  link -framework SDL2

  module Version {
    header "SDL_version.h"
    export *
  }

  module Event {
    header "SDL_events.h"
    export *
  }
  // ....
  export *
  module * {
    export *
  }
}

小结

Swift 的 C 模块(也是它的标准库部分)完全就是 llvm 的 Module 系统,在 import search path 的所有 module.map 中的模块都可以被识别,唯一缺点可能是如果有过于复杂用到太多高级 C 或者黑暗 C 语法的函数,无法很好识别,相信以后的版本会有所改善。

所以当有人问 Swift 到底有多少标准库的时候,答案就是,基本上系统里所有的 Objective-C 和 C 头文件都可以调用。自 iOS 7 时代,这些头文件就已经被组织为 Module 了,包括标准 C 库 Darwin.C。同样因为 Module 系统来自于传统的 C/C++/Objc 头文件,所以 Swift 虽然可以有 import ModA.ModB.ModC 的语句,但是整个模块函数名字空间还是平坦的。

一些有意思的模块可以探索探索,比如 simd,比如 Python(没错是的,直接调用 Python 解释器)等。

另外 Swift 的 -module-cache-path 参数可以控制这类模块预编译头的存放位置( .pcm 文件: pre compiled module)。

Xcode 项目的 Build Settings , Apple LLVM 6.0 - Language - Modules 有项目对 Module 支持的相关选项,默认是打开的。

第二部分 Swift 模块

说完了系统模块,该说 Swift 模块了。 Swift 自身的这个系统还是很赞的。

本节介绍怎样用 Swift 创建一个可 import 的模块。

几个文件类型

先清楚几个文件类型。假设 ModName.swift 是我们的 Swift 源码文件。

  • ModName.swiftmodule Swift 的模块文件,有了它,才能 import
  • ModName.swiftdoc 保存了从源码获得的文档注释
    • 文档注释以 /// 开头
  • libswiftModName.dylib 动态链接库
  • libswiftModName.a 静态链接库

TODO: 目前有个疑问就是 .swiftmodule 和链接库到底什么时候用哪个,以及具体作用。

.swift 源码文件

先明确一个概念,一个 .swift 文件执行是从它的第一条非声明语句(表达式、控制结构)开始的,同时包括声明中的赋值部分(对应为 mov 指令或者 lea 指令),所有这些语句,构成了该 .swift 文件的 top_level_code() 函数。

而所有的声明,包括结构体、类、枚举及其方法,都不属于 top_level_code() 代码部分,其中的代码逻辑,包含在其他区域,top_level_code() 可以直接调用他们。

程序的入口是隐含的一个 main(argc, argv) 函数,该函数执行逻辑是设置全局变量 C_ARGC C_ARGV,然后调用 top_level_code()

不是所有的 .swift 文件都可以作为模块,目前看,任何包含表达式语句和控制控制的 .swift 文件都不可以作为模块。正常情况下模块可以包含全局变量(var)、全局常量(let)、结构体(struct)、类(class)、枚举(enum)、协议(protocol)、扩展(extension)、函数(func)、以及全局属性(var { get set })。这里的全局,指的是定义在 top level 。

这里说的表达式指 expression ,语句指 statement ,声明指 declaration 。可能和有些人对相关概念的定义不同。实际上我特无奈有些人纠结于概念问题,而不是问题本身,本来翻译过来的舶来品就有可能有误差,当你明白那指的是什么的时候,就可以了。

模块编译方法

这里先以命令行操作为例,

1
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) ModName.swift -emit-library -emit-module -module-name ModName -v -o libswiftModName.dylib -module-link-name swiftModName

执行后获得 ModName.swiftdocModName.swiftmodulelibswiftModName.dylib.

这三个文件就可以表示一个可 import 的 Swift 模块。目前看起来 dylib 是必须得有的,否则链接过程报错。实际感觉 .swiftmodule 文件所包含的信息还需要继续挖掘挖掘。

多个源码文件直接依次传递所有文件名即可。

静态链接库 .a 目前还没有找到方法, -Xlinker -static 会报错。

命令行参数解释

相关命令行参数:

  • -module-name <value> Name of the module to build 模块名
  • -emit-library 编译为链接库文件
  • -emit-module-path <path> Emit an importable module to 编译模块到路径(全路径,包含文件名)
  • -emit-module Emit an importable module
  • -module-link-name <value> Library to link against when using this module 该模块的链接库名,就是 libswiftModName.dylib,这个信息会直接写入到 .swiftmodule

使用模块

使用模块就很简单了,记住两个参数:

-I 表示 import search path ,前面介绍过,保证 .swiftmodule 文件可以在 import search path 找到(这点很类似 module.map 文件,找得到这个就可以 import 可以编译)

-L 表示 链接库搜索路径,保证 .dylib 文件可以在其中找到,如果已经在系统链接库目录中,就不需要这个参数。

例如:

1
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) mymodtest.swift -I. -L.

此时表示所有 module 文件都在当前目录。

这两个选项都可以在 Xcode 中指定,所以如果你有小伙伴编译好的 module 想在你的项目里用是完全 ok 的。

For Xcode

很不幸,没能在 Xcode 中找到编译模块的相关方法。等我发现如何搞定的时候我会补上这个坑。

不过在任何含 Swift 项目的编译过程中, .swiftmodule 文件总是伴随着 .o 文件传递。

第三部分 瞎分析 .swiftmodule 文件

简单分析下一个 .swiftmodule 所包含的信息。

Foundation

这里先以标准库的 Foundation.swiftmodule 下手。

用 hexdump 查看发现它包含所有导出符号,以及 mangled name 。还有个文件列表,表示它是从哪些文件获得的(可以是 .swift 也可以是 .swiftmodule )。

用 strings 列出内容,发现 Foundation 库有如下特征:

1
2
3
4
5
6
7
8
9
10
11
...
Foundation
LLVM 3.5svn
/SourceCache/compiler_KLONDIKE/compiler_KLONDIKE-600.0.34.4.8/src/tools/swift/stdlib/objc/Foundation/Foundation.swift
/SourceCache/compiler_KLONDIKE/compiler_KLONDIKE-600.0.34.4.8/src/tools/swift/stdlib/objc/Foundation/KVO.swift
/SourceCache/compiler_KLONDIKE/compiler_KLONDIKE-600.0.34.4.8/src/tools/swift/stdlib/objc/Foundation/NSStringAPI.swift
CoreFoundation
Foundation
Swift
swiftFoundation
...

可以大胆猜测对应下:

  • -module-name => Foundation
  • 编译环境 => LLVM 3.5svn
  • 源文件列表 => …
  • 依赖列表 => CoreFoundation, Foundation, Swift
  • -module-link-name => swiftFoundation

我由此猜测, Foundation 的确是只有少量 Swift 代码做桥接。然后通过 Clang 模块将剩下工作交到底层。

分析其他类似模块也得到相同结果。

Swift 标准库

接下来有点好奇标准库 Swift 是怎么实现的。得到如下结果。

节选重要部分到 我的 Gist

里面有些很有意思的信息,有兴趣的同学可以去看看。

依赖模块 SwiftShims 是一个 module.map 定义的模块,桥接的部分头文件。源文件有相关信息和注释。大致意思是用来实现几个底层接口对象,比如 NSRange 邓。

其中-module-link-nameswift_stdlib_core

结论

LLVM Module 作为 Apple 提出的特性,已经被 Swift 完全采用,直接在它基础上建立了自己的模块系统。我相信它会影响到我们处理第三方库的方式方法。相信不久就会有相关工具基于它来管理依赖关系,比如老的 cocoapods4 可以加入新特性。

用 Swift 写模块目前并没有很好的 IDE 支持,所以不是很方便。基于猜测验证,上面的方法可以实现在 Swift 里 import Swift 模块,方法和结果看起来完全和官方模块相同。

Swift 的标准库完全是上面两种模块的结合体,用 Swift 模块封装 Clang 模块。这就解决了文章一开始提出的问题:为什么标准库大部分看起来是自动生成代码,少部分又好像是人工写的接口代码。

参考文献

Comments