【译】Swift 与 C API的交互(Swift 3)
作为与 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 中,常量通常用来为属性和函数参数提供一组可选值。使用NS_STRING_ENUM和NS_EXTENSIBLE_STRING_ENUM宏标注的Ojbective-C的typedef声明,可被Swift以普通类型的成员的方式引入。
表示一组可用值的常量,可以通过添加NS_STRING_ENUM宏,来将其引入为枚举。例如,下面这段关于TraficLightColor的Objective-C字符串常量的声明:
1 | typedef NSString * TrafficLightColor NS_STRING_ENUM; |
下面展示了Swift如何引入它们:
1 | enum TrafficLightColor: String { |
对于呈现一组可扩展的可用常量值来说,可通过添加NS_EXTENSIBLE_STRING_ENUM宏,来使其以结构体的形式被引入。例如,下面这段关于StateOfMatter的Objective-C字符串常量声明:
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 multiper, int multiplicand); |
下面展示了Swift如何引入它们:
1 | func product(_ multiplier: Int32, _ multiplicand: Int32) -> Int32 |
可变参数函数(Variadic Functions)
在Swift中,可以通过getValist(_:)或者withValist(_:_:)来调用 C 中诸如vaspritf的可变参数函数. getValist(_:)函数接收一个包含CVarArg值的数组,并返回一个CVaListPointer,与直接返回不同withValist(_:_:)则通过接受一个闭包来实现。返回值CVaListPointer之后会被传递给 C 可变参数函数的va_lsit参数。
例如,下面的代码展示了如何在Swift中调用vasprintf函数:
1 | func Swiftprintf(format: String, argument: CVarArg...) -> String? { |
注意
可选类型指针不能传入withVaList(_:invoke:)函数,
可以通过Int.init(bitPattern:)构造函数,来将可选类型指针转为Int,
Which has the same C variadic calling conventions as pointer on all supported platforms(??? 尚待完善)。
结构体
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 { |
需要时,可通过._name_的方式来引用一个枚举值。
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仅部分支持 C 联合体类型,对于引入的 C 联合体,Swift无法存取不支持的域(fields)。但那些使用联合体作为参数或返回值的 C 和Objective-C API,可以被Swift正确调用。
位域(Bit Fields)
Swift可以把结构体中的位域,诸如Foundation中的NSDecimal类型,引入为计算型存储属性。在对其进行读取时,Swift会自动将其值转换为Swift兼容类型。
匿名的结构体和联合体域(Union Fields)
C 中的Struct和union类型在定义时可以作为域而不命名,Swift是不支持匿名结构体的,所以这些域会被引入为以__Unnamed_fieldName格式命名的嵌套类型。
例如,这个名为Pie的 C 结构体,它包含一个匿名结构体curst域和匿名联合体filling域:
1 | struct Pie { |
它们会分别被Swift引入为,一个Pie.__Unamed_crust类型的curst属性和一个Pie.__Unamed_filling类型的filling属性。
指针
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中没有与 C 指针所指内容相应的类型,例如,一个不完全的结构体类型,那么这个指针会被引入为OpaquePointer。
常量指针
一个接受UnsafePointer<Type>类型参数的函数,同样可以接受下列类型参数:
- 一个UnsafePointer<Type>,UnsafeMutalbePointer<Type>,或AutoreleasingUnsafeMutablePointer<Type>类型的值,如有必要它会被转换为UnsafePointer<Type>类型。
- 如果Type是Int8或UInt8,则可接受一个String类型的值。该字符串会自动被转换为一个UTF8字符缓存,随之指向该缓存的指针被传入函数。
- 一个包含一个或多个变量、属性、Type类型下标引用的in-out表达式。表达式会以指向左起首位参数内存地址的指针形式被传入。
- [Type](一个含有Type类型元素的数组),会以指向数组首地址的指针形式传入。
传入函数的指针,仅保证在函数调用期间有效。不要尝试持有或在函数返回后访问该指针。
例如,这样一个函数:
1 | func takesAPointer(_ p: UnsafePointer<Float>!) { |
可以这样调用它:
1 | var x: Float = 0.0 |
一个接受UnsafePointer<Void>类型参数的函数,可以把任意Type类型操作数以UnsafePointer<Type>形式接受。
例如下面这个函数:
1 | func takesAVoidPointer(_ p: UnsafePointer<Void>!) { |
它可以这样被调用:
1 | var x: Float = 0.0, y: Int = 0 |
可变指针(Mutable Pointers)
一个接受UnsafeMutablePointer<Type>类型参数的函数,同样可以接受下列类型参数:
- 一个UnsafeMutablePointer<Type>类型的值
- 一个含有一个或多个变量、属性、Type类型下标引用的in-out表达式。表达式会以指向左起首位参数内存地址的指针形式被传入。
- inout[Type]的值,会以指向数组首地址的指针形式传入,与此同时其生命周期会被延长,持续于整个函数调用期间。
例如下面这个函数:
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 |
数据类型空间计算
在 C 语言中,通过sizeof操作符来获取各种变量或数据类型的空间大小,而Swift则分别通过sizeof(_:)和sizeofValue(_:)来获取指特定类型或值所占的内存空间大小。然而与 C 语言中的sizeof不同,Swift中的sizeof(_:)和sizeofValue(_:)函数未将因内存对齐而增加的额外空间计算在内。例如,在Darwin中以 C 方式来获取timeval结构体的空间是16字节,而通过Swift获取则是12字节。
(译者注:在最新的 Xcode 8 beta 6 中,这里提到的获取size的函数均被新的Enum MemoryLayout取代。由于官方英文文档尚未更新,所以译文暂不改动)
1 | printf(sizeof(timeval.self)) |
类似功能由 strideof(_:) 和 strideofValue(_:) 函数代替,这两个函数返回的内存空间大小与 C 的sizeof返回相同。
1 | printf(strideof(timeval.self)) |
例如,setsockopt(_:_:_:_:_:)函数,可以通过接受一个timeval指针和指针所指值的大小来设置sokect接收超时时间,这就需要用strideof()来计算值的长度:
1 | let sokfd = socket(AF_INET, SOCK_STREAM, 0) |
注意
仅有哪些符合 C 函数调用规则的Swift函数类型可以作为函数指针参数。
与 C 函数指针相同,带有@convertion(c)属性的Swift函数类型不会捕获其周边的代码环境。
更多内容,请至《Swift编程语言》(Swift 3)中类型属性一章。
单次初始化
在 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代码中是无效。
条件编译Block
Swift和Objective-C通过不同的方式实现了条件编译。Swift通过_条件编译block_来实现。例如,
1 | #if DEBUG_LOGGING |
编译条件判断中可以包含true和false字面值,自定义条件判断flag(通过 -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 来添加条件判断分支,此外在一个选择编译block中还能嵌套另一个选择编译block。
1 | #if arch(arm) || arch(arm64) |
与 C 语言的预编译不同,Swift的条件编译block必须完整且语法正确,这是因为Swift代码即使尚未被编译,也会进行语法检查。
特例,如果条件编译block包含swift()判断,那么这个表达式仅在Swift版本与判断条件相匹配的时候才会去解析该表达式。这是为了确保旧版编译器不会去尝试解析较新版本的Swift语法。
最后
本文译自《Using Swift with Cocoa and Objective-C》书中 Interacting with C APIs 一章。
受限于译者英语水平及翻译经验,译文难免有词不达意,甚至错误的地方,还望不吝赐教,予以指正