cover_image

简析Swift和C的交互

Cocoa开发者社区
2014年06月21日 00:05

转自·仁波切的博客微博

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

其中 name mangling 部分源自 WWDC

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

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

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

本内容包括 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. @asmname("println") func c_println() -> CInt // 声明,不需要 {} 函数体

  2. 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 代码的利器!

基本类型对应

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 的选项,但是没试出来用法。

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

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

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

  1. extension String {

  2. static func fromCString(cs: CString) -> String

  3. static func fromCString(up: UnsafePointer<CChar>) -> String

  4. }

  5. // 还有两个方便的工具方法。 Rust 背景的同学一定仰天长啸。太相似了。

  6. extension String {

  7. func withCString<Result>(f: (CString) -> Result) -> Result

  8. func withCString<Result>(f: (UnsafePointer<CChar>) -> Result) -> Result

  9. }

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

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

  1. @asmname("foo") func c_foo() -> CString

  2. // 注意这里没有 {},只是声明

  3. let ret = c_foo()

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

  1. let raw = foo() // UnsafePointer<Int8> <=> UnsafePointer<CChar>

  2. let ret = String.fromCString(ret)

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

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

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

  1. let key = "PATH"

  2. // 这里相当于把 UnsafePointer<CChar> 转为了 UnsafePointer<UInt8> 然后到 CString

  3. 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. xcrun swift-demangle __TFV5hello4Rectg9subscriptFOS_9DirectionSi

  2. _TFV5hello4Rectg9subscriptFOS_9DirectionSi ---> hello.Rect.subscript.getter (hello.Direction) -> Swift.Int

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

这里随便指定个例子先。

  1. @asmname("say_hello") func say_hello() -> Double {

  2. println("This is say_hello() in swift")

  3. return 3.14

  4. }

然后在 .c 文件中:

  1. #include <ProjectName-Swift.h>

  2. extern double say_hello();

  3. int some_func() {

  4. say_hello(); // or capture its value and process it

  5. return 0

  6. }

对于函数而言 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.c X.o

链接所有 .o 生成可执行文件

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

再看类型对应

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

7 种指针类型

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

COpaquePointer

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

“In computerprogramming, an opaque pointer is a special case of an opaque data type, adatatype declared to be a pointer to a record or data structure of someunspecified 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 类型获得,使用方法比较诡异,建议参考文章

CMutablePointerCConstPointer

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

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

CMutableVoidPointerCConstVoidPointer

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

小结指针

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

-可以直接用于 C 函数声明的第一类指针

COpaquePointerUnsafePointer<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 CVaListPointerVaListBuilder

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

  1. protocol CVarArg {

  2. func encode() -> Word[]

  3. }

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

  1. struct CVaListPointer {

  2. var value: UnsafePointer<Void>

  3. init(fromUnsafePointer from: UnsafePointer<Void>)

  4. @conversion func __conversion() -> CMutableVoidPointer

  5. }

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

  1. class VaListBuilder {

  2. init()

  3. func append(arg: CVarArg)

  4. func va_list() -> CVaListPointer

  5. }

工具类,方便地创建 CVaListPointer

还有一些工具函数:

  1. func getVaList(args: CVarArg[]) -> CVaListPointer

  2. func withVaList<R>(args: CVarArg[], f: (CVaListPointer) -> R) -> R

  3. func withVaList<R>(builder: VaListBuilder, f: (CVaListPointer) -> R) -> R

非常方便。

UnsafeArray

  1. struct UnsafeArray<T> : Collection, Generator

  2. var startIndex: Int { get }

  3. var endIndex: Int { get }

  4. subscript (i: Int) -> T { get }

  5. init(start: UnsafePointer<T>, length: Int)

  6. func next() -> T?

  7. func generate() -> UnsafeArray<T>

  8. }

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

Unmanaged

  1. struct Unmanaged<T> {

  2. var _value: T

  3. init(_private: T)

  4. func fromOpaque(value: COpaquePointer) -> Unmanaged<T>

  5. func toOpaque() -> COpaquePointer

  6. static func passRetained(value: T) -> Unmanaged<T>

  7. static func passUnretained(value: T) -> Unmanaged<T>

  8. func takeUnretainedValue() -> T

  9. func takeRetainedValue() -> T

  10. func retain() -> Unmanaged<T>

  11. func release()

  12. func autorelease() -> Unmanaged<T>

  13. }

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

C 头文件的导入行为

宏定义

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

枚举 enum

创建 enum 类型,并继承自 CUnsignedInt CInt 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. @asmname("curl_easy_setopt") func curl_easy_setopt(curl: COpaquePointer, option: CURLoption, param: CString) -> CURLcode

  2. @asmname("curl_easy_setopt") func curl_easy_setopt(curl: COpaquePointer, option: CURLoption, param: CBool) -> CURLcode

  3. let handle = curl_easy_init()

  4. // this should be a const c string. curl_easy_perform() will use this.

  5. let url: CString = "http://www.baidu.com"

  6. curl_easy_setopt(handle, CURLOPT_URL, url)

  7. curl_easy_setopt(handle, CURLOPT_VERBOSE, true)

  8. let ret = curl_easy_perform(handle)

  9. let error = curl_easy_strerror(ret)

  10. println("error = \(error)")

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

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

总结

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

------------------------------------------------------------

如果这篇文章对您或您的朋友有所帮助,您可以点击右上角的更多按钮图片

分享给您的朋友们~图片

CocoaChina是全球最大的苹果开发中文社区,它的官方微信每日定时推送各种精彩的研发教程资源和工具,介绍app推广营销经验,最新企业招聘和外包信息,以及Cocos2d引擎、Cocos Studio开发工具包的最新动态及培训信息。关注微信可以第一时间了解最新产品和服务动态!

请搜索微信号“CocoaChina”关注我们图片

继续滑动看下一个
Cocoa开发者社区
向上滑动看下一个