Swift 和 C 不得不說的故事

作者:Umberto Raimondi,原文鏈接,原文日期:2016-04-07
譯者:shanks;校對:pmst;定稿:CMB

從 Swift 開源到現(xiàn)在,只有短短的幾個月時間,Swift 卻已經(jīng)被移植到了許多新的平臺上,還有一些新的項目已經(jīng)使用了 Swift。這類移植,每個月都在發(fā)生著。

在不同平臺下混合使用 Swift 和 C 的可行性,看起來是一件非常難的實踐,只有非常有限的實踐資源,當然這是和你去封裝一個原生庫對比起來看的,你可以在你代碼運行的平臺上輕松地封裝一個原生庫。

官方文檔 Using Swift with Cocoa and Objective-C 已經(jīng)系統(tǒng)地講解了有關與 C 語言互調的基本知識。但僅限于此,尤其是在實際的場景中如何去使用這些橋接函數(shù),感覺仍然是一臉懵逼的。僅有少數(shù)博客文章會有此文檔筆記和使用講解。

這篇文章將在一些不是那么明顯的細節(jié)地方給你一些啟發(fā),同時給出一些實際的例子,講解如何與 C 語言的 API 互調。這篇文章主要是面向那些計劃在 Linux 下進行 Swift 開發(fā)的同學,另外文中的一些解釋,同樣適用于基于 Darwin 的操作系統(tǒng)。

首先簡要介紹如何把 C 類型導入 Swift 中,隨后我們將深入研究有關指針,字符串和函數(shù)的使用細節(jié),通過一個簡單的教程學習使用 LLVM 模塊創(chuàng)建 Swift 和 C 混編的項目。

GitHub或者zipped獲取 Swift/C 混合編碼的 playground。

內容介紹

<a name="c_type"></a>

C 類型

每一個 C 語言基本類型, Swift 都提供了與之對應的類型。在 Swift 中調用 C 方法的時候,會用到這些類型:

C 類型 Swift 對應類型 別名
bool CBool Bool
char,unsigned char CChar, CUnsignedChar Int8, UInt8
short, unsigned short CShort, CUnsignedShort Int16, UInt16
int, unsigned int CInt, CUnsignedInt Int32, UInt32
long, unsigned long CLong, CUnsignedLong Int, UInt
long long, unsigned long long CLongLong, CUnsignedLongLong Int64, UInt64
wchar_t, char16_t, char32_t CWideChar, CChar16, CChar32 UnicodeScalar, UInt16, UnicodeScalar
float, double CFloat, CDouble Float, Double

官方文檔中對上面表格也有介紹,展示了 Swift 類型和對應的 C 別名。

即使在你寫一些需要調用 C APIs 的代碼時,你都應該盡可能地使用 Swift 的 C 類型。你會注意到,大多數(shù)從 C 轉換到 Swift 的類型,都是簡單地使用了常用的 Swift 固定大小的類型,而這些類型,你應該已經(jīng)相當熟悉了。

<a name="arrays_and_structs"></a>

數(shù)組和結構體

讓我們接下來聊聊復合數(shù)據(jù)結構:數(shù)組和結構體。

理想的情況下,你希望定義一個如下全局數(shù)組:

c
//header.h

char name[] = "IAmAString";

在 Swift 中,有可能會被轉換成一個 Swift 字符串,或者至少是某種字符類型的數(shù)組。當然,當我們真正在 Swift 中使用這個導入的 name 數(shù)組,將會出現(xiàn)以下結果:

print(name) // (97, 115, 100, 100, 97, 115, 100, 0)

這個事實告訴我們,當你在做一個 Swift/C 混合的應用下時,在 C 語言層面,推薦使用指針表示一個對象的序列,而不是使用一個普通的數(shù)組。這樣能避免在 Swift 語言層面下痛苦的轉換。

但是等一下,如果我們使用一段復雜的代碼轉換數(shù)字元組,恢復成之前定義為數(shù)組的全局字符串,是否更加好呢?答案是否定的,我們將會在討論指針的時候,介紹如何使用一小段代碼如何復原數(shù)組元組。

幸運的是,以上的情況不會在處理結構體時候發(fā)生,將會如預期的轉換為 Swift 的結構體,結構體的成員也將會按照預期的方式轉換,每一個成員都會轉換成對應的 Swift 類型。

比如,有以下的結構體:

c
typedef struct {
    char name[5];
    int value;
    int anotherValue;
} MyStruct;

這個結構體將會轉換成一個 MyStruct 的 Swift 結構體。結構體的構造函數(shù)的轉換也很簡單,跟我們想象中的一樣:

let ms = MyStruct(name: (0, 0, 0, 0, 0), value: 1, anotherValue:2)
print(ms)

下文某個章節(jié),我們將看到這并非是唯一方法去構造和初始化一個結構體實例,尤其是在我們只需要一個指向空對象的指針時,更簡單的方式應該是手動分配一個新的空結構體指針實例。

<a name="enums"></a>

枚舉

如果你需要使用 Swift 訪問 C 的枚舉,首先在 C 中定義一個常見的枚舉類型:

c
typedef enum ConnectionError{
    ConnectionErrorCouldNotConnect = 0,
    ConnectionErrorDisconnected = 1,
    ConnectionErrorResetByPeer = 2
}

當轉換到 Swift 中時候,會與你期望的情況完全不同, Swift 中的枚舉是一個結構體,并且會有一些全局變量:

