转载

Swift 与 C 的交互

随着 Swift 开源的即将到来,我们很快就可以在没有 Apple 框架的平台上运行 Swift 代码了。因此,我们怎样才能使用诸如快速排序数组之类的功能呢?在本次 #Pragma 2015 演讲中,Chris Eidhof 阐述了如何使用 Swift 来封装一个 C 类库(:grey_exclamation:),从而让 Swift 开发者能够在所有平台上使用 C 标准库。

See the discussion on Hacker News .

Transcription below provided by Realm: a replacement for SQLite & Core Data with first-class support for Swift! Check out the Swift docs!

Sign up to be notified of new videos — we won’t email you for any other reason, ever.

About the Speaker: Chris Eidhof

Chris Eidhof 编写了很多 iOS 和 OS X 应用,包括 Deckset 和 Scenery 。他同样还在很多平台上留有笔迹,有 个人博客 、有 objc.io 、还有 一系列书籍 。他现在一直在维护着 UIKonf 。

@chriseidhof Website

概述(0:00)

我们将会看下如何使用一些简洁的办法来处理 C 与 Swift 之间的交互。具体而言,就是如何在 Swift 中使用 C 的 API。我们以快速排序这个例子开始。

我们现在准备使用 C 标准库中的快速排序方法。通常而言,这个办法是非常不明智的,因为 Swift 中已经内置了排序函数了,由于这些内置的排序函数经过了优化并且工作性能良好,因此一般情况下我们应该使用这些函数。我们这不过是举一个如何使用 C API 的例子,千万不要使用这种版本的快速排序!现在让我们找一个数字数组,然后对其进行排序。在 Swift 中,通常情况下你会这么做:

let numbers = [3,1,2,17,4]  print(numbers.sort())

你也可以使用可以修改数组本身的 sortInPlace()

var numbers = [3,1,2,17,4]  numbers.sortInPlace()  print(numbers)

qsort(3:05)

好的,现在我们就要使用 C 函数让这个操作变得更复杂一点。我们将要使用 qsort 。在终端中输入 man qsort 指令,你会看到:

void qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));

voidqsort 函数的返回类型。它不返回任何东西。这个快排函数同时还修改了原有的数组。 qsort 函数的第一个参数是一个指向数组首地址的 void 指针。这是 C 用来表达:“这是一个数组,其中可能存在任何东西”的方式。对于一个整数数组来说,指向首地址的应该是一个 int 指针,但是这里由于我们不知道数组中会是什么类型,因此这里的参数是 void 指针。第二个参数是 nel ,它指的是数组中元素的数目。C 中的数组必须有一个指针指向其首元素,并且还要指定元素的数目。第三个参数是 width ,它是数组中单个元素所占用的内存空间大小。如果要执行遍历操作的话,那么就需要通过这个参数来实现递增。最后一个参数是一个比较函数,它接收两个 const void 类型的指针,然后返回一个整数。这两个指针表示数组中进行比较的两个元素指针。元素通过 const void 指针进行传递,这意味着我们也可以将任何类型的东西传递进去。 const 标识意味着你不能改变元素的值,它是一个 常量 。星号(*)表明这是一个 C 函数指针,你便可以在 Swift 中使用这些函数指针。

var numbers = [2, 1, 17, 3]  qsort(&numbers, numbers.count, sizeOf(Int)) { (l, r) -> Int32 in   let left: Int = UnsafePointer(l).memory   let right: Int = UnsafePointer(r).memory    if left < right { return -1 }   if left == right {return 0 }   return 1 }

这个函数的第一个参数需要为指向数组第一个元素的指针。在 Swift 当中,我们很容易实现这个功能,那就是 &numbers 。第二个参数是数组中元素的总数。我们可以使用 numbers.count 。接下来,我们需要得到单个元素的空间大小。如果你用过 C 的话,你肯定知道 sizeof() 这个函数。但是这个函数并不适合,我们过一会儿会对此进行说明。第四个参数是用来比较两个数字的一个比较函数。这个闭包获取两个参数,也就是比较符左边和右边的两个元素。它需要返回一个 Int32 类型的数字。然后我们对着两个元素进行比较。这样我们便能够使用 C 中内置的快排方法得到一个排序好的数组了。

