【译】Swift与 C API的交互(Swift 4 beta)
作为与 Objective-C 交互的一部分,Swift 对 C 语言的类型和特性也提供了良好的兼容。Swift还提供了相应的交互方式,以便在需要时可以在代码中使用常见的 C 结构模式。
基本类型
虽然 Swift 提供了与 C 语言中 char,int,float 和 double 等基本类型等价的类型。但这些类型,如 Int,不能与 Swift 核心类型进行隐式转换。因此除非代码中有明确要求(使用等价的 C 类型),否则都应使用 Int(等 Swift 核心类型)。
C | Swift |
---|---|
bool | CBool |
char, signed char | CChar |
unsigned char | CUnsignedChar |
short | CShort |
unsigned short | CUnsignedShort |
int | CInt |
unsigned int | CUnsignedInt |
long | CLong |
unsigned long | CUnsignedLong |
long long | CLongLong |
unsigned long long | UnsignedLongLong |
wchar_t | CWideChar |
char16_t | CChart16 |
char32_t | CChart32 |
float | CFloat |
double | CDouble |
全局常量
定义在 C 和 Objective-C 源文件中的全局常量,会自动被 Swift 编译器引入为全局常量。
导入的枚举和结构体
在 Objective-C 中,常量通常用来为属性和函数参数提供一组可选值。Ojbective-C 中那些被 NS_STRING_ENUM 或 NS_EXTENSIBLE_STRING_ENUM 宏标注的 typedef 声明,会被 Swift 以普通类型的成员的方式导入。通过 NS_STRING_ENUM 声明的枚举不可以在添加额外的值进行扩展,而通过 NS_EXTENSIBLE_STRING_ENUM 声明的枚举,则可以在 Swift 的扩展(extension)中进行扩展。
通过 NS_STRING_ENUM 宏声明的一组常量,会被导入为结构体。例如,下面这段在 Objective-C 中 被声明为字符串常量的 TraficLightColor 类型:
1 | typedef NSString * TrafficLightColor NS_STRING_ENUM; |
下面是 Swift 导入后结果:
1 | struct TrafficLightColor: RawRepresentable {//注意区别,Swift 3 中是导入为枚举的 |
通过 NS_EXTENSIBLE_STRING_ENUM 宏声明的一组可扩展的常量值,同样会被导入为结构体。例如,下面这段在 Objective-C 中被声明为字符串常量的 StateOfMatter 类型:
1 | typedef NSString * StateOfMatter NS_EXTENSIBLE_STRING_ENUM; |
下面是 Swift 导入后结果:
1 | struct StateOfMatter: RawRepresentable { |
以可扩展形式声明的常量,在被导入后,会额外添加一个可忽略参数标签的构造器,以便扩展添加新值。
通过 NS_EXTENSIBLE_STRING_ENUM 宏声明的常量,导入后,可在 Swift 代码中扩展添加新值。
1 | extension StateOfMatter { |
###函数
Swift可以把任何声明在 C 头文件中的函数作为全局函数导入。例如,下面的 C 函数声明:
1 | int product(int multiplier, int multiplicand); |
下面是 Swift 导入后结果:
1 | func product(_ multiplier: Int32, _ multiplicand: Int32) -> Int32 |
变参函数(Variadic Functions)
在 Swift 中,可以通过 getValist(\_:)
或者 withValist(\_:\_:)
来调用 C 中诸如 vaspritf 这样的变参函数。 getValist(\_:)
函数接收一个包含 CVarArg 值的数组,并返回一个 CVaListPointer,与直接返回不同的是 withValist(\_:\_:)
通过接受一个闭包来实现。其结果 CVaListPointer 最后会作为 va_lsit 参数传递给 C 变参函数。
例如,下面的代码展示了如何在 Swift 中调用 vasprintf 函数:
1 | func Swiftprintf(format: String, argument: CVarArg...) -> String? { |
注意
可选类型指针不能传入 `withVaList(_:invoke:)` 函数,
可以通过 `Int.init(bitPattern:)` 构造函数,来将可选类型指针转为 Int,
在所有支持的平台上该指针的调用规则与 C 相同。
结构体
Swift 可以把任何头文件中声明的 C 结构体引入为 Swift 结构体,引入后的结构体会为每一个原 C 结构体成员生成一个存储型属性和一个逐一成员构造器。如果被引入的成员均有默认值,那么 Swift 同时会生成一个无参数的默认构造器。例如下面这个 C 结构体:
1 | struct Color { |
下面是与之相应的 Swift 结构体:
1 | public struct Color { |
将函数引入为类型成员
CoreFoundation 框架中的 C API,大都提供了用于创建、存取、以及修改 C 结构体的函数。可以通过在代码添加CF_SWIFT_NAME 宏标记,来让 Swift 将这些 C 函数引入为结构体的成员函数。例如,下面这段C函数声明:
1 | Color ColorCreateWithCMYK(float c, float m, float y, float k) CF_SWFIT_NAME(Color.init(c:m:y:k:)); |
下面展示了 Swift 如何将它们以类型成员的方式导入:
1 | extension color { |
传入 CF_SWIFT_NAME 宏的参数语法与 #selector
表达式相同。CF_SWIFT_NAME 宏中的 self,表示接收该方法的实例对象。
注意
使用 CF_SWIFT_NAME 宏时不能改变被引入成员函数的参数顺序和数量。
如果想更Swift点,可以重写一个Swift函数,然后在其内部再调用所需的 C 函数
(译者:用Swift再做一层封装)
枚举
Swift 可以把任何使用 NS_ENUM 宏标记的 C 枚举引入为 Int 类型的 Swift 枚举。无论是系统框架还是其它代码,引入后的枚举都会自动移除原命名前缀,
例如,下面这个通过 NS_ENUM 宏声明的 C 枚举:
1 | typedef NS_ENUM(NSInteger, UITableViewCellStyle){ |
在 Swift 导入后,会呈现为以下形式:
1 | enum UITableViewCellStyle: Int { |
通过点语法(译者注:.valueName)来引用一个枚举值。
1 | let cellStyle: UITableViewCellStyle = .default |
注意:
Swift 引入的 C 枚举,在构造时即使入参与声明不一致,也不会导致构造失败。
这么处理是为了与 C 兼容,因为 C 枚举允许任意类型的值,即使这个值没有暴露在头文件中,
而仅仅是供内部使用。
那些使用 NS_ENUM 或 NS_OPTIONS 宏声明的 C 枚举会被导入为 Swift 结构体。C 枚举中的每个成员都会被导入为一个与结构体类型相同的只读的全局计算型属性,而非结构体成员属性。
例如,下面这个未使用 NS_ENUM 宏声明的 C 结构体
1 | typedef enum { |
在 Swift 中,它会被导入为以下形式:
1 | struct MessageDisposition: RawRePresentable, Equatable{} |
Swift 会自动为导入的 C 枚举类型适配 Equaltable 协议。
选项型枚举(Option sets)
Swift 同样可以把 NS_OPTIONS 宏标注的 C 选项型枚举导入为 Swift 的选项(Option set)。与先前枚举的引入类似,选项的命名前缀也会被移除。
例如,下面这个在 Objective-C 声明的选项:
1 | typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing){ |
在 Swift 中会被导入为以下形式:
1 | public struct UIViewAutoresizing : OptionSet { |
在 Objective-C 中,选项型枚举实际上是整型位掩码。既可以通过位或运算符(|)来组合选项值,又可以通过位与运算符(&)检查选项值。我们还可以通过常量或表达式来创建选项,空选项用常量零(0)表示。
在 Swift 中,选项是以一个遵从 OptionSet 协议的结构体来实现的,每个选项都是该结构体的一个静态变量。与枚举类似,我们既可以通过字面量数组来创建一个选项,又可以通过点(.)语法获取选项的一个值。一个空的选项既可以通过字面量空数组([])创建,也可以通过调用其默认构造函数创建。
注意
当引入 NS_OPTIONS 宏标记的 C 枚举时,任何值为 0 的选项成员都会被 Swift 标记为无效,
因为 Swift 中使用空选项来表示没有选项。
选项与 Swift 中的 Set 集合类型相似,可以通过 insert(\_:)
或 formUnion(\_:)
函数添加选项值,也可以通过 remove(\_:)
或 substract(\_:)
函数删除一个选项值,还可以通过 contains(\_:)
来查验选项值。
1 | let options: Data.Base64EncodingOptions = [ |
联合体(Unions)(译者注:该部分为新增加内容个,Swift 3 是不支持的)
Swift 会把 C 联合体导入为 Swift 的结构体。虽然 Swift 不支持联合体,但是被导入后其使用仍与 C 联合体非常相似。例如,下面这个拥有 isAlive 和 isDead 两个域的联合体 SchroedingersCat:
1 | union SchroedingersCat { |
在 Swift 中会将其导入为一下形式:
1 | sturct SchroedingsCat { |
由于 C 中的联合体所有域共享一段内存,所以被 Swift 导入后生成的所有计算型属性也具有相同的内存结构。因此,当改变任何一个该结构体实例的计算型属性时,那么该实例的其它属性也会发生相应改变。
例如下面这个例子,当改变 SchroedingersCat 实例上的 isAlive 计算型属性时,其另一个计算型属性 isDead 也发生来相应的变化:
1 | var mittens = SchroedingersCat(isAlive: fasle) |
位域(Bit Fields)
Swift 可以把结构体中的位域,例如 Foundation 中的 NSDecimal 类型,导入为计算型属性。在对其进行访问时,Swift 会自动将其值转换为与 Swift 相兼容的类型。
匿名的结构体和联合体域(Union Fields)
(译者注:这里变化很大,且受限译者理解水平,翻译的可能不够准确)
C 中的 Struct 和 union 类型可定义成匿名或者匿名类型,匿名域由具名的相邻结构题或联合体类型组成。
例如,这个名为 Cake 的 C 结构体,它包含一个拥有 layers 和 height 两个相邻域的匿名联合体,以及一个域为 toppings的匿名结构题类型:
1 | struct Cake { |
在 Cake 被导入后,可以这样初始化并调用它:
1 | var simpleCake = Cake() |
Cake 在被导入后,会生成一个逐一成员构造器,可为其域传自定义值来构造该结构体,例如:
1 | let cake = Cake( .init(layers: 2), toppings: .init(icing: true, sprinkles: false)) |
由于 Cake 结构体的第一个域是匿名的,所以它的构造器的第一个参数也没有标签。由于 Cake 结构体的域含有匿名类型,所以使用 .init 构造器,通过类型推断的方式来为结构体的每个匿名域设置初始值。
指针
Swift 一直在尽力避免直接访问指针。但仍提供了丰富的指针类型以备不时之需。下表使用 Type 来指代不同语言的相应类型。
变量、参数、返回值的指针对照关系如下:
C Syntax | Swift Syntax |
---|---|
const Type * | UnsafePointer<Type> |
Type * | UnsafeMutablePointer<Type> |
类类型指针对照关系如下:
C Syntax | Swift Syntax |
---|---|
Type * const * | UnsafePointer<Type> |
Type * __strong * | UnsafeMutablePointer<Type> |
Type ** | AutoreleasingUnsafeMutablePointer<Type> |
无类型指针与原始内存(指针)的对照关系表:(译者注:swift 3中是不存在的)
C Syntax | Swift Syntax |
---|---|
const void * | UnsafeRawPointer<Type> |
void * | UnsafeMutableRawPointer<Type> |
Swift 还提供来用于操作缓存的指针类型,请参见 缓存指针(Buffer Pointers)。
如果 Swift 中没有与 C 指针所指内容相应的类型,例如,一个不完全的结构体(incomplete struct)类型,那么这个指针会被导入为 OpaquePointer。
常量指针
一个接受 UnsafePointer<Type> 类型参数的函数,同样可以接受下列类型参数:
- 一个 UnsafePointer<Type>,UnsafeMutalbePointer<Type>,或 AutoreleasingUnsafeMutablePointer<Type> 类型的值,如有必要它会被转换为 UnsafePointer<Type> 类型。
- 如果 Type 是
Int8
或UInt8
,则可接受一个 String 类型的值。该字符串会自动被转换为一个 UTF8 字符缓存(buffer),然后将指向该缓存(buffer)的指针传入该函数。 - 一个包含一个或多个变量、属性、Type 类型下标引用的 in-out 表达式。表达式会以指向左起首位参数内存地址的指针形式被传入。
- [Type](一个含有Type类型元素的数组),会以指向数组首地址的指针形式传入。
传入函数的指针,仅保证在函数调用期间有效。不要尝试持有或在函数返回后访问该指针。
例如,这样一个函数:
1 | func takesAPointer(_ p: UnsafePointer<Float>) { |
可以这样调用它:
1 | var x: Float = 0.0 |
一个接受 UnsafePointer<Void> 类型参数的函数,同样可以接受相同操作数的任意 Type 类型的 UnsafePointer<Type> 指针。
例如下面这个函数:(译者注:注意无类型指针的变动,由Void变成了Raw)
1 | func takesARawPointer(_ p: UnsafeRawPointer?) { |
它可以这样被调用:
1 | var x: Float = 0.0, y: Int = 0 |
可变指针(Mutable Pointers)
一个接受 UnsafeMutablePointer<Type>类型参数的函数,同样可以接受下列类型参数:
- 一个 UnsafeMutablePointer<Type> 类型的值
- 一个含有一个或多个变量、属性、Type类型下标引用的 in-out 表达式,以指向多个值地址的指针的形式被传入。
- 一个包含多个变量、属性、或下标引用的[Type]类型的 in-out表达式,会以指向该数组起始地址的指针形式传入,与此同时其生命周期会被延长,持续于整个函数调用期间。
例如下面这个函数:
1 | func takesAMutablePointer(_ p: UnsafeMutablePointer<Float>) { |
它可以这样被调用:
1 | var x: Float = 0.0 |
它可以这样被调用:
1 | var x: Float = 0.0, y: Int = 0 |
它可以这样被调用:
1 | var x: NSDate? = nil |
注意
如欲了解更多关于 Swift 是如何计算不同数据类型和值的空间大小的,请参考 数据类型空间计算(Data Type Size Calculation)。
数据类型空间计算(Data Type Size Calculation)(译者注:注意该部分内容变动很大)
在 C 语言中,可以通过 sizeof 和 alignof 操作符来获取任意变量或数据类型的内存占用大小及对齐情况。在 Swift 中可以通过访问 MemoryLayout
1 | print(MemoryLayout<timeval>.size) |
当在 Swift 中调用的 C 函数需要传入类型大小或值大小的时候,这些就派上用场了。例如,setsockopt(_:_:_:_:_:)函数,可以通过接受一个 timeval 指针和指针所指值的大小来设置sokect接收超时选项(SO_RCVTIMEO):
1 | let sokfd = socket(AF_INET, SOCK_STREAM, 0) |
欲了解更多信息,请参见 MemoryLayout
仅一次的初始化(One-Time Initialization)
在 C 语言中,POSIX 的 pthread_once()
函数和 Grand Central Dispatch 中的 dipatch_once()
、dispatch_once_f()
函数都有保证代码仅被初始化一次的机制。在 Swift 中,全局常量和存储型属性即使被多个线程同时交替存取,也能保证仅初始化一次。这是由语言自身特点来实现的, 因此 POSIX 和 Grand Central Dispatch 中相应的 C 函数未在 Swift 开放。
预处理命令
Swift 编译器没有预处理程序。相应地,它通过编译属性,条件编译 block,和语言特性来实现相同功能。因此,预处理命令不会被导入到 Swift 中。
简单宏命令
在 Swift 中可通过全局常量来代替,在 C 和 Objective-C 中由 #define 定义的常量。例如,#define FADE_ANNOTATION_DURATION 0.35,可以在 Swift 中被更好的表示为 let FADE_ANNOTATION_DURATION = 0.35。由于宏定义的常量可以被直接映射为 Swift 的全局变量,所以编译器会自动引入那些定在 C 和 Objective—C 源文件中的简单宏定义。
复杂宏命令
Swift 没有与 C 或 Objective-C 中的复杂宏命令相应的构功能特性,这里的复杂宏是指那些带有括号,与函数类似,却未用于定义常量的宏。C 和 Objective-C 中的复杂宏命令通常被用来规避类型检查限制,或者充当被大量使用的代码的模板。与此同时宏也让debuging和重构变困难。Swfit中可以通过函数和泛型来更好地达到这一目的。综上,C 和Objective-C中的复杂宏命令在Swift代码中是无效。
条件编译代码块(Conditional Compilation Blocks)
Swift 和 Objective-C 通过不同的方式实现了代码的条件编译。Swift 通过_条件编译代码块_来实现。例如,如果通过 swift -D DEBUG_LOGGING 设置 DEBUG_LOGGING 条件编译标识,那么编译器就会引入位于条件代码块中的代码。
1 | #if DEBUG_LOGGING |
编译条件判断中可以包含 true 和 false 字面值,自定义条件判断标识(通过 -D <#flag#>指定),和下表所列的平台判断标识。
Function | Valid arguments |
---|---|
os() | OSX, iOS, watchOS, tvOS, Linux |
arch() | x86_64, arm, arm64, i386 |
swift() | >=followed by version number |
注意
通过arch(arm)来判断ARM 64设备,不会返回true。当代码以32位iOS模拟器为目标编译时,arch(i386)会返回true。
通过逻辑与 && 和逻辑或 || 符号可以混合判断条件,通过逻辑否 !可以做假条件判断,还可以通过 #elseif 和 #else 来添加条件判断分支,此外在一个选择编译代码块儿中还能嵌套另一个选择编译代码块儿。
1 | #if arch(arm) || arch(arm64) |
与 C 语言的预编译不同,Swift 的条件编译代码块儿必须完整且语法正确,这是因为 Swift 代码即使尚未被编译,也会进行语法检查。
特例,如果条件编译代码块儿包含 swift() 判断,那么这个表达式仅在 Swift 版本与判断条件相匹配的时候才会去解析该表达式。这是为了确保旧版编译器不会去尝试解析较新版本的 Swift 语法。
最后
本文译自《Using Swift with Cocoa and Objective-C (Swift 4 beta)》书中 Interacting with C APIs 一章。相对于 Swift 3 新版本既对原有实现作出了改变,又有新增加了一些对 C 的支持,为了便于对比还是选择新开了一篇文章。
受限于译者英语水平及翻译经验,译文难免有词不达意,甚至错误的地方,还望不吝赐教,予以指正