struct ConnectionError : RawRapresentable, Equatable{ }

var ConnectionErrorCouldNotConnect: ConnectionError {get}
var ConnectionErrorDisconnected: ConnectionError {get}
var ConnectionErrorResetByPeer: ConnectionError {get}

顯然這樣做的話,我們將喪失 Swift 原生枚舉提供的所有功能點。但是如果在 C 中使用一個特定的宏定義的話,我們將得到我們想要的結果:

c
typedef NS_ENUM(NSInteger,ConnectionError) {
    ConnectionErrorCouldNotConnect,
    ConnectionErrorDisconnected,
    ConnectionErrorResetByPeer   
}

使用NS_ENUM宏定義的枚舉(關于這個宏定義如何對應到一個經(jīng)典的 C 枚舉的知識,請參看這里),以下代碼展示在 Swift 如何導入這個枚舉:

enum ConnectionError: Int {
    case CouldNotConnect
    case Disconnected
    case ResetByPeer
}

需要注意的是,枚舉值的轉換是去掉了枚舉名的前綴了的,這是 Swift 其中一個轉換的規(guī)則,你也會在使用標準的基于 Swift iOS/OSX 框架時候看到這種規(guī)則。

另外, Swift 提供了 NS_OPTIONS 宏定義,用于定義一個可選項集合,遵從 OptionSetType 協(xié)議(目前為 OpertionType )。關于此宏定義的更多介紹,請參看官方文檔

<a name="unions"></a>

聯(lián)合體

接下來讓我們看看聯(lián)合體,一個有趣的 C 類型,在 Swift 中沒有對應的數(shù)據(jù)結構。

Swift 僅部分支持聯(lián)合體,意思是當一個聯(lián)合體被導入時,不是每一個字段都會被支持,造成的結果就是,你在 C 中定義的某些字段將不可用(截止目前,沒有一個文檔說明什么不被支持)。

讓我們用一個實際的例子來說明這個被文檔遺忘的 C 類型:

c
//header.h
union TestUnion {
    int i;
    float f;
    unsigned char asChar[4];
} testUnion;

在這里我們定義一個 TestUnion 類型,還有一個相關的 testUnion 聯(lián)合體變量,一共有 4 字節(jié)的內存,其中每一個字段代表不同的視角,在 C 語言中,我們可以訪問 testUnion 變量,這個變量可以是整形,浮點數(shù)和 char 字符串。

由于在 Swift 中,沒有類似的數(shù)據(jù)結構與聯(lián)合體對應,所以這種類似將在 Swift 中被視作一個結構體

strideof(TestUnion)  // 4 bytes

testUnion.i = 33
testUnion.f  // 4.624285e-44
testUnion.i  // 33
testUnion.asChar // (33, 0, 0, 0)

testUnion.f = 1234567
testUnion.f  // 1234567
testUnion.i  // 1234613304
testUnion.asChar // (56, 180, 150, 73)

正如我們對聯(lián)合體期望那樣,上面第一行代碼驗證這個類型的確只占 4 個字節(jié)的內存長度。接下來的代碼,修改其中一個字段,然后驗證包含在其他字段中得值是否同時被更新。但是為什么當我們設置 testUnion 的整型字段為 33 時,我們獲取對應的 float 字段的值卻為 4.624285e-44?

這就跟聯(lián)合體如何工作有關了。你可以把一個聯(lián)合體想象為一個字節(jié)包,根據(jù)每個字段組成的格式化規(guī)則進行讀寫,在上面的例子中,我們設置的 4 個字節(jié)的內存區(qū)域,與 Int32(32)的字節(jié)內容組成是相同的,然后我們讀取這4個字節(jié)的內存區(qū)域,解釋成為的字節(jié)模式是一個 IEEE 的浮點數(shù)。

我們使用一個有用的(但是危險的) unsafeBitCast 函數(shù)來驗證上面的解釋:

var fv:Float32 = unsafeBitCast(Int32(33), Float.self)   // 4.624285e-44

以上代碼的作用,與使用聯(lián)合體的浮點類型,訪問一個包含 Int32(33) 的字節(jié)內存做得事情一樣。賦值給了一個浮點類型,并且沒有做任何的轉換和內存安全檢查。

到目前為止我們已經(jīng)學習了聯(lián)合體的行為,那么我們能在 Swift 中手動實現(xiàn)一個類似的結構體嗎?

即使沒有去查看源代碼,我們也可以猜到 TestUnion 只是一個簡單的結構體,只有4個字節(jié)的內存數(shù)據(jù)塊(是那種形式的并不重要),我們只能訪問其中的計算屬性,這些計算屬性把所有的轉換細節(jié)封裝在了 set/get 方法中了。

<a name="the_size_of_things"></a>

關于長度的那些事

在 Swift 中,你可以使用 sizeof 函數(shù)獲取特定類型(原生的和組合的)的數(shù)據(jù)長度,就像你在 C 語言中使用 sizeof 操作符一樣。Swift 同時還提供了一個 sizeOfValue 函數(shù),返回一個類型給定值的數(shù)據(jù)長度。

但是 C 語言中 sizeof 返回值包含了附加填充保證內存對齊,而 Swift 中的函數(shù)只是返回變量的數(shù)據(jù)長度,不管究竟是如何在內存中存儲的,然而這在大多數(shù)情況與我們的期望背道相馳。

