作者: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 混編的項目。
內容介紹
<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ù)而使用 strideof
和 strideOfValue
方法,讓我們通過一個例子來看看 sizeof
和 strideof
返回的區(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ī)則,不同的處理器架構下,strideof
和 sizeof
之間返回的值會有所不同,一個附加的工具函數(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ù) 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)
})
這個函數(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ù):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
來設置內存,第二個例子展示了一個真實的用例,提供一個可選的內存分配方法,在這里我們映射了一個新的內存頁,但是我們只是映射了一個特定的內存區(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
<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 Begemann和Andy 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
, objc
和 swift
。
另外存在一個可選的方案來解決這個限制,在 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
}
使用 passRetained
和 passUnretained
方法, 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。