猫·仁波切

会研发的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 模块。这就解决了文章一开始提出的问题:为什么标准库大部分看起来是自动生成代码,少部分又好像是人工写的接口代码。

参考文献

Swift and C Interop Cont. (简析 Swift 和 C 的交互,Part 二)

声明: 转载注明我或者 SwiftChina . weibo

本文的发现基于个人研究,目测是官方外的首创。请尊重原创。

本文是讲述 Swift 与 C 交互操作系列文章的第二部分。解决之前的所有遗留问题。

第一部分请参考 Swift and C Interop 简析Swift和C的交互

本文将介绍实际应用过程中会遇到的各类情况。

再看类型对应

标准类型这里就不提了,上面的文章讲的很明白了。

7 种指针类型

从代码看,我认为 Swift 对应 C 的指针时候,存在一个最原始的类型 RawPointer,但是它是内部表示,不可以直接使用。所以略过。但它是基础,可以认为它相当于 Word 类型(机器字长)。

以下内容再 Xcode6-beta3 中不适用 请参考 Swift 在 Xcode6-beta3 中的变化

COpaquePointer

不透明指针。之前我以为它很少会用到,不过现在看来想错了,虽然类型不安全,但是很多场合只能用它。它是直接对应 RawPointer 的。字长相等。

In computer programming, an opaque pointer is a special case of an opaque data type, a datatype declared to be a pointer to a record or data structure of some unspecified type. - 来自 Wikipedia

几乎没有任何操作方法,不带类型,主要用于 Bridging Header 中表示 C 中的复杂结构指针

比如一个例子, libcurl 中的 CURL * 的处理,其实就是对应为 COpaquePointer

UnsafePointer

泛型指针。直接对应 RawPointer。字长相等。

处理指针的主力类型。常量中的 C_ARGV 的类型也是它 UnsafePointer<CString>

支持大量操作方法:

  • 通过 .memory 属性 { get set } 操作指针指向的内容
  • 支持 subscript ,直接对应于 C 的数组,例如 C_ARGV[1]
  • 通过 alloc(num: Int) 分配数组空间
  • initialize(val: T) 直接初始化
  • offset 操作 .succ() .pred()
  • 可以从任意一种指针直接调用构造函数获得
  • 隐式类型转换为非 COpaquePointer 之外的任意一种指针

AutoreleasingUnsafePointer

之前特地写文介绍过这个指针类型。NSError 的处理就主要用它。传送门: Swift NSError Internals(解析 Swift 对 NSError 操作)

内部实现用了语言内置特性,从名字也可以看出来,这个应该是非常棒的一个指针,可以帮助管理内存,逼格也高。内存直接对应 RawPointer 可以传递给 C 函数。

  • 通过 .memory 属性 { get set } 操作指针指向的内容
  • 直接从 &T 类型获得,使用方法比较诡异,建议参考文章

CMutablePointer CConstPointer

分别对应于 C 中的 T *const T *。不可直接传递给 C 函数,因为表示结构里还有一个 owner 域,应该是用来自动管理生命周期的。sizeof 操作返回 16。但是可以有隐式类型转换。

操作方法主要是 func withUnsafePointer<U>(f: UnsafePointer<T> -> U) -> U,用 Trailing Closure 语法非常方便。

CMutableVoidPointer CConstVoidPointer

分别对应于 C 中的 void *const void *。其他内容同上一种。

小结指针

以上 7 种指针类型可以分未两类,我给他们起名为 第一类指针 和 第二类指针 。(你看我在黑马克思耶,算了这个梗太深,参考马克思主义政治经济学)

  • 可以直接用于 C 函数声明的 第一类指针
    • COpaquePointer UnsafePointer<T> AutoreleasingUnsafePointer<T>
    • 是对 RawPointer 的封装,直接对应于 C 的指针,它们的 sizeof 都是单位字长
  • 不可用于声明 第二类指针
    • CMutablePointer<T> CConstPointer<T> CMutableVoidPointer CConstVoidPointer
    • 直接从 Swift 对象的引用获得(一个隐藏特性,引用隐式转换)(主要构造方法)
    • 包含了一个 owner 字段,可以管理生命周期,理论上在 Swift 中使用
    • 通过 .withUnsafePointer 方法调用

所有指针都实现了 LogicValue 协议,可以直接 if a_pointer 判断是否为 NULL