我想你應該可以猜到, Swift 同時也提供了 2 個附加的函數(shù),正確地得到變量或者類型的長度,并且計算包括用于對齊需要的額外空間,大多數(shù)情況下,你應該習慣替換之前的一些函數(shù)而使用 strideofstrideOfValue 方法,讓我們通過一個例子來看看 sizeofstrideof 返回的區(qū)別:

print(strideof(CChar))  // 1 byte

struct Struct1{
    let anInt8:Int64
    let anInt:Int16
    let b:Bool
}

print(sizeof(Struct1))    // 11 (8+2+1) byte
print(strideof(Struct1))  // 16 (8+4+4) byte

同時當計算額外的空間時,需要遵守處理器架構的對齊規(guī)則,不同的處理器架構下,strideofsizeof 之間返回的值會有所不同,一個附加的工具函數(shù)alignof可供使用。

<a name="null_nil_0"></a>

Null, nil 和 0

幸運的是, Swift 沒有提供一個額外的常量來表示 null 值,你只能使用 Swift 的 nil ,不管指定的變量或者參數(shù)的類型是什么。

在后面談到指針時,nil 作為參數(shù)傳遞將會自動被轉換成一個 null 指針。

<a name="macros"></a>

宏定義

簡單的 C 宏定義會轉換成 Swift 中得全局常量,與 C 中的常量有點類似:

c
#define MY_CONSTANT 42

將被轉換成:

let MY_CONSTANT = 42

更加復雜的宏定義和預處理指令會徹底被 Swift 忽略摒棄。

Swift 也提供了一個簡單的條件式編譯聲明方式,指明某些具體的代碼片段只能在特定的?操作系統(tǒng),架構或版本的 Swift 中使用。

#if arch(arm) && os(Linux) && swift(>=2.2)
    import Glibc
#elseif !arch(i386)
    import Darwin
#else
    import Darwin
#endif

puts("Hello!")

在這個例子中,我們根據(jù)不同的編譯環(huán)境,ARM Linux 或者其他環(huán)境,決定需要導入的標準 C 庫,用于在不同的環(huán)境中編譯和使用。

這些用來定制編譯行為的可用函數(shù)是: os() (可用值: OSX, iOS, watchOS, tvOS, Linux), arch() (可用值: x86_64, arm, arm64, i386) 和 swift() (要求參數(shù)值指定大于等于某個版本號)。這些函數(shù)可以結合一些基本的邏輯與運算符一起使用,構建更加復雜的規(guī)則:&&, ||, !。

盡管你可能對此不太了解,你只要記住在 OSX 中應該導入 Darwin(或者其中某個依賴它的框架)到你的項目中就可以了,用于獲取 libc 的函數(shù), 而在 Linux 的平臺上,你應該導入 Glibc。

<a name="working_with_pointers"></a>

指針操作

指針被自動的轉換為不同類型的 UnsafePointer<Memory> 對象,對象取決于指針指向值的特征:

C 指針 Swift 類型
int * UnsafeMutablePointer<Int32>
const int * UnsafePointer<Int32>
NSDate** AutoreleasingUnsafeMutablePointer<NSDate>
struct UnknownType * COpaquePointer

通用的規(guī)則是,可變的指針變量指向可變的變量,在第三個示例中,指向對象指針的指針被轉換為 AutoreleasingUnsafeMutablePointer

然而,如果指向的類型沒有完全定義或不能在 Swift 中表示,這種指針將會被轉換為 COpaquePointer (在 Swift 3.0 中,將會簡化為 OpaquePointer ),一種沒有類型的指針,特別是只包含一些位(bits)的結構體。 COpaquePointer 指向的值不能被直接訪問,指針變量首先需要轉換才能使用。

UnsafeMutablePointer 類型會自動轉換為 UnsafePointer<Type> (比如當你傳入一個可變的指針到一個需要不可變指針的函數(shù)中時),反過來轉換的話,將會出現(xiàn)編譯錯誤。一個指向不可變值的指針,不能被轉換成一個指向可變值的指針,在這種情況下,Swift 會保證最小的安全性。

類名稱帶有unsafe字眼代表了我們如何去訪問內容,但是指向的對象的生命周期是怎么樣的,我們應該如何處理,難道是通過 ARC 嗎?

我們已經(jīng)知道,Swift 使用 ARC 來管理引用類型的生命周期(一些結構體和枚舉類型包含引用類型時,也會被管理起來。)并且跟蹤宿主,那么 UnsafePointers 的行為是通過一些特有的方式進行的嗎?

答案是否定的,如果 UnsafePointer<Type> 結構體指向的是一個引用類型(一個類的對象)或者包含一些被跟蹤的引用,那么 UnsafePointer<Type> 結構體將被跟蹤。你應該知道這些事實,這會有助于去理解一些奇怪的事情,在我們后面討論內存分配的時候會遇到。

現(xiàn)在我們已經(jīng)知道指針是如何轉換的,另外還有2個事情要說明一下:指針如何解引去獲取或者修改指向的值,以及我們如何能獲取一個指向新的或者已經(jīng)存在的 Swift 變量的指針。

一旦你得到一個非空的 UnsafePointer<Memory> 變量時,直接使用 memory 屬性獲取或者修改指向的值(校對者注:目前 Swift3 中已改為 pointee 解引取值):

var anInt:Int = myIntPointer.memory   //UnsafePointer<Int> --> Int

myIntPointer.memory = 42

