如何把字符串數組從 Swift 傳遞給 C
Swift 允許我們將原生的字符串直接傳遞給一個接受 C String(即 char * )的 C API。 比如說,你可以在 Swift 里調用 strlen 函數,如下所示:
import Darwin // or Glibc on Linux
strlen("Hello :smiley:") // → 10
雖然在 Swift 中, const char * 參數是作為 UnsafePointer \<Int8>! 導入的,但這的確可行。 Swift 導入的 strlen 函數的完整類型定義如下:
func strlen(_ __s: UnsafePointer<Int8>!) -> UInt
類型檢查器能夠 將 String 值傳遞給一個 UnsafePointer<Int8> 或 UnsafePointer<UInt8> 參數 。在此過程中,編譯器隱式地創建了一個緩沖區,它包含一段以 UTF-8 編碼[^1]、以 null 結束的字符串,并傳回一個指向緩沖區的指針給函數。
對 C 字符串數組沒有內置支持
Swift 處理單個 char * 參數的方式非常簡便。但是,一些 C 函數接收字符串數組(一個 char * 或 char * [] )作為參數,而 Swift 對將 [String] 傳遞給一個 char * 參數并沒有內置支持。
一個實用的例子是子進程啟動時的 posix_spawn 函數。 posix_spawn 的最后兩個參數( argv 和 envp )是用于傳遞新進程的參數和環境變量的字符串數組。文檔中是這么說明的:
argv (和 envp )是指向以 null 結尾的字符串數組指針,數組元素指向以 null 結束的字符串。
Swift 將這些參數中 C 類型的 char * const argv [] 轉換為難以處理的 UnsafePointer <UnsafeMutablePointer<Int8>?>! ,感嘆號表示 對可選值隱式解包 ,告訴我們 API 這里的參數不能為空,即 Swift 不知道函數是否接受傳遞 NULL(在這種情況下外層 UnsafePointer 將為可選值)。我們必須參考文檔來回答這個問題。在本示例中,文檔明確聲明了 argv 必須至少包含一個元素(生成程序的文件名)。 envp 可以為 NULL ,表示它將繼承其父進程的環境。
將 Swift 字符串數組轉換為 C 字符串數組
假設我們想為 posix_spawn [^2] 提供一個優雅的 Swift 接口。 我們的封裝函數應該接收以下參數,一是正在啟動的程序的路徑,二是字符串數組:
/// 產生一個子進程
///
/// - Returns: A pair containing the return value of `posix_spawn` and the pid of the spawned process.
func spawn(path: String, arguments: [String]) -> Int32
現在我們需要將參數數組轉換為 posix_spawn 能夠接收的格式。 這需要幾個步驟:
- 以 UTF-8 編碼字符串元素。
- 為每個 UTF-8 編碼的字符串的末尾添加一個空字節。
- 將所有 UTF-8 編碼的、以空字節結尾的字符串拷貝到一個緩沖區中。
- 在緩沖區的末尾添加另一個空字節,表明 C 數組的結尾。
- 確保緩沖區存在于 posix_spawn 被調用的整個生命周期內。
withArrayOfCStrings 在標準庫中
Swift 團隊也需要使用這個功能來運行標準庫的單元測試,因此標準庫的源也包括一個名為 withArrayOfCStrings 的函數。現在這是一個私有函數,不公開暴露給 stdlib 使用者(雖然它被聲明為 public ,大概因為不這么做的話單元測試無法看到它)。但這個函數依然對我們可見。這是該函數的接口:
public func withArrayOfCStrings<R>(
_ args: [String],
_ body: ([UnsafeMutablePointer<CChar>?]) -> R
) -> R
它具有與 withUnsafePointer 及其變體相同的形式:它的結果類型 R 是一個泛型,并且接收一個閉包作為參數。其思想是,在將字符串數組轉換為 C 數組之后, withArrayOfCStrings 調用閉包,傳遞 C 數組,并將閉包的返回值轉發給其調用者。這使得 withArrayOfCStrings 函數完全控制它自己創建緩沖區的生命周期。
我們現在可以這樣實現 spawn 函數:
/// Spawns a child process.
///
/// - Returns: A pair containing the return value of `posix_spawn` and the pid of the spawned process.
func spawn(path: String, arguments: [String]) -> (retval: Int32, pid: pid_t) {
// Add the program's path to the arguments
let argsIncludingPath = [path] + arguments
return withArrayOfCStrings(argsIncludingPath) { argv in
var pid: pid_t = 0
let retval = posix_spawn(&pid, path, nil, nil, argv, nil)
return (retval, pid)
}
}
為什么這是可行的呢?能注意到 withArrayOfCStrings 的閉包參數的類型為 ([UnsafeMutablePointer<CChar>?]) -> R 。參數類型 [UnsafeMutablePointer <CChar>?] 看起來與 posix_spawn 要求的 UnsafePointer <UnsafeMutablePointer<Int8>?>! 并不兼容,但其實是兼容的。 CChar 只是 Int8 的別名。再者,正如 Swift 對于傳遞給 C 的字符串會有特殊處理,編譯器隱式地將原生 Swift 數組傳遞給接收 UnsafePointer<Element> 參數的 C 函數。因此我們可以將數組直接傳遞給 posix_spawn ,只要它的元素類型與指針指向元素的類型相匹配。
這是使用 spawn 函數的樣例:
let (retval, pid) = spawn(path: "/bin/ls", arguments: ["-l", "-a"])
這是執行程序的輸出:
$ swift spawn.swift
posix_spawn result: 0
new process pid: 17477
total 24
drwxr-xr-x 4 elo staff 136 Oct 27 17:04 .
drwx---r-x@ 41 elo staff 1394 Oct 24 20:12 ..
-rw-r--r--@ 1 elo staff 6148 Oct 27 17:04 .DS_Store
-rw-r--r--@ 1 elo staff 2342 Oct 27 15:28 spawn.swift
(注意,如果你在 playground 中調用它, posix_spawn 會返回一個錯誤,可能是因為 playground 的沙盒不允許生成子進程。因此最好通過命令行創建,或在 Xcode 中創建一個新的命令項目)。
工作原理
withArrayOfCStrings 的完整實現 如下所示:
public func withArrayOfCStrings<R>(
_ args: [String], _ body: ([UnsafeMutablePointer<CChar>?]) -> R
) -> R {
let argsCounts = Array(args.map { $0.utf8.count + 1 })
let argsOffsets = [ 0 ] + scan(argsCounts, 0, +)
let argsBufferSize = argsOffsets.last!
var argsBuffer: [UInt8] = []
argsBuffer.reserveCapacity(argsBufferSize)
for arg in args {
argsBuffer.append(contentsOf: arg.utf8)
argsBuffer.append(0)
}
return argsBuffer.withUnsafeMutableBufferPointer {
(argsBuffer) in
let ptr = UnsafeMutableRawPointer(argsBuffer.baseAddress!).bindMemory(
to: CChar.self, capacity: argsBuffer.count)
var cStrings: [UnsafeMutablePointer<CChar>?] = argsOffsets.map { ptr + $0 }
cStrings[cStrings.count - 1] = nil
return body(cStrings)
}
}
讓我們逐行解說。第一行為輸入的字符串創建一個 UTF-8 編碼的字符計數(加上為空的終止標識的一字節)的數組:
let argsCounts = Array(args.map { $0.utf8.count + 1 })
下一行讀取這些字符計數,并計算每個輸入字符串的字符偏移量,即每個字符串將在緩沖區中的開始位置。第一個字符串當然將被定位在偏移量為零的地方,并通過累積字符計數來計算后續偏移量:
let argsOffsets = [ 0 ] + scan(argsCounts, 0, +)
代碼使用一個名為 scan 的幫助函數,它 定義在同一個文件里 。注意, argsOffsets 包含的元素數量比 argsCounts 多一個。因為 argsOffsets 的最后一個元素是最后一個輸入字符串之后的偏移量,即所需的緩沖區的大小。
下一步是創建一個字節數組(元素類型為 UInt8 )用作緩沖區。由于緩沖區會自動增長,因此調用 reserveCapacity 不是必要的。但如果在開始時能事先知道的所需容量并保留的話,可以避免重復的分配行為:
let argsBufferSize = argsOffsets.last!
var argsBuffer: [UInt8] = []
argsBuffer.reserveCapacity(argsBufferSize)
現在可以將 UTF-8 編碼的字節寫入緩沖區,并在每個輸入的字符串后添加一個空字節:
for arg in args {
argsBuffer.append(contentsOf: arg.utf8)
argsBuffer.append(0)
}
此時,我們有一個正確格式的字節數組( UInt8 )。我們仍然需要構造指向緩沖區中的元素的指針數組。這就是函數最后一部分的作用:
return argsBuffer.withUnsafeMutableBufferPointer {
(argsBuffer) in
let ptr = UnsafeMutableRawPointer(argsBuffer.baseAddress!).bindMemory(
to: CChar.self, capacity: argsBuffer.count)
var cStrings: [UnsafeMutablePointer<CChar>?] = argsOffsets.map { ptr + $0 }
cStrings[cStrings.count - 1] = nil
return body(cStrings)
}
我們利用 withUnsafeMutableBufferPointer 獲得數組,其元素表示指向緩沖區的指針。內部閉包的第一行代碼通過 UnsafeMutableRawPointer 將元素指針的類型從 UnsafeMutablePointer<UInt8> 轉換為 UnsafeMutablePointer <CChar> 。 (從 Swift 3.0 開始,你不能直接在類型化的指針之間進行轉換, 你必須首先轉換成 Unsafe[Mutable] RawPointer 。)這段代碼的可讀性不是很好,但對我們來說這行之后的內容才是重要的。本地 ptr 變量是指向緩沖區中的第一個字節的 UnsafeMutablePointer<CChar> 。
現在,為了構造指針數組,我們為第二行中創建的字符偏移數組做映射,并根據每個偏移量向后移動指針。最后將結果數組中的最后一個元素設置為 nil ,用作表示數組結尾的空指針(記得我們之前說的 argsOffset 要比輸入數組包含多一個元素嗎?因此重寫最后一個元素是正確的)。
最后,我們可以調用從調用者傳遞過來的閉包,傳遞指向 C 字符串的指針數組。
[^1]: 注意,由于上面的 emoji 是以 UTF-8 格式傳遞的,它在 strlen 函數里會占用四個“字符“。
[^2]: 在這里使用了 posix_spawn 作為簡單的例子來講解。但在生產代碼中,應該使用 Foundation 框架里更高級的 Process 類(née NSTask )來實現。
來自:http://swift.gg/2016/12/20/swift-array-of-c-strings/