Pragma eidhof cover

Swift 与 C 的交互

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


概述 (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 中使用这些函数指针。

Receive news and updates from Realm straight to your inbox

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 类库。

About the content

This talk was delivered live in October 2015 at #Pragma Conference. The video was transcribed by Realm and is published here with the permission of the conference organizers.

Chris Eidhof

Chris Eidhof is the author of many iOS and OS X applications, including Deckset and Scenery. He has also written extensively on the subject, from his personal blog to objc.io to a variety of books. He formerly ran UIKonf, and still runs frequently.

4 design patterns for a RESTless mobile integration »

close