myIntPointer[0] = 43

你也可以訪問同類型指針序列中的特定元素,就像你在 C 語言中使用數(shù)組下標那樣,每次累加索引值,移動到序列中下一個 strideof(Memory) 長度的元素位置。

另外一方面,如果你獲取一個變量的 UnsafePointer 指針,然后將其作為參數(shù)傳遞給函數(shù),只有在這種情況下,

使用 & 操作符能夠簡單地將 inout 參數(shù)傳遞到函數(shù)中:

let i = 42
functionThatNeedsAPointer(&i)

考慮到操作符不能運用在那些描述過的函數(shù)調用上下文之外的轉換,如果你需要獲取一個指針變量做進一步的計算(例如指針類型轉換), Swift 提供了 2 個工具函數(shù) withUnsafePointerwithUnsafeMutablePointer

withUnsafePointer(&i, { (ptr: UnsafePointer<Int>) -> Void in
    var vptr= UnsafePointer<Void>(ptr)  
    functionThatNeedsAVoidPointer(vptr)
})

let r = withUnsafePointer(&i, { (ptr: UnsafePointer<Int>) -> Int in
    var vptr = UnsafePointer<Void>(ptr)
    return functionThatNeedsAVoidPointerAndReturnsInt(vptr)
})

這個函數(shù)創(chuàng)建了一個給定變量的指針對象,把它傳入給一個閉包,閉包使用它然后返回一個值。在閉包作用域里面,指針能夠保證一直有效,可以認為只能在閉包的上下文中使用,不能返回給外部的作用域。

這種方式使得訪問變量可能引發(fā)的不安全性被限制在一個定義良好的閉包作用域中。在上面的例子中,我們在傳遞這個參數(shù)給函數(shù)之前,把整型指針轉換為了void指針。要感謝 UnsafePointer 類的構造函數(shù)可以直接做這種指針之間的轉換。

接下來讓我們簡單看看之前的 COpaquePointer , ,關于COpaquePointer ,沒有特別的地方,它可以很容易地轉換成一個給定類型的指針,然后使用 memory 屬性來訪問值,就像其他的UnsafePointer一樣。

// ptr is an untyped COpaquePointer

var iptr: UnsafePointer<Int>(ptr)
print(iptr.memory)

現(xiàn)在讓我們回到本文開頭定義的那個字符數(shù)組上來,根據(jù)我們目前掌握的知識點,知道一個 CChar 的元組可以自動轉換成一個指向 CChar 序列的指針,這樣可以輕松地把這個元組轉換成字符串:

let namestr = withUnsafePointer(&name, { (ptr) -> String? in
    let charPtr = UnsafeMutablePointer<CChar>(ptr)
    return String.fromCString(charPtr)
})
print(namestr!) //IA#AString

我們可以使用其他方式獲得一個指向典型 Swift 數(shù)組的指針,然后調用某個方法將其轉換成 UnsafeBufferPointer :

let array: [Int8] = [ 65, 66, 67, 0 ]
puts(array)  // ABC
array.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer<Int8>) in
    puts(ptr.baseAddress + 1) //BC
}

請注意 UnsafeBufferPointer 可以使用 baseAddress 屬性,這個屬性包含了緩沖區(qū)的基本地址。

還有另外一個類型的指針我們還沒有討論:函數(shù)指針。從 Swift 2.0開始,C 函數(shù)指針被導入為閉包,使用一個特殊的屬性標記 @convention(c) ,表示這個閉包遵從 C 調用約定,我們將在接下來的某個章節(jié)解釋其具體的含義。

請暫時忽略具體的實現(xiàn)細節(jié),你只需了解函數(shù)指針的基本知識:每導入一個 C 函數(shù),如果需要將函數(shù)指針作為參數(shù)傳入時,會使用一個內置定義的閉包,或者一個 Swift 函數(shù)引用(就像其他指針一樣,nil 也是允許的)作為參數(shù)。

<a name="allocating_memory"></a>

內存分配

到現(xiàn)在為止,我們僅使用指針指向已經(jīng)存在的 Swift 對象,但是并沒有手動分配過內存。在這個章節(jié)中,我們將會學習如何在 Swift 中使用推薦的方式進行內存分配,或者就如我們在 C 語言中所做的那樣,使用malloc系列函數(shù)完成內存分配(可能在一些特定情況下非常有用)。

在開始之前,我們需要意識到 UnsafePointers 和古老的 C 指針一樣,在它們的生命周期中存在 3 種可能的狀態(tài):

  • 未分配的:沒有預留的內存分配給指針
  • 已分配的:指針指向一個有效的已分配的內存地址,但是值沒有被初始化。
  • 已初始化:指針指向已分配和已初始化的內存地址。

指針將根據(jù)我們具體的操作在這 3 個狀態(tài)之間進行轉換。

大多數(shù)情況下,推薦你使用 UnsafePointer 類提供處理指針的方法分配一個新的對象,然后獲取指向這個實例的指針,并進行初始化?操作,一旦使用完畢,清空它的內容并釋放它指向的內存。

讓我們看看一個基本的例子:

var ptr = UnsafeMutablePointer<CChar>.alloc(10)

ptr.initializeFrom([CChar](count: 10, repeatedValue: 0))

// 對對象進行一些操作
ptr[3] = 42

ptr.destroy() //清理

ptr.dealloc(10) //釋放內存