nil 类型实现了到所有指针类型的隐式类型转换,等价于 C 中的 `NULL,可以直接判断。

什么时候用什么?这个问题我也在考虑中,以下是我的建议。

  • 对应复杂结构体,不操作结构体字段的: COpaquePointer 例如 CURL *
  • 日常操作: UnsafePointer<T>
  • 同时需要在 Swift 和 C 中操作结构体字段,由 Swift 管理内存:AutoreleasingUnsafePointer<T>
  • Swift 中创建对象,传递给 C: 第二类指针

工具类型

CVarArg CVaListPointer VaListBuilder

用于处理 C 语言中的可变参数 valist 函数。

1
2
3
protocol CVarArg {
  func encode() -> Word[]
}

表示该类型可以作为可变参数,相当多的类型都实现了这个。

1
2
3
4
5
struct CVaListPointer {
    var value: UnsafePointer<Void>
    init(fromUnsafePointer from: UnsafePointer<Void>)
    @conversion func __conversion() -> CMutableVoidPointer
}

对应于 C,直接给 C 函数传递,声明、定义时使用。

1
2
3
4
5
class VaListBuilder {
    init()
    func append(arg: CVarArg)
    func va_list() -> CVaListPointer
}

工具类,方便地创建 CVaListPointer

还有一些工具函数:

1
2
3
func getVaList(args: CVarArg[]) -> CVaListPointer
func withVaList<R>(args: CVarArg[], f: (CVaListPointer) -> R) -> R
func withVaList<R>(builder: VaListBuilder, f: (CVaListPointer) -> R) -> R

非常方便。

UnsafeArray

1
2
3
4
5
6
7
8
struct UnsafeArray<T> : Collection, Generator {
  var startIndex: Int { get }
  var endIndex: Int { get }
  subscript (i: Int) -> T { get }
  init(start: UnsafePointer<T>, length: Int)
  func next() -> T?
  func generate() -> UnsafeArray<T>
}

处理 C 数组的工具类型,可以直接 for-in 处理。当然,只读的,略可惜。

Unmanaged

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Unmanaged<T> {
  var _value: T
  init(_private: T)
  func fromOpaque(value: COpaquePointer) -> Unmanaged<T>
  func toOpaque() -> COpaquePointer
  static func passRetained(value: T) -> Unmanaged<T>
  static func passUnretained(value: T) -> Unmanaged<T>
  func takeUnretainedValue() -> T
  func takeRetainedValue() -> T
  func retain() -> Unmanaged<T>
  func release()
  func autorelease() -> Unmanaged<T>
}

顾名思义,手动管理 RC 的。避免 Swift 插入的 ARC 代码影响程序逻辑。

C 头文件的导入行为

宏定义

数字常量 CInt, CDouble (带类型后缀则为对应类型,如 1.0f ) 字符常量 CString 其他宏 展开后,无定义

枚举 enum

创建 enum 类型,并继承自 CUnsignedIntCInt (enum 是否有负初始值)

可以通过 .value 访问。

结构体 struct

创建 struct 类型,只有默认 init ,需要加上所有结构体字段名创建。

可变参数函数

转为 CVaListPointer。手动重声明更好。这里举 Darwin 模块的例子说。

1
func vprintf(_: CString, _: CVaListPointer) -> CInt

从 C 调用 Swift

只能调用函数。

之前说过,用 @asmname("name") 指定 mangled name 即可。

然后 C 语言中人工声明下函数。很可惜自动导出头文件不适用于 C 语言,只适用于 Objective-C 。

目测暂时无法导出结构体,因为 Swift 闭源不提供相关头文件。靠猜有风险。

全局变量不支持用 @asmname("name") 控制导出符号名。目测可以尝试用 mangled name 访问,但是很不方便。

示例

我尝试调用了下 libcurl 。

项目地址在 andelf/curl-swift 包含编译脚本(就一句命令)。

Bridging Header 只写入 #include<curl/curl.h> 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@asmname("curl_easy_setopt") func curl_easy_setopt(curl: COpaquePointer, option: CURLoption, param: CString) -> CURLcode
@asmname("curl_easy_setopt") func curl_easy_setopt(curl: COpaquePointer, option: CURLoption, param: CBool) -> CURLcode

let handle = curl_easy_init()

// this should be a const c string. curl_easy_perform() will use this.
let url: CString = "http://www.baidu.com"

curl_easy_setopt(handle, CURLOPT_URL, url)
curl_easy_setopt(handle, CURLOPT_VERBOSE, true)

let ret = curl_easy_perform(handle)
let error = curl_easy_strerror(ret)
println("error = \(error)")

值得注意的是其中对单个函数的多态声明, curl_easy_setopt 实际上第三个参数是 void *

以及对 url 的处理,实际上 libcurl 要求设置的 url 参数一直保持到 curl_easy_perform 时,所以这里用 withUnsafePointer 或者 withCString 是不太可取的方法。实际上或许可以用 Unmanaged<T> 来解决。

总结

我觉得说这么多。。。

调用 C 已经再没有别的内容可说了。其他的就是编程经验的问题,比如如何实现 C 回调 Swift 或者 Swift 回调 C 。可以参考其他语言的做法。解决方法不只一种。

参考文献

NSObject Pattern Match in Swift (简析 Swift 中的 Pattern Match)

本文正式标题是:简析 Swift 中的 Pattern Match

副标题是:妈蛋 Swift 你又用黑属性坑大家了: 为什么 switch case 可以用数字匹配字符

声明: 原创发现,原创内容。转载注明我或者 SwiftChina . weibo

问题的提出

故事是这样的,昨天有人在论坛上发了个帖子,虽然不太规范而且含糊,不过还是能看出来他在问什么。传送门

重新把问题简化搬运过来。

1
2
3
4
5
6
7
8
9
10
11
let count = "0"
switch count {
    case 1:
    println("location 1")
    case 0:
    println("location 2")
    case "1":
    println("location 3")
    default:
    println("location 4")
}

为什么上面的代码可以通过编译?

问题验证

这几天说了很多了, Swift 是强类型语言,所以类型不同肯定不能在一起。直觉上,这段代码肯定不可能编译通过,错误一定是类型错误。

果断扔代码到文件,然后编译(我个人习惯用命令行直接编译):

1
2
// 填入 test.swift
xcrun swift -g test.swift

果然出错,不过出错信息略诡异:

1
2
3
test.swift:4:6: error: could not find an overload for '~=' that accepts the supplied arguments
case 1:
     ^

故事到这里也许完事了。嗯,不可以编译,问题也许是 Xcode 6 beta 版的 BUG 什么的。微博上的 @肇鑫 也提到, 用 Playground 编译也是出错,出错信息同上。

看起来暂时是无法重现问题。

出错信息

先解释下这个出错信息,熟读那本 ibook 的同学,可能会记得,在 swift case 那节是有讲到 ~= 这个运算符的,也讲到了如何自定义它。

Expression Pattern

An expression pattern represents the value of an expression. Expression patterns appear only in switch statement case labels.

The expression represented by the expression pattern is compared with the value of an input expression using the Swift standard library ~= operator. The matches succeeds if the ~= operator returns true. By default, the ~= operator compares two values of the same type using the == operator. It can also match an integer value with a range of integers in an Range object.

所以其实 ~= 是一个匹配运算符(Pattern Match Operator),专门用在 Pattern Match 的地方,比如 switch 的 case 。一般情况下它直接调用 == 运算符。

原书还写道:

You can overload the ~= operator to provide custom expression matching behavior. For example, you can rewrite the above example to compare the point expression with a string representations of points.

说明 Pattern Match 的行为是可以通过重载 ~= 控制的。书中有重载的例子,小伙伴们可以看看。

深入下,来看看 ~= 到底是什么东西:

1
2
@transparent func ~=<T : Equatable>(a: T, b: T) -> Bool
func ~=<T : RandomAccessIndex where T.DistanceType : SignedInteger>(x: Range<T>, y: T) -> Bool

可以看到,首先它可以匹配两个相同类型的 Equatable,而且是 @transparent,说明没有额外开销, inline 函数,从书中看,完全等价于调用 ==,不深究了。

然后,它可以匹配一个 Rnage<T>T,这也就是我们在 switch case 里写的 case 1..18: println("未成年") 背后的奥秘,略有开销。

以上说明了为什么出错信息提示为什么是 ~= 运算符重载失败,而不是类型错误(其实也算是一种类型错误,没有找到适合运算符的类型)。

再次尝试重现

有了上面的线索,是不是怀疑我们遗漏了什么?回到原问题提出的地方:

1
2
3
4
override func viewDidLoad() {
super.viewDidLoad()
let count = "0"
....

看起来,他是在 App 环境下调用的,这也许就是问题的根源。之前有介绍过, Cocoa 框架给 Swift 语言添加了很多东西,包括一大批的 extension,还有一堆隐式类型转换。

我们在 test.swift 第一行加入 import Cocoa,重新编译。结果正常通过编译。代码执行结果符合预期,走 default 分支。

这也不科学!

小伙伴们惊呆了,看起来 count 变量明明是一个 Character,结果竟然能匹配 CharacterInt

祭出 lldb

lldb 挂上 exe ,看看汇编代码是怎么样的。带上注释。我摘录最能说明问题的一段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
   ; 这里是 case 1
   0x100001d1a:  movabsq $0x01, %rdi
   0x100001d24:  movq   %rax, -0x148(%rbp)

   ;  1 转换为 NSNumber
   0x100001d2b:  callq  0x100003f32               ; symbol stub for: Swift.Int.__conversion (Swift.Int)() -> ObjectiveC.NSNumber

   ; 此处省略调用 ARC 代码若干
   ....

   ; 这里其实是 count: String , 三个寄存器表示
   ; 检查 String 大小你会发现 sizeof(String)  24,三个 64bit
   ; 熟悉 Rust 的同学知道,字符串实现,无非三个 field  size  capacity pointer
   ; 猜测 Swift 完全相同因为它大量借鉴了 Rust (不服来喷)
   0x100001d43:  movq   -0x128(%rbp), %rdi
   0x100001d4a:  movq   -0x130(%rbp), %rsi
   0x100001d51:  movq   -0x138(%rbp), %rdx

   0x100001d58:  movq   %rax, -0x158(%rbp)
   ; 这里将 count: String 转换为 NSString
   0x100001d5f:  callq  0x100003f26               ; symbol stub for: Swift.String.__conversion (Swift.String)() -> ObjectiveC.NSString
   0x100001d64:  movq   -0x150(%rbp), %rdi
   0x100001d6b:  movq   %rax, %rsi
   ; **最最神奇的地方来了,这里调用了 ~= ObjectiveC 版,比较两个 NSObject 的版本。
   0x100001d6e:  callq  0x100003f20               ; symbol stub for: ObjectiveC.~= @infix (ObjectiveC.NSObject, ObjectiveC.NSObject) -> Swift.Bool

分析

上面一堆看不懂没关系,这里再复述下在 Cocoa 环境(或者说 Foundation 环境下)第一条 case 1: 的流程:

  • 将 1 转换为 NSNumber,通过隐式类型转换实现
  • count 转换为 NSString,通过隐式类型转换实现
  • 调用 NSObject 重载版的 ~= 运算符进行比较

这么看起来明了多了。但是,我们的 Character 哪里去了?为什么 count 的类型变了?

直接下断点然后 repl 命令检查,果然,我们没有指定类型的 count 在这个版本中变成了一个 String

从标准库定义看, CharacterString 都实现了 ExtendedGraphemeClusterLiteralConvertible 协议,保证他们能从字符字面常量获得。所以实际上每当我们写 let foo = "a" 其实是完全依赖编译器的类型推导,选择到底是 Character 还是 String

说明 Swift 编译器高大上,可以从当前加载的所有类型中推导出合法合理能编译的类型组合。来自 Haskell 背景的同学一定笑而不语,当然那种怎么改都凑不对类型的痛苦也是其他同学无法理解和感同身受的。

回头再看看 ObjectiveC.~= 这个运算符重载:

1
func ~=(x: NSObject, y: NSObject) -> Bool

定义在 ObjectiveC 模块里,使用 import ObjectiveC 可以加载。这个模块是 Swift 到 Objective-C 的一些映射模块,基础的 NSObject 方法还有一些运行时控制函数。

当然 Foundation, Cocoa 这些都会加载它,所以理论上写应用的时候不需要指定,只要你用到了 NSObject 就一定有它。

实际上 WWDC 某一集里讲到(忘记了,等我再找到会补上),对比 NSObject 的时候,Swift 会选择调用 isEqual 方法。我们反汇编验证下。简单加点注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(lldb) dis -a 0x100003f20
asmtest`symbol stub for: ObjectiveC.~= @infix (ObjectiveC.NSObject, ObjectiveC.NSObject) -> Swift.Bool:
   0x100003f20:  jmpq   *0x1252(%rip)             ; (void *)0x00000001000e88d0: ObjectiveC.~= @infix (ObjectiveC.NSObject, ObjectiveC.NSObject) -> Swift.Bool