我们可以用一个方法将其好好封装起来,这样我们就可以用简单的方法时不时使用这个函数了。

func quicksort(var input: [Int]) -> [Int] {   qsort(&input, input.count, sizeOf(Int)) { (l, r) -> Int32 in     let left: Int = UnsafePointer(l).memory     let right: Int = UnsafePointer(r).memory      if left < right { return -1 }     if left == right {return 0 }     return 1   }   return input }

那么对于字符串来说起作用吗?最简单的方式就是将上面那个函数进行拷贝,然后改点东西就成:

func quicksort(var input: [String]) -> [String] {   qsort(&input, input.count, sizeOf(String)) { (l, r) -> Int32 in     let left: String = UnsafePointer(l).memory     let right: String = UnsafePointer(r).memory      if left < right { return -1 }     if left == right {return 0 }     return 1   }   return input }

qsort_b(18:40)

如果我们想要为其他类型创建快速排序的话,我们需要一遍又一遍地复制,这是一项很繁杂的工作。我们可以通过 Swift 的泛型让事情变得简单一些。我们使用快排的第二种版本: qsort_b

void qsort_b(void *base, size_t nel, size_t width, int (^compar)(const void *, const void *));

我们对这个类型进行分析。 qsort_b 接收一个 void 指针。这个指针指向数组中的第一个元素。然后接收元素总数、元素占用空间,以及比较函数指针作为参数。然而,如果你仔细观察的话你会发现这个函数有一个很不起眼的不同点。在 qsort 中我们使用的是星号。在 qsort_b 中,我们用的是 Caret(^) 符号。这意味着这里接收的是一个闭包。如果你在 Objective-C 、C 和 Swift 中都可以使用闭包的话,那么就可以在闭包外指定泛型了。使用 qsort_b 的话就可以使用下面这段代码了:

func quicksort<A: Comparable>(var input: [A]) -> [A] {   qsort_b(&input, input.count, sizeOf(A)) { (l, r) -> Int32 in     let left: A = UnsafePointer(l).memory     let right: A = UnsafePointer(r).memory      if left < right { return -1 }     if left == right {return 0 }     return 1   }   return input }

现在,我们拥有了一个泛型的快排函数。只要 A 类型是遵循 Comparable 协议的,那么这个函数就可以用。我们当然可以说“万事大吉”。不过,我们需要让事情变得更复杂些。快排拥有一个基于闭包的变体,然而绝大多数你所使用的 C API 都不会有类似的变体。它们只能够接受函数指针。在 C 中,惯用做法是使用传递上下文的函数指针。

qsort_r(20:25)

如果你在封装 C 标准库中的其他函数的话,一般来说你都不能使用基于闭包的 API 变体。你需要使用第三种方式来解决这个问题,也就是快排的第三种形式:

void qsort_r(void *base, size_t nel, size_t width, void *thunk, int (*compar)(void *, const void *, const void *));

这个变体参数有相同的数组指针、有元素总数、有元素内存大小,不同的是多了一个名为 thunk 的空指针。比较函数同样发生了变化。 compar 多接收一个空指针作为参数,另外两个才是空指针常量。 thunk 这个新增的参数作为指向 compar 函数的第一个参数传入。我们可以在这个 thunk 指针中放入任何东西,然后我们在比较函数内部再获取这个参数。这也是绝大多数 C API 都提供的一种方式。因此,与 qsort_b 相比,我更喜欢使用 qsort_r ,然后将比较函数作为参数传递到 thunk 当中。

extension Comparable {   static func compare(l: UnsafePointer<Void>, _ r: UnsafePointer<Void>) -> Int32 {     let left: Self = UnsafePointer(l).memory     let right: Self = UnsafePointer(r).memory     if left < right { return -1 }     if left == right { return 0 }     return 1   }  }  typealias CompareFunc = (UnsafePointer<Void>, UnsafePointer<Void>) -> Int32  func cmp(thunk: UnsafeMutablePointer<Void>, l: UnsafePointer<Void>, r: UnsafePointer<Void>) -> Int32 {   let compareFunc: CompareFunc = UnsafeMutablePointer(thunk).memory   return compareFunc(l,r) }  extension Array where Element: Comparable {   func quicksort() -> [Element] {     var array = self     var compare = Element.compare     qsort_r(&array, array.count, strideof(Element), &compare, cmp)     return array   }    mutating func quicksortInline() {     self = quicksort()   }  }  var myArray = ["Hello", "ABC", "abc", "test"] myArray.quicksortInline()  print(myArray)