這里我們使用 alloc(num: Int) 分配長度為 10 的 CChars (UInt8) 內存塊,這等同于調用 malloc 方法分配指定長度的內存,然后將內容轉換成我們需要的特定類型。前一種方法會避免更少的錯誤,因為我們不用去手動指定總體長度。

一旦 UnsafeMutablePointer 被分配一塊內存后,我們必須初始化這個可變的對象,使用 initialize(value: Memory)initializeFrom(value: SequenceType) 方法指定初始內容。當操作對象完畢,我們想釋放分配的內存資源,首先會使用 destroy 清空內容,然后調用 dealloc(num: Int) 方法釋放指針。

必須指出,Swift 運行時不負責清空內容和釋放指針,因此為一個變量分配內存之后,一旦使用完畢,你還要肩負起釋放內存的責任。

讓我們看看另外一個例子,這次指針指向是一個復雜的 Swift 值類型:

var ptr = UnsafeMutablePointer<String>.alloc(1)
sptr.initialize("Test String")

print(sptr[0])
print(sptr.memory)

ptr.destroy()
ptr.dealloc(1)

包括分配/初始化和清理/析構化 2 個階段的系列操作,對于值類型和引用類型來說是一樣的。但是如果你仔細研究,你會發(fā)現(xiàn)對于相同的值類型(比如整型,浮點數(shù)或者一些簡單結構體),初始化過程并非必須,你可以通過 memory 屬性或者下標來進行初始化。

但是這種方式不適用指針指向一個類,或某些特定的結構體和枚舉的情況。必須進行初始化操作,這是為什么呢?

當你使用上面提及的方式修改內存內容,從內存管理角度來說,有關這種行為背后的原因和發(fā)生時有關的。讓我們來看一個不需要手動初始化內存的代碼片段,倘若我們在沒有初始化 UnsafePointer 情況下改變了指針指向的內存,會引發(fā)崩潰。

struct MyStruct1{
    var int1:Int
    var int2:Int
}

var s1ptr = UnsafeMutablePointer<MyStruct1>.alloc(5)

s1ptr[0] = MyStruct1(int1: 1, int2: 2)
s1ptr[1] = MyStruct1(int1: 1, int2: 2) // 似乎不應該是這樣,但是這能夠正常工作

s1ptr.destroy()
s1ptr.dealloc(5)

這里沒有問題,可以使用,讓我們看看其他例子:

class TestClass{
    var aField:Int = 0
}

struct MyStruct2{
    var int1:Int
    var int2:Int
    var tc:TestClass // 這個字段是引用類型
}

var s2ptr = UnsafeMutablePointer<MyStruct2>.alloc(5)
s2ptr.initializeFrom([MyStruct2(int1: 1, int2: 2, tc: TestClass()),   
                      MyStruct2(int1: 1, int2: 2, tc: TestClass())]) // 刪除這行初始化代碼將引發(fā)崩潰

s2ptr[0] = MyStruct2(int1: 1, int2: 2, tc: TestClass())
s2ptr[1] = MyStruct2(int1: 1, int2: 2, tc: TestClass())

s2ptr.destroy()
s2ptr.dealloc(5)

這段代碼的作用已在前面的指針操作章節(jié)進行了相關解釋,MyStruct2 包含一個引用類型,所以它的生命周期交由 ARC 管理。當我們修改其中一個指向的內存模塊值的時候,Swift 運行時將試圖釋放之前存在的對象,由于這個對象沒有被初始化,內存存在垃圾,你的應用將會崩潰。

請牢記這一點,從安全的角度來講,最受歡迎的初始化手段是使用 initialize 分配完成內存后,直接設置變量的初始值。

另外一個方法來自與本節(jié)最開始的一個提示,導入標準 C 庫(Darwin 或者 Linux 下的 Glibc),然后使用 malloc 系列函數(shù):

var ptr = UnsafeMutablePointer<CChar>(malloc(10*strideof(CChar)))

ptr[0] = 11
ptr[1] = 12

free(ptr)

你可以看到,我們并沒有使用之前推薦的方法來初始化實例,那是因為我們在最近的一節(jié)中注明了,類似 CChar 和一些基本結構體,更適合使用這種方式。

接下來讓我們看看兩個附加的例子來講解兩個常用的函數(shù):memcpymmap

var val = [CChar](count: 10, repeatedValue: 1)
var buf = [CChar](count: val.count, repeatedValue: 0)

memcpy(&buf, &val, buf.count*strideof(CChar))
buf // [1,1,1,1,1,1,1,1,1,1]

let ptr = UnsafeMutablePointer<Int>(mmap(nil, 
                                        Int(getpagesize()), 
                                        PROT_READ | PROT_WRITE, 
                                        MAP_ANON | MAP_PRIVATE, 
                                        -1, 
                                        0))

ptr[0] = 3

munmap(ptr, Int(getpagesize()))

這段代碼和你使用 C 語言做的類似,請注意你可以使用 getpagesize() 輕松地獲取內存頁的大小。

第一個例子展示我們可以使用 memcpy 來設置內存,第二個例子展示了一個真實的用例,提供一個可選的內存分配方法,在這里我們映射了一個新的內存頁,但是我們只是映射了一個特定的內存區(qū)域或者說一個特定的文件指針,在這案例中,我們可以不用初始化直接訪問這里之前存在的內容。