(lldb) dis -a 0x00000001000e88d0
libswiftObjectiveC.dylib`ObjectiveC.~= @infix (ObjectiveC.NSObject, ObjectiveC.NSObject) -> Swift.Bool:
   0x1000e88d0:  pushq  %rbp
   0x1000e88d1:  movq   %rsp, %rbp
   0x1000e88d4:  pushq  %r15
   0x1000e88d6:  pushq  %r14
   0x1000e88d8:  pushq  %rbx
   0x1000e88d9:  pushq  %rax
   ; 64位 寄存器传参
   0x1000e88da:  movq   %rsi, %rbx
   0x1000e88dd:  movq   %rdi, %r14
   0x1000e88e0:  movq   0x3da1(%rip), %rsi        ; "isEqual:"
   0x1000e88e7:  movq   %rbx, %rdx
   0x1000e88ea:  callq  0x1000eb670               ; symbol stub for: objc_msgSend
   ; 测试 isEqual: 调用的返回值,存到 %r16b
   0x1000e88ef:  testb  %al, %al
   0x1000e88f1:  setne  %r15b
   0x1000e88f5:  movq   %rbx, %rdi
   0x1000e88f8:  callq  0x1000eb676               ; symbol stub for: objc_release
   0x1000e88fd:  movq   %r14, %rdi
   0x1000e8900:  callq  0x1000eb676               ; symbol stub for: objc_release
   ; 本运算符函数返回值
   0x1000e8905:  movb   %r15b, %al
   0x1000e8908:  addq   $0x8, %rsp
   0x1000e890c:  popq   %rbx
   0x1000e890d:  popq   %r14
   0x1000e890f:  popq   %r15
   0x1000e8911:  popq   %rbp
   0x1000e8912:  retq

啧啧, Instruction Pointer Relative Addressing 用的飞起。 64 位名字也特好听。 RIP 。 对了, MacOSX 下都是 PIC 位置无关代码。

从上面看,果然是调用了 Objective-C 的 isEqual:

总结

  • Swift 的编译器会根据大范围的代码推导类型,遍历 AST 然后用树或者图算法填充未指定的类型
    • "c" 可以是任意 ExtendedGraphemeClusterLiteralConvertible 类型,包括 CharacterStringCStringUnicodeScalarStaticString
  • swith case 语句使用 ~= 运算符执行匹配操作
    • 非 Foundation 环境下,相当于 == 运算符
    • 在 Foundation 环境下,如果是 NSObject 及其子类,相当于 isEqual: 方法
    • a..b 这样的匹配也是通过 ~= 运算符实现,也可以重载后自定义
  • 在 Foundation 环境下,隐式类型转换遍地都是

教训

  • 真用到字符类型 Character 的时候,还是显式指定吧
  • 需要额外注意能隐式类型转换到 NSObject 的几个类型,避免非预期行为

参考文献

Swift NSError Internals(解析 Swift 对 NSError 操作)

声明: 原创发现,原创内容。转载注明我或者 SwiftChina . weibo

其中基础部分借鉴了 WWDC 407 部分的介绍。

相关的分析主要基于我 dump 出的 Swift 标准库声明代码,位于 我的 Github andelf/Defines-Swift

引子

WWDC 407 中简单提到了 NSError 在 Swift 中的处理方法,说NSErrorPointer is Swift’s version of NSError **,其中大概内容是:

  • Foundation 的所有函数中的 NSError ** 都被对应为 Swift 中的 NSErrorPointer (自定义函数也应如此)
  • 调用时候使用下面的方式:
1
2
var error: NSError?
foobar(...., error: &error)
  • 这里一定要用 var,只有 var 才有引用的概念,才可以作为左值
  • 定义时(重载一些带 NSError 参数的函数)(注意其中的 .memory 部分。):
1
2
3
4
5
6
override func contentsForType(typeName: String!, error: NSErrorPointer) -> AnyObject! {
    if cannotProduceContentsForType(typeName) { if error {
        error.memory = NSError(domain: domain, code: code, userInfo: [:]) }
    return nil }
    // ...
}

到这里,基本怎么在 Swift 使用 NSError 已经足够了。但是本着探究妈蛋 Swift 到底藏起了多少黑魔法的伟(er)大(bi)精神,我继续分析下这里面的槽点。

这不科学!

我们知道 Swift 是强类型语言。类型不同怎么能在一起?直接出错才对。

当然也可能是之前提到过的“隐式类型转换”,所以这里探索下 NSErrorNSErrorPointer 的关系,到底是如何实现了 Objective-C 中的 NSError **

而且有一个逆天的地方是,NSErrorPointer 没有被标记为 inout,但传递参数时候需要传递 NSError 的引用。

上代码

这里给出 Swift 中 NSErrorNSErrorPointer 及相关类型的声明代码。(部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@objc(NSError) class NSError : NSObject, NSCopying, NSSecureCoding, NSCoding {
  @objc(initWithDomain:code:userInfo:) init(domain: String!, code: Int, userInfo dict: NSDictionary!)
  @objc(errorWithDomain:code:userInfo:) class func errorWithDomain(domain: String!, code: Int, userInfo dict: NSDictionary!) -> Self!
  @objc var domain: String! {
    @objc(domain) get {}
  }
  @objc(init) convenience init()
  ....
}

typealias NSErrorPointer = AutoreleasingUnsafePointer<NSError?>

struct AutoreleasingUnsafePointer<T> : Equatable, LogicValue {
  let value: RawPointer
  @transparent init(_ value: RawPointer)
  @transparent static func __writeback_conversion_get(x: T) -> RawPointer
  @transparent static func __writeback_conversion_set(x: RawPointer) -> T
  @transparent static func __writeback_conversion(inout autoreleasingTemp: RawPointer) -> AutoreleasingUnsafePointer<T>
  var _isNull: Bool {
    @transparent get {}
  }
  @transparent func getLogicValue() -> Bool
  var memory: T {
    @transparent get {}
    @transparent set {}
  }
}

分析

由以上代码可以知道,NSErrorPointer 实际上是 AutoreleasingUnsafePointer<Optional<NSError>> 类型(这里我顺便展开了 Type?), AutoreleasingUnsafePointer<T> 封装了一个 RawPointer,发挥想象力,差不多知道它就是个 void *,指向类型 T。在本例中,指向 NSError?

一般情况下 Swift 的 NSError! 被用于对应 Objective-C 的 NSError *, 一层指针,可 nil. 所以这里,两层指针算是对应上了(一层 RawPointer, 一层靠语言本身的特性实现,这里暂时不考虑 Type!Type? 的差异性,使用时候多注意就可以)。

然后就是棘手的问题,这里的类型转换是如何实现的?这里没有看到标准的隐式类型转换。但是有 __writeback_conversion* 一系列 static 函数。应该是他们起到了转换的作用(再次说明,Swift 是强类型语言,所以必然这样的特性有黑魔法)。

从名字和标准库的定义看,这三个函数应该是同时起作用,用于表述三个类型的关联关系。我写了个例子。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct Foobar {
    var val: Int

    @transparent static func __writeback_conversion_get(x: String) -> Int {
        println("location 1")
        if let v = x.toInt() {
            return v
        } else {
            return 0
        }

    }
    @transparent static func __writeback_conversion_set(x: Int) -> String {
        println("location 2")
        return toString(x + 10000)
    }
    @transparent static func __writeback_conversion(inout temp: Int) -> Foobar {
        println("location 3")
        return Foobar(val: temp)
    }

}

func test_foobar(v: Foobar) {
    dump(v, name: "dump from test_foobar")
}

var word = "9527"
test_foobar(&word)
dump(word, name: "word")

如上,代码输出是:

1
2
3
4
5
6
location 1
location 3
 dump from test_foobar: V5trans6Foobar (has 1 child)
  - val: 9527
location 2
- word: 19527

这三个函数在一次隐式调用中,全部都用到了,调用顺序如上。看起来隐藏在背后的关系明了了一些,流程大概是:

  • word 调用 Foobar.__writeback_conversion_get,获得一个 Int
  • Int 调用 Foobar.__writeback_conversion,构造了一个 Foobar 对象
  • Foobar 交给 test_foobar 函数内部处理
  • 函数逻辑结束后,调用 Foobar.__writeback_conversion_set,获得字符串,赋值给 word

我返回到 NSError 说。

回到 NSError 说

最简删节版代码:

1
2
3
4
5
6
7
8
9
// T = NSError? = Optional<NSError>
struct AutoreleasingUnsafePointer<T> {
  let value: RawPointer
  static func __writeback_conversion_get(x: T) -> RawPointer
  static func __writeback_conversion_set(x: RawPointer) -> T
  static func __writeback_conversion(inout autoreleasingTemp: RawPointer) -> AutoreleasingUnsafePointer<T>
  ...
  var memory: T { get set }
}

当一个函数接受 AutoreleasingUnsafePointer<T> 作为参数的时候,我们可以直接传递 T 的引用 &T,然后整个流程自动转换为

  • 用该引用参数调用 __writeback_conversion_get 获得一个 RawPointer
  • 调用 __writeback_conversion 获得一个 AutoreleasingUnsafePointer<T> 对象
  • 将这个对象交给函数内部处里,调用相关方法
  • 函数返回时,调用 __writeback_conversion_set,然后将结果赋值给一开始的引用参数,类型 &T 的那个。

如果是 NSError 的情况, 情况如下:

  • 某库函数接受 NSErrorPointer 作为参数,我们直接传递 NSError? 的引用 &error
  • error 作为参数调用 __writeback_conversion_get, 获得 RawPointer
  • 用这个 RawPointer 调用 __writeback_conversion,获得一个 NSErrorPointer 对象
  • 函数进行相关处理。对于 NSError 来说,这里一般是 Foundation 代码或者是 override 后的代码,在出错或者异常时访问 .memory,设置相应的错误
  • 函数返回。用之前的 RawPointer 调用 __writeback_conversion_set,获得 NSError? 然后赋值给引用 error
  • 接下来代码中可以对 error 进行判断,处理异常

其中 RawPointer 之前我们已经讨论过,是指针,在整个过程中值不变,指针指向的内存,就是 .memory 属性操作的部分,才是可变的部分,不属于 AutoreleasingUnsafePointer<T> 的成员访问控制范围,所以这就是为什么函数参数中的 NSErrorPointer 为什么没有标记为 inout 的原因。

总结

这三个 static 函数合起来组成了整体功能。我觉得可以把这个功能叫做“引用的隐式类型转换”或者"隐式引用类型转换"。

当然,它的强大之处在于用隐藏了二重指针的细节,同时这个特性也可以有其他作用。比如。。。(我没想好)

参考文献

Swift Runtime

Swift is written in C++ 、Objective-C、Swift、 Assembly.

TO BE CONTINUED

libdyld.dylib`start

