今天在项目中引入了一个第三方库(名为 TestModule),其中,它有一个名为 TestProtocol 的协议以及 4 个协议方法,然而,当我在 TestViewController 声明后添加了这个协议名并实现了这 4 个协议方法后,编译器却告诉我,TestViewController 并没有实现 TestProtocol。经过重启 Xcode、重写协议方法,问题依旧。偶然我按住 command 点了一下库名,翻看了一下源码,发现 protocol 其中的一个方法的参数类型为 TestModule.TestClass 而不是我以为的 TestClass, 仔细一想,原来是我自己定义的一个结构体类型和它重名了。。。

当然,从错误提示上来说,这个锅 Swift 编译器得背一半😂,然而,对 Swift 模块化(Module)的不熟悉才是浪费了这么多时间的根本原因,因此有必要学习一下 Swift 的模块系统。

C/C++ 里,如果要引用另一个源文件里的代码,一般是 include 一个头文件,编译器在预处理的时候会将这个头文件的所有内容导入到这个文件当中。因此,即使你只是用 C++ 写一个打印 Hello,World 的小程序,在预处理后你的源文件生成的中间代码也会有10多万行。这也就导致了如果你在一个文件中导入了大量的头文件,编译时间可能会大大延长。

因此,LLVM 在很早的时候就引入了 Module 这个概念用来解决这个问题。一个 Module 主要包括了两个部分:

  • Module 暴露出来的接口
  • Module 的具体实现

当模块被编译后,会生成一个名为 module.map 的文件,这个文件是对一个模块的所有头文件的结构化描述,它使用一种叫 Module Map Language 的语言来描述,一个简单的源文件如下:

module MyLib {
  explicit module A {
    header "A.h"
    export *
  }

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

它描述了一个名为 MyLib 的模块,同时这个模块对外又显式的暴露了 MyLib.AMyLib.B 这两个子模块,因此你可以使用

import MyLib

来导入 MyLib 模块的所有公开接口,同时,你也可以使用

import MyLib.A

只导入 MyLib.A 这个子模块的接口。

举个例子,相信大家对于 C 语言的 stdio.h 这个头文件并不陌生,它属于标准库的一部分,使用的时候一般直接

#include<stdio.h>

就可以使用了。在对标准库模块化以后,stdio.h 变成了 stdio 模块,是 std 这个模块的一个子模块,它的源文件大致如下:

// stdio.c
export std.stdio:
public:
typedef struct {
  ...
} FILE;
int printf(const char*, ...) {
// ... }
int fprintf(FILE *,
// ... }
const char*, ...) {
int remove(const char*) {
// ... }

在这个文件的开头,使用 export 表明了这个文件对外暴露一个叫 std.stdio 的模块,用 public 关键字表明了这个模块对外暴露了哪些接口可供使用。同时很重要的一点是,这个文件只有 .c 源文件,没有头文件!

std 库被编译后,生成的 module.map 部分如下:

// /usr/include/module.map
module std {
  module stdio { header “stdio.h” }
  module stdlib { header “stdlib.h” }
  module math { header “math.h” }
}

对于 Swift 来说,由于 Swift 没有头文件,并且有严格的访问控制权限,因此 Swift 对于模块系统支持的非常好。对于一个名为 TestModName.swift 的文件来说,编译后它会生成三个文件:

  • TestModName.swiftmodule
  • TestModName.swiftdoc
  • TestModName.dylib

其中,TestModName.swiftmodule 文件包括一个叫 TestModName.modulemap 的文件,用于描述这个模块的接口。这是它区别于 C 模块的一个方面,对于 C 模块来说,module.map 是为了兼容以前的旧标准。

然而,不是所有的 .swift 文件都可以编译成一个模块。如果一个 .swift 文件有表达式语句和控制结构,那么这个 .swift 文件就不能编译为一个模块,也就是说,一个 .swift 文件只能包含

  • 全局变量(var)
  • 全局常量(let)
  • 结构体(struct)
  • 类(class)
  • 枚举(enum)
  • 协议(protocol)
  • 扩展(extension)
  • 函数(func)
  • 全局属性(var { get set })

回到最初的问题,查看这个库编译出来的的 modulemap 文件:

framework module TestModule {
  umbrella header "TestModule-umbrella.h"

  export *
  module * { export * }
}

可知,这个库将所有的公开声明直接暴露给使用者,因此,如果这个模块里有一个名为 TestClass 的类型,并且自己的项目中没有同名的类型,那么,你可以在导入这个模块之后直接使用 TestClass,然而,如果你已经定义了一个同名的类型,为了避免冲突,最好使用 TestModule.TestClass 的形式,因为如果不指定模块的话,Swift 会优先在这个文件所在的模块内寻找你使用的类型。写到这里,突然想到前段时间学习 macOS 开发时,由于 Swift 的调试器目前速度过慢,于是我选择在代码里打印相关的值来查看运行状况。然而,在 macOS 下,直接使用 print 方法,系统自动打开了打印机😂,原来 Cocoa for macOS 下已经声明了一个叫 print 的方法用来控制打印机。。。

这是我学习 Swift 模块系统的一篇总结,如有疏漏还望不吝赐教😅

参考