讓我們接下來看看來自 SwiftyGPIO 中真實的案例, 在這里我映射了一個內存區(qū)域, 包含了樹莓派的數(shù)字 GPIO 的注冊,將會被用到貫穿到整個庫的讀取和寫入值的情況。

// BCM2708_PERI_BASE = 0x20000000
// GPIO_BASE = BCM2708_PERI_BASE + 0x200000 /* GPIO controller */
// BLOCK_SIZE = 4*1024

private func initIO(id: Int){
    let mem_fd = open("/dev/mem", O_RDWR|O_SYNC)
    guard (mem_fd > 0) else {
        print("Can't open /dev/mem")
        abort()
    }

    let gpio_map = mmap(
        nil,
        BLOCK_SIZE,           // Map length
        PROT_READ|PROT_WRITE, // Enable read/write
        MAP_SHARED,           // Shared with other processes
        mem_fd,               // File to map
        GPIO_BASE             // Offset to GPIO peripheral
        )

    close(mem_fd)

    let gpioBasePointer = UnsafeMutablePointer<Int>(gpio_map)
    if (gpioBasePointer.memory == -1) {    //MAP_FAILED not available, but its value is (void*)-1
        print("mmap error: " + String(gpioBasePointer))
        abort()
    }
    
    gpioGetPointer = gpioBasePointer.advancedBy(13)
    gpioSetPointer = gpioBasePointer.advancedBy(7)
    gpioClearPointer = gpioBasePointer.advancedBy(10) 

    inited = true
}

當映射從 0x20200000 開始的 4KB 區(qū)域后,我們獲得三個感興趣的寄存器地址,之后可以通過內存屬性來讀取或者寫入這些值了。

<a name="pointer_arithmetic"></a>

指針計算

使用指針運算來移動序列或者獲取一個復雜變量特定成員的引用,在 C 語言中非常常見,我們可以在 Swift 做到嗎?

當然可以,UnsafePointer 和它的可變變量,提供了一些方便的方法,允許像 C 語言那樣對指針使用增加或者修改的計算操作:
successor() , predecessor() , advancedBy(positions:Int)distanceTo(target:UnsafePointer<T>)

var aptr = UnsafeMutablePointer<CChar>.alloc(5)
aptr.initializeFrom([33,34,35,36,37])

print(aptr.successor().memory) // 34
print(aptr.advancedBy(3).memory) // 36
print(aptr.advancedBy(3).predecessor().memory) // 35

print(aptr.distanceTo(aptr.advancedBy(3))) // 3

aptr.destroy()
aptr.dealloc(5)

但是說老實話,即使我提前展示了這些方法,并且這些是我推薦給你使用的方法,但是還是可以增加或者減少一個 UnsafePointer (不是很 Swift 化),來得到指針從而獲得序列中的其他元素:

print((aptr+1).memory) // 34
print((aptr+3).memory) // 36
print(((aptr+3)-1).memory) // 35

GitHub或者zipped獲取 Swift/C 混合編碼的 playground。

<a name="working_with_strings"></a>

字符串操作

我們現(xiàn)在已經(jīng)知道,當一個 C 函數(shù)有一個 char 指針的參數(shù)時,這個參數(shù)將在 Swift 被轉換成 UnsafePointer<Int8> ,但是自從 Swift 可以自動地將字符串轉換 UTF8 緩存的指針后,你也可以使用字符串作為指針調用這些函數(shù),而不需要提前手動進行轉換。

另外,如果你在調用一個需要 char 指針的函數(shù)之前,需要對這個指針進行附加的操作,Swift 的字符串提供了 withCString 方法,傳入一個 UTF8 字符緩存給一個閉包,這個閉包返回一個可選值。

puts("Hey! I was a Swift string!") // 傳入 Swift 字符串到 C 函數(shù)中

var testString = "AAAAA"

testString.withCString { (ptr: UnsafePointer<Int8>) -> Void in
    // Do something with ptr
    functionThatExpectsAConstCharPointer(ptr)
}

可以直接把一個 C 字符串轉換成一個 Swift 字符串,只需要使用 String 靜態(tài)方法 fromCString ,需要注意的是,C 字符串必須有空終止字符串。(譯者注:字符串以 "\0" 結束)。

let swiftString = String.fromCString(aCString)

如果你想在 Swift 中植入一些 C 代碼,用來處理字符串,比如處理用戶輸入,你可能有需求比較字符串中每個字符和一個單獨的 ASCII碼或者一個ASCII返回,這些操作,能在把字符串設計為結構體的 Swift 代碼中實現(xiàn)嗎?

答案是肯定的,但是我不在這里對 Swift 的字符串展開深入的探討,如果你想學到更多關于 Swift 是結構體的知識點,請查看Ole BegemannAndy Bargh的文章獲取更多的知識。

下面看一個例子,我們定義了一個函數(shù),判斷一個字符串是否只由基本可以打印的 ASCII 字符組成,這樣我們可以在 C 的代碼中使用這個字符串:

func isPrintable(text:String)->Bool{
    for scalar in text.unicodeScalars {
        let charCode = scalar.value
        guard (charCode>31)&&(charCode<127) else {
            return false // Unprintable character
        }
    }
    return true
}

在 C 中,字符整型值和一個 ASCII 組成的字符串中的每個字符之間的比較,換到 Swift 代碼中并沒有改變很多,是使用的每個字符串的 unicode 值進行的比較。需要注意的是。需要明確的是,這個方法只能在字符串是由單個標量單位支持時候有用,不是通用的。