1
2
3
4
5
main(argc, argv) {
  once() { C_ARGC = argc }
  once() { C_ARGV = argv }
  top_level_code()
}

All code in swift file goes to top_level_code

Swift and C Interop(简析Swift和C的交互)

声明: 转载注明我或者 SwiftChina . weibo

其中 @asmname 的两个用法源于我的猜测验证,用到了 Xcode, lldb, nm, llvm ir 等工具或格式。

其中 name mangling 部分源自 WWDC。

相关的分析主要基于我 dump 出的 Swift 标准库声明代码,位于 我的 Github andelf/Defines-Swift

目前有两个小部分还未完成。

之前好像简单说过 Swift 和 Objective-C 的交互问题。其实我们也可以用 Swift 调用纯 C 代码或者基于 C 的第三方库。(这里不会也永远不会考虑 C++ 的情况,因为不支持,不过可以用 C 写 wrapper, 这个没有任何问题。)

Swift 官方文档中,以及那本已经被迅速翻译为中文的 ibooks 书中,都提到了 Swift 调用 Objective-C 和 C 是有很好支持的。不过没有细节。

这里不会考虑如何用 C 调用 Swift, 暂时,看心情。:) 本内容包括 Swift 调用 C 和相应的 C 调用 Swift,项目混编。

