Swift 和 C 不得不說的故事

做者:Umberto Raimondi,原文連接,原文日期:2016-04-07
譯者:shanks;校對:pmst;定稿:CMBhtml

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

在不一樣平臺下混合使用 Swift 和 C 的可行性,看起來是一件很是難的實踐,只有很是有限的實踐資源,固然這是和你去封裝一個原生庫對比起來看的,你能夠在你代碼運行的平臺上輕鬆地封裝一個原生庫。git

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

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

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

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

內容介紹

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 固定大小的類型,而這些類型,你應該已經至關熟悉了。

數組和結構體

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

理想的狀況下,你但願定義一個以下全局數組:

c
//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 類型。

好比,有如下的結構體:

c
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 中定義一個常見的枚舉類型:

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宏定義的枚舉(關於這個宏定義如何對應到一個經典的 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 類型:

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 個附加的函數,正確地獲得變量或者類型的長度,而且計算包括用於對齊須要的額外空間,大多數狀況下,你應該習慣替換以前的一些函數而使用 strideofstrideOfValue 方法,讓咱們經過一個例子來看看 sizeofstrideof 返回的區別:

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

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

Null, nil 和 0

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

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

宏定義

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

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<Int32>
const int * UnsafePointer<Int32>
NSDate** AutoreleasingUnsafeMutablePointer<NSDate>
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 個工具函數 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)
})

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

這種方式使得訪問變量可能引起的不安全性被限制在一個定義良好的閉包做用域中。在上面的例子中,咱們在傳遞這個參數給函數以前,把整型指針轉換爲了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 和一些基本結構體,更適合使用這種方式。

接下來讓咱們看看兩個附加的例子來說解兩個經常使用的函數: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 來設置內存,第二個例子展現了一個真實的用例,提供一個可選的內存分配方法,在這裏咱們映射了一個新的內存頁,可是咱們只是映射了一個特定的內存區域或者說一個特定的文件指針,在這案例中,咱們能夠不用初始化直接訪問這裏以前存在的內容。

讓咱們接下來看看來自 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

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

字符串操做

咱們如今已經知道,當一個 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 的字符串展開深刻的探討,若是你想學到更多關於 Swift 是結構體的知識點,請查看Ole BegemannAndy Bargh的文章獲取更多的知識。

下面看一個例子,咱們定義了一個函數,判斷一個字符串是否只由基本能夠打印的 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 語言參考中類型屬性章節中有詳細描述,表示調用時候閉包必須聽從的約定,可能的值有: cobjcswift

另外存在一個可選的方案來解決這個限制,在 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 函數:

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 保持了一個給定的對象,對應的增長或者不增長它的引用計數。

由於回調須要一個 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 代碼的源文件:

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

爲了區分 C 源代碼和其餘代碼,咱們在項目根目錄中創建了 CExample 文件夾,把 C 代碼文件放到裏面。

咱們必須在這個目錄下建立一個 module.map 文件,而後這個文件定義了咱們導出的 C 模塊和對應的 C 頭文件。

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

你能夠看到,咱們導出了頭文件定義的全部內容,其實模塊能夠在咱們須要的時候部分導出。

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

想學習更多的關於 LLVM 模塊和全部選項的信息,請查看官方文檔

最後一步是把模塊目錄添加到編譯器的查詢路徑中。你須要作的是,打開項目屬性配置項,在 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 在昨天進行了合併操做,閱讀這篇文章能夠看到它如何進行工做。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索