那么在字符和他們的數(shù)字 ascii 值之間如何進行轉換呢?

為了轉換一個數(shù)字為對應的 字符 或者 字符串 時,我們首先要把它轉換成 UnicodeScalar ,然后更加緊湊的方式是使用 UInt8 提供的特定的構造函數(shù):

let c = Character(UnicodeScalar(70))   // "F"

let s = String(UnicodeScalar(70))      // "F"

let asciiForF = UInt8(ascii:"F")       // 70

上面例子中的 guard 語句可以改成 UInt8(ascii:) 增加可讀性。

<a name="working_with_functions"></a>

函數(shù)操作

在字符串一節(jié)我們可以看到,Swift 自動將作為參數(shù)的 C 函數(shù)指針變成閉包,但是有一個主要的缺點是,閉包被用作 C 函數(shù)指針參數(shù)時,不能捕獲任何在上下文外的值。

為了對此進行約束,這種類型的閉包(這種閉包是從 C 函數(shù)指針轉換而來),被自動的加上一個特定特定類型屬性@convention(c), 在 Swift 語言參考中類型屬性章節(jié)中有詳細描述,表示調用時候閉包必須遵從的約定,可能的值有: c , objcswift

另外存在一個可選的方案來解決這個限制,在 Chris Eidhof 的這篇文章中可以看到,使用一個基于代碼塊(block-based)函數(shù),如果你是在一個基于 Darwin 的系統(tǒng)上調用一個函數(shù)就會有一個代碼塊的變量,傳入一個保持環(huán)境的對象到函數(shù)中,同時遵守了常見的 C 模式。

接下來我們簡要說說可變參數(shù)函數(shù)。

Swift 不支持傳統(tǒng)的 C 可變參數(shù)函數(shù),可以肯定的是,在你第一次試圖調用類似于printf之類的可變參數(shù)函數(shù)時,Swift 將在編譯時就報錯。如果你真的需要調用它們,唯一可行的方案是創(chuàng)建一個 C 的包裹函數(shù),限制參數(shù)的數(shù)量或者使用va_list(Swift 支持)來間接接受多個參數(shù)。

所以,即使 printf 不能工作,但是 vprintf 或者其他支持 va_list 的函數(shù)可以在 Swift 中工作。

為了把數(shù)組參數(shù)或者一個可變的 Swift 參數(shù)列表轉換為 va_list 指針,每一個參數(shù)必須實現(xiàn) CVarArgType ,然后你只需要調用 withVaList 來獲取 CVaListPointer ,這個指針指向你的參數(shù)列表( getVaList 也可以用但是文檔推薦盡量不使用它)。讓我們看看一個使用 vprintf 的例子:

withVaList(["a", "b", "c"]) { ptr -> Void in
    vprintf("Three strings: %s, %s, %s\n", ptr)
}

<a name="unmanaged"></a>

Unmanaged

我們已經(jīng)或多或少了解有關指針的知識點,但仍然不可避免存在一些我們已知卻無法處理的事項。

如果我們把一個 Swift 引用對象作為參數(shù),傳遞給一個在回調中返回結果的函數(shù)中,會怎么樣呢?我們能保證,在切換上下文時,Swift 對象仍然在哪里,而 ARC 沒有釋放它嗎?答案是不能,我們不能做假設,這個對象仍然存在在哪里。

使用 Unmanaged ,使用一個帶有一些有趣的工具方法的類,來解決上面我們提到的情況。帶有 Unmanaged 你可以改變對象的引用計數(shù),在你需要它的時候轉換為COpaquePointer。

讓我們來看一個實際的案例,這里有一個前面我們描述有這個特性的 C 函數(shù):

c
// cstuff.c
void aCFunctionWithContext(void* ctx, void (*function)(void* ctx)){
    sleep(3);
    function(ctx);
}

然后使用 Swift 代碼來調用它:

class AClass : CustomStringConvertible {
    
    var aProperty:Int=0

    var description: String {
        return "A \(self.dynamicType) with property \(self.aProperty)"
    }
}

var value = AClass()

let unmanaged = Unmanaged.passRetained(value)
let uptr = unmanaged.toOpaque()
let vptr = UnsafeMutablePointer<Void>(uptr)

aCFunctionWithContext(vptr){ (p:UnsafeMutablePointer<Void>) -> Void in
    var c = Unmanaged<AClass>.fromOpaque(COpaquePointer(p)).takeUnretainedValue()
    c.aProperty = 2
    print(c) //A AClass with property 2
}

使用 passRetainedpassUnretained 方法, Unmanaged 保持了一個給定的對象,對應的增加或者不增加它的引用計數(shù)。

因為回調需要一個 void 指針,我們首先使用 toOpaque() 獲取 COpaquePointer ,然后把它轉換為 UnsafeMutablePointer<Void> 。

在回調中,我們做了相反的轉換,獲取到指向原始類的引用,然后修改它的值。

我們從未管理的對象提取出類,我們可以使用 takeRetainedValue 或者 takeUnretainedValue ,使用上面描述的相似的手法,對應地減少或者取消未修改的值的引用計數(shù)。

在這個例子中,我們沒有減少引用計數(shù),所以即使跳出了閉包的范圍,這個類也不會被釋放。這個類將通過未管理的實例中進行手動釋放。

這只是一個簡單的,或許不是最好的案例,用來表示 Unmanaged 可以解決的一系列問題,想要獲取更多的Unmanaged信息,請查看 NSHipster 的文章。