这里主要面向 MacOSX 应用。iOS 或许可以适用。

先复习下区别。

第一部分 预备知识

语言区别

说到底就是 C 少了很多东西。但是多了个指针。

对于 C 来说,最头疼的莫过于指针,而 Swift 是一门没有指针的语言。是不是要吐血?相对来说指针不但代表者指针操作传参,还有指针运算等等。

第二部分 调用 C

这里主要讨论函数的调用。对于结构、枚举、类的兼容性暂时没尝试。

C 标准库

好消息是,对于标准库中的 C 函数,根本不需要考虑太多导入头文件神马的。比如 strlenputcharvprintf。当然 vprintf 需要多说几句,后面说。

请直接 import Darwin 模块。

这些标准库函数表示为 Darwin.C.HEADER.name

实际上由于 Swift 模块结构是平坦的,他们均位于 Darwin 中,所以基本上是直接用的。

然后 CoreFoundation 用到了 Darwin ( @exported 导入,所以相当于这些名字也在 CoreFoundation 中)。

然后 Foundation 用到了 CoreFoundation (也是 @exported 导入。)

所以其实你导入 Foundation 的时候,这些 C 函数都是直接可用的。比如 putchar 一类。

多说一句,Cocoa 当然也包含 Foundation

C 函数

好吧假设你有个牛逼到顶天的算法是 C 写的,不对,假设是别人写的,牛逼到你只能凑合用却看不懂然后自己也写不出没空迁移的地步。

我们直接创建一个 Swift 项目,然后 New File,添加一个 .c 文件。

这时候 Xcode 会弹出对话框询问是否配置 Bridge Header,确认就可以了。也可以手动添加 Bridge Header,位置在项目的 Build Settings 中的 Swift Compiler - Code Generation 子项里。指向你的 Bridge Header 文件名就可以了。