我们都干了些啥?我们简单回顾一下。首先我们以一个非常简单的 qsort 例子开始,对一些数字进行了排序。接着,我们将其放入到函数中以便让其能够对任何数字类型的数组有效。然后我们开始为其增加泛型特性,功能变得更加复杂了。闭包相同来说还好,但是一旦我们需要使用 qsort_r 的时候,事情变得更加离奇了。然而,这在 C 的函数库中是一个很常见的模式,当你想封装 C 函数库的时候,你都可以使用这个方法。你或许会想“我为啥要封装 C 的函数库呢?”我认为一旦 Swift 开源之后,我们就会让其在诸如 Linux 等多个平台上能够很好的运行。而在 Linux 上我们很可能无法访问存在于 Cocoa 和 iOS 中的所有 Apple 框架。我们需要封装关于网络、绘图等一系列的框架。这样,知道关于如何与 C 函数库协同工作就变得很重要了。这就是为什么我认为这个知识是很有用的。

问与答(39:47)

问:如果你打算在项目中使用 C 的函数和类库的话,那么把文件拷贝到项目当中就可以在相同的模组当中用 Swfit 调用了吗?

Chirs:没错,这和在 Swift 当中使用 Objective-C 很相似。

问:假设你有一个 C 类库,并且你能使用的 API 数据都只能以函数指针的形式来显示,不能够使用任何的闭包 API 或者转换 API的话。是否就没有别的办法了吗,或者只能够使用上下文来进行传递了呢?

Chris:是的,如果没有 thunk 或者经常调用上下文的话,那么我们就别无他法了。

问:你开始这么做的动机是什么呢?当你第一次开始这么做的时候你的感受如何?当最后成功的时候感受又是如何呢?

Chris:我们先谈谈所谓的“动机”。正常情况下,我不会使用 C 类库,但是当苹果开源了 Swift 之后,我就在想这么做会不会很有意思。与此同时,我和 Airspeed Velocity 组建了一个撰写 Swift 新书籍的团队。这个计划是在苹果宣布开源之前实施的,之后,当苹果宣布开源的时候我就意识到:“我的天,Swift 即将无处不在了!”。对于绝大多数平台来说,我们无法使用苹果的自建框架。我很享受在 Linux 编写 Swift 代码的感觉,以及使用 Swift 来编写后端程序。然而,我就需要学习如何封装 C API。因此,在我们的书当中,我们决定着重强调与 C 语言交互的这部分。

如果你使用的 C API 是异步工作的话,你或许会考虑很多关于内存管理的工作。你需要保留某些对象,并且确保在离开函数范围后不会出现内存溢出的情况。不过当你读完 Kernighan 和 Ritchie 那本 C 语言书之后,你会发现这十分简单。不过,对于 C API 和库来说,它们之间还有大量的不同。这可能会造成极大的困扰。接着,我意识到只需要掌握很少一部分的技巧,就可以将这些 API 进行封装,然后就可以在 Swift 当中使用它们了,我觉得这个功能十分强大。现在我知道我可以在 Swift 当中使用绝大多数的 C 类库了。我为此建立封装,进行充分的测试,最后就可以为我所用了。

问:在 Github 上有一个名为 SwiftGo 的项目展示了 Go 和 Swift 的桥接工作,这个项目看起来很有前景。你对在 Swift 与诸如 Go 这样的语言建立桥接的工作是怎么看待的呢?

Chris:我不是很确切地知道 SwiftGo 所做的工作,不过如果我的记忆没错的话,他们是为 Go 功能封装了一个库,而不是为 Go 语言本身封装了一个库。我觉得这个项目很强大因为现在你就可以很好地使用它了。我很乐意见到越来越多的人在 Swift 当中封装 C 类库。

See the discussion on Hacker News .

Sign up to be notified of new videos — we won’t email you for any other reason, ever.

正文到此结束
Loading...