<a name="working_with_files"></a>

文件操作

在一些平臺上,我們可以直接使用標準 C 語言庫中的函數(shù)處理文件,讓我們看看一些讀取文件的例子吧:

let fd = fopen("aFile.txt", "w")
fwrite("Hello Swift!", 12, 1, fd)

let res = fclose(file)
if res != 0 {
    print(strerror(errno))
}

let fd = fopen("aFile.txt", "r")
var array = [Int8](count: 13, repeatedValue: 0)
fread(&array, 12, 1, fd)
fclose(fd)

let str = String.fromCString(array)
print(str) // Hello Swift!

從上面的代碼你可以看到,關于文件訪問沒有什么奇怪的或者復雜的操作,這段代碼和你使用 C 語言編碼是差不多的。需要注意的是我們可以完全獲取錯誤信息和使用相關的函數(shù)。

<a name="bitwise_operations"></a>

位操作

當你和 C 進行互調時候,有很大的可能會進行一些位操作,我推薦一篇之前寫的文章,覆蓋到了這方面你想了解的知識點。

<a name="swift_and_c_mixed_projects"></a>

Swift 和 C 的混合項目

Swift 項目可以使用一個橋接的頭文件來訪問 C 庫, 這個做法與使用 Objective-C 庫是類似的。

但是這種方法不能用在框架項目中,所以我們采用一個更通用的替代方法,不過需要一些簡單的配置。我們將創(chuàng)建一個 LLVM 模塊,其中包含一些我們要導入到 Swift 的 C 代碼。

假設我們已經(jīng)在 Swift 項目中添加了 C 代碼的源文件:

c
//  CExample.c
#include "CExample.h"
#include <stdio.h>

void printStuff(){
    printf("Printing something!\n");
}

void giveMeUnsafeMutablePointer(int* param){ }
void giveMeUnsafePointer(const int * param){ }

和對應的頭文件:

c
//  CExample.h
#ifndef CExample_h
#define CExample_h

#include <stdio.h>
#define IAMADEFINE 42

void printStuff();
void giveMeUnsafeMutablePointer(int* param);
void giveMeUnsafePointer(const int * param);

typedef struct {
    char name[5];
    int value;
} MyStruct;

char name[] = "IAmAString";
char* anotherName = "IAmAStringToo";

#endif /* CExample_h */

為了區(qū)分 C 源代碼和其他代碼,我們在項目根目錄中建立了 CExample 文件夾,把 C 代碼文件放到里面。

我們必須在這個目錄下創(chuàng)建一個 module.map 文件,然后這個文件定義了我們導出的 C 模塊和對應的 C 頭文件。

c
module CExample [system] {
    header "CExample.h"
    export *
}

你可以看到,我們導出了頭文件定義的所有內容,其實模塊可以在我們需要的時候部分導出。

此外,這個例子中實際的庫文件源碼已經(jīng)包含在項目中了,但是如果你想導入一個在系統(tǒng)中存在的庫到 Swift 中的話,你只需要創(chuàng)建一個 module.map (不需要在源碼的目錄下創(chuàng)建),然后指定頭文件或者系統(tǒng)的頭文件。只是你需要在 modulemap 文件中使用link libname指令指定這個庫的頭文件名和具體的庫的關聯(lián)關系(和你手動使用 -llibname 一樣去鏈接這個庫)。然后你也可以在一個 module.map 中定義多個模塊。

想學習更多的關于 LLVM 模塊和所有選項的信息,請查看官方文檔。

最后一步是把模塊目錄添加到編譯器的查詢路徑中。你需要做的是,打開項目屬性配置項,在 Swift Compiler - Search Paths 下的 Import Paths 中添加模塊路徑(${SRCROOT}/CExample

然后就這樣,我們可以導入這個 C 模塊到 Swift 代碼中,然后使用其中的函數(shù)了:

import CExample

printStuff()
print(IAMADEFINE) //42

giveMeUnsafePointer(UnsafePointer<Int32>(bitPattern: 1))
giveMeUnsafeMutablePointer(UnsafeMutablePointer<Int32>(bitPattern: 1))

let ms = MyStruct(name: (0, 0, 0, 0, 0), value: 1)
print(ms)

print(name) // (97, 115, 100, 100, 97, 115, 100, 0)
//print(String.fromCString(name)!) // Cannot convert it

print(anotherName) //0xXXXXXX pointer address
print(String.fromCString(anotherName)!) //IAmAStringToo

<a name="closing_thoughts"></a>

結束語

我希望這篇文章至少能夠給你帶來心中對于探索 Swift 和 C 交互這個未知世界的一些光亮,但是我也不是期望能夠把你在項目過程中遇到的問題都解決掉。

你也會發(fā)現(xiàn),想把事情按照預期的方向進行,你需要多做一些實驗。在下個版本的 Swift 中(譯者注:指 Swift 3.0),與 C 的互調會變得更強。(在 Swift 2.0 才引入的 UnsafePointer 和相關的函數(shù),在這之前,和 C 的互調有一些困難)

用一個提示作為結束,關于 Swift Package Manager 和支持 Swift/C 混編項目,自動生成 modulemaps 來支持導入 C 模塊的一個 pr 在昨天進行了合并操作,閱讀這篇文章可以看到它如何進行工作。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權,最新文章請訪問 http://swift.gg。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容