一般这个文件是 ProjectName-Bridging-Header.h。情况基本和与 Objective-C 混编没区别。

剩下的工作就很简单了。在 .c 文件填上传说中的牛逼算法。在 ProjectName-Bridging-Header.h 中加上该函数原型或者引入相关的头文件。

在 Swift 中调用的名字和 C 名字一样就可以了,比如你定义了一个 int mycsort() 那么在 Swift 中就是 func mycsort() -> CInt

这时候问题来了。一个漂亮的问题。

我的 C 函数名字和 Swift 标准库冲突怎么办?比如我定义了一个函数就叫 println,我们知道 Swift 里也有个 println

这样,如果直接调用会提示 Ambiguous use of 'println'。没辙了么?

这里有个我发现的 Undocumented Featuer 或者说是 Undocumented Attribute。你转载好歹提下我好吧。(发现方法是通过 Xcode 查看定义,然后通过 nm 命令发现符号, 对照 llvm ir 确认的。)

那就是 @asmname("func_name_in_c")。用于函数声明前。使用方法:

1
int println() { .... }
1
2
@asmname("println") func c_println() -> CInt // 声明,不需要 {} 函数体
c_println() // 调用

也就是 C 中的同名函数,我们可以给赋予一个别名,然后正常调用。这么一看就基本没有问题了。至于类型问题,待会说,详细说。

C Framework

很多时候我们拿到的是第三方库,格式大概是个 Framework。比如 SDL2.framework。举这个例子是因为我想对来说比较熟悉 SDL2。

直接用 Finder 找到这个 .framework 文件,拖动到当前项目的文件列表里,这样它将作为一个可以展开的文件夹样式存在于我们的项目中。

ProjectName-Bridging-Header.h 中引入其中需要的 .h

比如我们引入 SDL2.framework,那么我们就需要写上 #import <SDL2/SDL.h>

然后在 Swift 文件里正常调用就好了。

所以其实说到底核心就是那个 ProjectName-Bridging-Header.h,因为它是作为参数传递给 Swift 编译器的,所以 Swit 文件里可以从它找到定义的符号。

但是,这个桥接头文件的一切都是隐式的,类型自动对应,所以很多时候需要我们在 Swift 里调用并封装。或者使用 @asmname(...) 避免名字冲突。

第三部分 类型转换

前面说到了 C 中有指针,而 Swift 中没有,同时基本类型还有很多不同。所以混编难免需要在两种语言的不同类型之间进行转换。

牢记一个万能函数 reinterpretCast<T, U>(T) -> U,只要 T, U sizeof 运算相等就可以直接转换。这个在之前的标准库函数里有提到。调用 C 代码的利器!

基本类型对应

以下内容再 Xcode6-beta3 中不适用 请参考 Swift 在 Xcode6-beta3 中的变化。简言之,不在对应为 C 别名类型,而是直接对应到 Siwft 类型。而指针类型简化为 UnsafePointer<T>ConstUnsafePointer<T> 两种, COpaquePointer 依然存在。另新增了 CFunctionPointer<T>

  • int => CInt
  • char => CChar / CSignedChar
  • char* => CString
  • unsigned long = > CUnsignedLong
  • wchar_t => CWideChar
  • double => CDouble
  • T* => CMutablePointer
  • void* => CMutableVoidPointer
  • const T* => CConstPointer
  • const void* => CConstVoidPointer

继续这个列表,你肯定会想这么多数值类型,怎么搞。其实大都是被 typealias 定义到 UInt8Double 这些的。放心。C 中数值类型全部被明确地用别名定义到带 size 的 Swift 数值类型上。完全是一样用的。

其实真正的 Pointer 类型只是 UnsafePointer<T>,大小与 C 保证一致,而对于这里不同类型的 Pointer,其实都是 UnsafePointer 到它们的隐式类型转换。还有个指针相关类型是 COpaquePointer,不过没试验怎么用

UPDATE: 我们在调用的时候,更多地用到 COpaquePointer,我将再坑一篇介绍它。

同时 NilType,也就是 nil 有到这些指针的隐式类型转换。所以可以当做任何一种指针的 NULL 用。

还有个需要提到的类型是 CString, 他的内存 layout 等于 UnsafePointer<UInt8>,下面说。

CString

用于表示 char *\0 结尾的 c 字符串,实际上似乎还看到了判断是否 ASCII 的选项,但是没试出来用法。

实现了 StringLiteralConvertibleLogicValue。可以从字符串常量直接赋值获得 CStringLogicValue 也就是是 if a_c_str {},实际是用于判断是否为 NULL,可用,但是不稳定,老 crash。

运算符支持 ==,判断两字符串是否相当,猜测实际是 strcmp 实现,对比 NULL 会 crash。Orz。

CString 和 String 的转换通过一个 extension 实现,也是很方便。

1
2
3
4
5
6
7
8
9
extension String {
  static func fromCString(cs: CString) -> String
  static func fromCString(up: UnsafePointer<CChar>) -> String
}
// 还有两个方便的工具方法。 Rust 背景的同学一定仰天长啸。太相似了。
extension String {
  func withCString<Result>(f: (CString) -> Result) -> Result
  func withCString<Result>(f: (UnsafePointer<CChar>) -> Result) -> Result
}

在我们的 Bridging Header 头文件中 char * 的类型会对应为 UnsafePointer<CChar>,而实际上 CString 更适合。所以在 Swift 代码中,往往我们要再次申明下这个函数。或者用一个函数封装下,转换成我们需要的类型。

例如,假设在 Bridging Header 中我们声明了 char * foo();,那么,在 Swift 代码中我们可以用上面提到的方法:

1
2
3
@asmname("foo") func c_foo() -> CString
// 注意这里没有 {},只是声明
let ret = c_foo()

当然也可以直接调用原始函数然后类型转换:

1
2
let raw = foo() // UnsafePointer<Int8> <=> UnsafePointer<CChar>
let ret = String.fromCString(ret)

如果这里要转成 CString 就略复杂了,因为 CString 构造函数接受的参数是 UnsafePointer<UInt8>, 而 CCharInt8 的别名,所以还牵扯到 Genrics 类型转换,不够方便。

如果非要作为 CString 处理,可以用 reinterpretCast(),直接转换。但是请一定要知道自己在转换什么,确保类型的 sizeof 相同,确保转换本身有意义。

例如获得环境变量字符串:

