Swift 和 C 不得不說的故事

BeuMoll 7年前發布 | 18K 次閱讀 Swift RxSwift

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

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

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

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

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

內容介紹

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 類型。你會注意到,大多數從 C 轉換到 Swift 的類型,都是簡單地使用了常用的 Swift 固定大小的類型,而這些類型,你應該已經相當熟悉了。

數組和結構體

讓我們接下來聊聊復合數據結構:數組和結構體。

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

//header.h

char name[] = "IAmAString";

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

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

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

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

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

比如,有以下的結構體:

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

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

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

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

枚舉

如果你需要使用 Swift 訪問 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 中使用一個特定的宏定義的話,我們將得到我們想要的結果:

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

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

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

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

另外, Swift 提供了 NS_OPTIONS 宏定義,用于定義一個可選項集合,遵從 OptionSetType 協議(目前為 OpertionType )。

聯合體

接下來讓我們看看聯合體,一個有趣的 C 類型,在 Swift 中沒有對應的數據結構。

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

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

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

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

由于在 Swift 中,沒有類似的數據結構與聯合體對應,所以這種類似將在 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)

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

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

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

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

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

到目前為止我們已經學習了聯合體的行為,那么我們能在 Swift 中手動實現一個類似的結構體嗎?

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

關于長度的那些事

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

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

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

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

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

Null, nil 和 0

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

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

宏定義

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

#define MY_CONSTANT 42

將被轉換成:

let MY_CONSTANT = 42

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

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

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

puts("Hello!")

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

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

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

指針操作

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

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

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

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

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

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

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

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

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

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

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

myIntPointer.memory = 42

myIntPointer[0] = 43

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

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

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

let i = 42
functionThatNeedsAPointer(&i)

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

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)
})

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

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

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

// ptr is an untyped COpaquePointer

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

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

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

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

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

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

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

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

內存分配

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

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

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

指針將根據我們具體的操作在這 3 個狀態之間進行轉換。

大多數情況下,推薦你使用 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 個階段的系列操作,對于值類型和引用類型來說是一樣的。但是如果你仔細研究,你會發現對于相同的值類型(比如整型,浮點數或者一些簡單結構體),初始化過程并非必須,你可以通過 memory 屬性或者下標來進行初始化。

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

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

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())]) // 刪除這行初始化代碼將引發崩潰

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

s2ptr.destroy()
s2ptr.dealloc(5)

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

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

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

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

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

free(ptr)

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

接下來讓我們看看兩個附加的例子來講解兩個常用的函數: memcpy 和 mmap :

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

讓我們接下來看看來自 SwiftyGPIO 中真實的案例, 在這里我 映射了一個內存區域 , 包含了樹莓派的數字 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 區域后,我們獲得三個感興趣的寄存器地址,之后可以通過內存屬性來讀取或者寫入這些值了。

指針計算

使用指針運算來移動序列或者獲取一個復雜變量特定成員的引用,在 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

字符串操作

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

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

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

var testString = "AAAAA"

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

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

let swiftString = String.fromCString(aCString)

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

答案是肯定的,但是我不在這里對 Swift 的字符串展開深入的探討。

下面看一個例子,我們定義了一個函數,判斷一個字符串是否只由基本可以打印的 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 值進行的比較。需要注意的是。需要明確的是,這個方法只能在字符串是由單個標量單位支持時候有用,不是通用的。

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

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

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

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

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

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

函數操作

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

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

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

接下來我們簡要說說可變參數函數。

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

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

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

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

Unmanaged

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

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

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

讓我們來看一個實際的案例,這里有一個前面我們描述有這個特性的 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
}

使用 passRetained 和 passUnretained 方法, Unmanaged 保持了一個給定的對象,對應的增加或者不增加它的引用計數。

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

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

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

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

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

文件操作

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

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 語言編碼是差不多的。需要注意的是我們可以完全獲取錯誤信息和使用相關的函數。

位操作

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

Swift 和 C 的混合項目

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

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

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

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

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

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

和對應的頭文件:

//  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 */

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

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

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

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

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

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

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

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

結束語

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

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

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

 

來自:http://codebuild.me/2016/12/13/swift-and-c-everything-you-need-to-know/

 

 本文由用戶 BeuMoll 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!