1
2
3
let key = "PATH"
// 这里相当于把 UnsafePointer<CChar> 转为了 UnsafePointer<UInt8> 然后到 CString
let path_str: CString = reinterpretCast(key.withCString(getenv))

Unmanaged

这个先挖坑,随后填上。

VaList

这个也是坑,随后填上。

第三部分 C 调用 Swift

如果项目里加入了 C 文件,那么它可以调用我们的 Swift 函数么?答案是可以的,而且令人吃惊地透明。这也许是因为 Apple 所宣传的,Small Runtime 概念吧。极小的语言运行时。

和 Objective-C 混编类似,配置好 Bridging Header 的项目,在 .c .h .m 文件中都可以使用一个叫做 ProjectName-Swift.h 的头文件,其中包含 Swift 代码导出的函数等。

参考之前的 Objective-C 和 C 交互我们可以知道,说到底交互就是链接过程,只要链接的时候能找到符号就可以。

不过不能高兴太早,Swift 是带类、枚举、协议、多态、泛型等的高级语言,符号处理明显要比 C 中的复杂的多,现代语言一般靠 name mangle 来解决这个问题。也就是说一个 Swift 函数,在编译到 .o 的时候,名字就不是原来那么简单了。比如 __TFV5hello4Rectg9subscriptFOS_9DirectionSi 这样的名字。

Xcode 自带了个工具, 可以查看这些 mangled name 到底是什么东西:

1
2
xcrun swift-demangle __TFV5hello4Rectg9subscriptFOS_9DirectionSi
_TFV5hello4Rectg9subscriptFOS_9DirectionSi ---> hello.Rect.subscript.getter (hello.Direction) -> Swift.Int

当我们从 C 调用的时候,应该规避这样的名字。还记得前面的 @asmname 么?没错,它可以用于指定 Swift 函数的符号名,我猜测应该有指定 mangled name 的作用,但是不是特别确定。

这里随便指定个例子先。

1
2
3
4
@asmname("say_hello") func say_hello() -> Double {
    println("This is say_hello() in swift")
    return 3.14
}

然后在 .c 文件中:

1
2
3
4
5
6
7
8
#include <ProjectName-Swift.h>

extern double say_hello();

int some_func() {
  say_hello(); // or capture its value and process it
  return 0
}

当然这里的 extern 一行是可选的,因为实际上声明会存在于 ProjectName-Swift.h 中,只是为了避免首次编译警告而已(第二次以后的编译,其实这个 Header 已经被缓存起来了,这个牵扯到 Xcode 的编译过程)。 错了。

对于函数而言 extern 必须手动加上,对于 class 、 protocol ,会在生成的头文件里。

按照这个思路,其实很容易实现 Swift 调用 C 中调用了 Swift 函数的函数。这意味着,可以通过简单的方法封装支持向 C 传递 Swift block 作为回调函数。难度中上,对于有过类似扩展编写经验的人来说很简单。

第四部分 编译过程

其实调用基本就这么多, Objective-C 那篇文章中说的编译过程同样有效。我 C-c C-v 下:

  • 编译所有 X.swift 文件到 X.o (with -emit-objc-header, -import-objc-header) (其中包含 .swiftmodule 子过程)
    • 由于选项里有 -emit-objc-header,所以之后的 C 文件可以直接 import 对应的 ProjectName-Swift.h
  • 编译 X.cX.o
  • 链接所有 .o 生成可执行文件

仔细理解上面的简简单单四行编译过程说明,你就明白为什么 .swfit 和 .c 可以互相调用了。其中两个 Header 文件起到了媒介的作用,一个将 .c/.m 文件中的定义暴露给 Swift,另一个将 .swift 中的定义暴露给 .c/.m 。

Swift Misc

Just Notes. Keep Updating.

escaped variable name

1
2
let `let` = 1000
dump(`let`, name: "variable named let")

Optimise Parameter

  • -Onone // optimization off, safety checks on
  • -O // optimization on, safety checks on
  • -Ofast // optimization on, safety checks off

High Level Optimization

  • Removing abstraction penalties
  • Generic specialization
  • Devirtualization (Resolving dynamic method calls at compile-time)
    • If Swift can see where you constructed the object
    • If Swift knows that a class doesn’t have any subclasses
    • If you’ve marked a method with the @final attribute
  • ARC optimization
  • Enum analysis
  • Alias analysis
  • Value propagation
  • Library optimizations on strings, arrays, etc.

Closure Memoize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func memoize<T: Hashable, U>( body: (T)->U ) -> (T)->U {
  var memo = Dictionary<T, U>()
  return { x in
    if let q = memo[x] { return q }
    let r = body(x)
    memo[x] = r
    return r
} }

func memoize<T: Hashable, U>( body: ((T)->U, T)->U ) -> (T)->U {
  var memo = Dictionary<T, U>()
  var result: ((T)->U)!
  result = { x in
    if let q = memo[x] { return q }
    let r = body(x)
    memo[x] = r
    return r
}
  return result
}

lldb

Raw Display

(lldb) fr v -R age

Protocol

fr v val

fr v -d r val

misc

repl // repl loop

// find stop reason t i // thread info // how did this code get called, list frames bt // thread backtrace

一般主要查找 top_level_code 位置,找到出错未知

// what is the failure conditions f 1 // frame select 1

p foo // expression foo

b filename.swift:8 b func_name

br s -r timestwo.String // breakpoint set –func-regex timestwo.String

b * br m -c “valueInCents < Int(amountInCents)” // breakpoint modify –condition “…” br m -c “….” 3.1

b * br co a // breakpoint command add // enter commands, DONE to end

br l // breakpoint list

br dis 1 // breakpoint disable 1

Infos

The stop reason tells you what happened

  • EXC_BAD_INSTRUCTION Assertion
  • SIGABRT Exception (usually Objective-C)
  • EXC_BAD_ACCESS Memory error

The stack tells you how it happened The expression command helps you inspect variables

swift-demangle

1
2
3
xcrun swift-demangle __TIFSs4dumpU__FTQ_4nameGSqSS_6indentSi8maxDepthSi8maxItemsSi_Q_A1_
_TIFSs4dumpU__FTQ_4nameGSqSS_6indentSi8maxDepthSi8maxItemsSi_Q_A1_ --->
Swift.(dump <A>(A, name : Swift.String?, indent : Swift.Int, maxDepth : Swift.Int, maxItems : Swift.Int) -> A).(default argument 2)

dispatch queue

1
2
3
import Foundation
var queue : dispatch_queue_t = dispatch_queue_create("my_queue", nil)
dispatch_sync(queue) {println(world")}

Upcast vs Downcast

A matter of idom

Upcast: T to AnyObject

Downcast: AnyObject as T

Swift Array T[]

Two representations.

  • Native Array => |len|cap|0|1|2|..|
  • Cocoa Array => NSArray obj

Unmanaged

Used in CF.

1
2
3
4
5
6
7
8
struct Unmanaged<T: AnyObject> {
  func takeUnretainedValue() -> T   // for +0 returns
  func takeRetainedValue() -> T  // for +1 returns


  func retain() -> Unmanaged<T>
  func release()
  func autorelease() -> Unmanaged<T>
}

Weak Reference

Weak references are optional values

  • Use strong references from owners to the objects they own
  • Use weak references among objects with independent lifetimes
  • Use unowned references from owned objects with the same lifetime // 互相绑定的引用,生命周期相同

Initializer

  • Initialize all values before you use them
  • Set all stored properties first, then call super.init
  • Designated initializers only delegate up
  • Convenience initializers only delegate across

Capture List

避免循环引用

1
2
3
4
5
init() {
  onChange = {[unowned self] temp in
    self.currentTemp = temp
  }
}

QuickLookObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum QuickLookObject {
  case Text(String)
  case Int(Int64)
  case UInt(UInt64)
  case Float(Double)
  case Image(Any)
  case Sound(Any)
  case Color(Any)
  case BezierPath(Any)
  case AttributedString(Any)
  case Rectangle(Double, Double, Double, Double)
  case Point(Double, Double)
  case Size(Double, Double)
  case Logical(Bool)
  case Range(UInt64, UInt64)
  case View(Any)
  case Sprite(Any)
  case URL(String)
  case _Raw(UInt8[], String)
}

Add Quick Look support to NSObject subclasses only Implement the debugQuickLookObject() method

1
2
3
func debugQuickLookObject() -> AnyObject? {
    return "Some Quick Look type"
}

Swift Special Protocols

via WWDC 404

Protocols are your hooks into the Swift core language Swift generics combine abstraction, safety, and performance in new ways Read, experiment, and have fun. There’s plenty to discover!

LogicValue // 重载 if val

Printable // 重载 println

1
2
3
protocol Printable {
    var description: String { get }
}

Sequence // 重载 for-in

1
2
3
4
5
6
7
8
9
protocol Sequence {
  typealias GeneratorType : Generator
  func generate() -> GeneratorType
}

protocol Generator {
  typealias Element
  mutating func next() -> Element?
}
1
2
3
4
5
// for .. in { }
var __g = someSequence.generate()
while let x = __g.next() {
    ...
}}

*Convertible

  • IntegerLiteralConvertible
  • FloatLiteralConvertible
  • StringLiteralConvertible
  • ArrayLiteralConvertible
  • DictionaryLiteralConvertible

Equatable

==

Swift and ObjectiveC Interop Cont. (Swift 与 Objective-C 之间的交互,续坑)

关于 _ConditionallyBridgedToObjectiveC

When you bridge from a Swift array to an NSArray object, the elements in the Swift array must be AnyObject compatible. For example, a Swift array of type Int[] contains Int structure elements. The Int type is not an instance of a class, but because the Int type bridges to the NSNumber class, the Int type is AnyObject compatible. Therefore, you can bridge a Swift array of type Int[] to an NSArray object. If an element in a Swift array is not AnyObject compatible, a runtime error occurs when you bridge to an NSArray object.

其他 Tips

When you declare an outlet in Swift, the compiler automatically converts the type to a weak implicitly unwrapped optional and assigns it an initial value of nil. In effect, the compiler replaces @IBOutlet var name: Type with @IBOutlet weak var name: Type! = nil.

In Swift, there are no readwrite and readonly attributes. When declaring a stored property, use let to make it read-only, and use var to make it read/write. When declaring a computed property, provide a getter only to make it read-only and provide both a getter and setter to make it read/write.

Swift String Extension for Integer Index(扩展String使之可以支持数字Index和数字Range)

使用效果:

1
2
3
4
5
6
let foo: String = "Hello World 世界"
println("foo[13] \(foo[13])")
println("foo[-1] \(foo[-1])")
println("foo[-2] \(foo[-2])")
dump(foo[1..5], name: "foo[1:4]")
dump(foo[10...13], name: "foo[10:13]")

简单介绍下实现背后的思路和想法。

首先熟悉 String/Range 的一定知道这玩意 subscript 类型只能是 String.Index,也就是通过 String.startIndex 或者 .endIndex 获得。很不爽,实际使用场景里面大家直接把 String 当 Unicode 字符串搞。假定无风险好了。

sizeofValue 检查 aString.endIndex 发现大小是 32,猜测是 4x8 (64位)个整数或者指针,如果是我,索引值肯定放在这个结构的第一位,所以这就是 _Dummy 结构的由来(猜测+代码验证),就是为了和 String.Index 类型大小一致结构相似(反正我只关心第一个,其他的我用 _padding 补全)

然后用强制类型转换,然后操作这个值。

至于重载 subscript ,那啥。看书好了。

然后有人说字符串没有长度方法。。

countElements(aString) 标准库函数。可以用 extension 做到 String 里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extension String {
    struct _Dummy {
        var idxVal: Int
        var _padding: Int
        var _padding2: Int
        var _padding3: Int
    }
    subscript (i: Int) -> Character {
        var dummy: _Dummy = reinterpretCast(i >= 0 ? self.startIndex : self.endIndex)
        dummy.idxVal += i
        let idx: String.Index = reinterpretCast(dummy)
        return self[idx]
    }
    subscript (subRange: Range<Int>) -> String {
        var start: _Dummy = reinterpretCast(self.startIndex)
        var end = start
        start.idxVal = subRange._startIndex
        end.idxVal = subRange._endIndex
        let startIndex: String.Index = reinterpretCast(start)
        let endIndex: String.Index = reinterpretCast(end)
        return self[startIndex..endIndex]
    }
}

讨论内容在 SwiftChina. 其中有人给出了负数 range 的实现。