Swift 3.0 Unsafe World • meron@tech

翻译自:meron@tech :http://technology.meronapps.com/2016/09/27/swift-3-0-unsafe-world-2/?utm_source=Swift_Developments&utm_medium=email&utm_campaign=Swift_Developments_Issue_58

像大多数现代语言一样,Swift,让你生活在一个幸福的编程世界中,这是由于所有的内存都由一个外部元素管理,它可能是由编译器/运行时管理像Swift,或者略微差一点依赖于垃圾收集器。 对于隐藏在语言中来来去去的引用实例,很少需要处理这些问题。

然而,为了给Swift添加更多的功能,你可能需要调用一个乱糟糟的C API像OpenGL或POSIX函数,在这些情况下,你将需要自己处理某些事情,这些事让我们很头痛,是的,我说的就是指针以及在堆中手动分配内存问题。

在3.0之前,Swift 的Unsafe API有点混乱,多种路径都能实现相同的结果,这导致你止于复制和粘贴这些问题到stackoverflow中,而不真正明白发生了什么。 在3.0中一切都改变了,它已经变得更好。

在本文中,我不会写如何从Swift 2.x迁移到3.0代码,而是写在3.0中这些事情是如何工作的,并且会与C进行比较,因为通常使用不安全引用的主要目的是与低级C API交互。

让我们开始做最简单的操作,分配一个整数到内存。

在C中,你会做这样的事情:

int *a = malloc(sizeof(int));

*a = 42;

printf("a's value: %d", *a);

free(a)

相同的事情 在 Swift 中:

let a = UnsafeMutablePointer.allocate(capacity: 1)

a.pointee = 42

print("a's value: \(a.pointee)") // 42

a.deallocate(capacity: 1)

我们在Swift中看到的第一个类型是 UnsafeMutablePointer, 这个通用结构表示一个指向T类型的指针,如你所见,它有一个静态方法,allocate 它存储 capacity 元素.

你可以想象, 有一个 UnsafeMutablePointer 变量 UnsafePointer 不允许你改变指针. 此外, 不可变的 UnsafePointer  甚至没有 allocate 方法.

在swift中,还有另一种方法来生成Unsafe[Mutable]Pointer, 它是通过使用 & 运算符. 当将参数传递给块或函数时, 你可以使用 & 来获取指针. 让我们看一个例子

func receive(pointer: UnsafePointer ) {

print("param value is: \(pointer.pointee)") // 42

}

var a: Int = 42

receive(pointer: &a)

& 操作运算需要类型为 var, 但它会给出你需要的所有情况. 你可以采用一个可变引用,甚至改变它,例如:

func receive(mutablePointer: UnsafeMutablePointer) {

mutablePointer.pointee *= 2

}

var a = 42

receive(mutablePointer: &a)

print("A's value has changed in the function: \(a)") // 84

第一个例子和最后一个例子之间有一个重要的区别。 在第一个中,我们手动分配内存,(我们需要手动分配它),而带函数的这个例子,我们是从Swift分配的内存中创建指针。 显然,管理存储器和获取指针是两个不同的问题。 在最后一部分,我们将讨论内存管理。

但是,我们如何才能只用指针指向一个Swift管理的内存,而不创建一个函数呢?要做到这一点,我们要使用 withUnsafeMutablePointer, 它会引用一个 Swift 类型和一个指针作为参数的块。让我们看看如何实现:

var a = 42

withUnsafeMutablePointer(to: &a) { $0.pointee *= 2 }

print("a's value is: \(a)") // 84

现在我们知道了这些, 我们可以着手准备在它的参数中使用指针调用 C APIs, 让我们看一个使用 POSIX 函数 opendir / readdir 来列出当前目录内容的例子.

var dirEnt: UnsafeMutablePointer?

var dp: UnsafeMutablePointer

?

let data = ".".data(using: .ascii)

data?.withUnsafeBytes({ (ptr: UnsafePointer) in

dp = opendir(ptr)

})

repeat {

dirEnt = readdir(dp)

if let dir = dirEnt {

withUnsafePointer(to: &dir.pointee.d_name, { ptr in

let ptrStr = unsafeBitCast(ptr, to: UnsafePointer.self)

let name = String(cString: ptrStr)

print("\(name)")

})

}

} while dirEnt != nil

在指针之间转换

当处理C语言的API时,有时需要将指向结构体的指针转换为不同的结构体。 在C语言中很容易做到(非常危险和容易出错),但正如你在Swift中看到的,所有的指针都是类型化的,这意味着  UnsafePointer 不能用在需要 UnsafePointer 的地方, 这样的好处是代码足够安全, 但同时使得不可能和需要这种类型转换的 C APIs进行交互 , 例如接口 bind() 函数. 对于这种情况, 我们将使用 withMemoryRebound 它是一个将指针从一个类型转换为另一个类型的函数, 让我们看看我们如何在 bind 函数中转换, 通常创建一个 sockaddr_in 结构体 然后转换为 sockaddr

var addrIn = sockaddr_in()

// Fill sockaddr_in fields

withUnsafePointer(to: &addrIn) { ptr in

ptr.withMemoryRebound(to: sockaddr.self, capacity: 1, { ptrSockAddr in

bind(socketFd, UnsafePointer(ptrSockAddr), socklen_t(MemoryLayout.size))

})

}

在转换指针时有一种特殊情况,一些 C APIs 需要传递一个 void* 指针. 在Swift 3.0之前,你可以使用 UnsafePointer 实现, 但是在3.0中添加了一个新类型来处理这些类型的指针: UnsafeRawPointer. 这个结构不是通用的  所以它意味着它不会将信息与任何类型绑定,并简化我们的代码。要创建 UnsafeRawPointer 我们可以在其构造函数中包装一个现有的指针。如果我们想要反过来, 将 UnsafeRawPointer 转换为特定类型的指针,我们需要使用上一个示例的 withMemoryRebound 但在这种情况下,它被称为 assumingMemoryBound.

let intPtr = UnsafeMutablePointer.allocate(capacity: 1)

let voidPtr = UnsafeRawPointer(intPtr)

let intPtrAgain = voidPtr.assumingMemoryBound(to: Int.self)

指针作为数组        直到这里,我们已经涵盖了指针的典型用法,你可以处理大多数C API调用,但指针可以用于更多的用途,其中之一就是遍历一块存有数据的内存. 在Swift中,我们有几种方法来做这样的事。 实际上  UnsafePointer 有一个 advanced(by:) 方法,允许你遍历内存, advanced(by:)返回另一个 UnsafePointer 让我们可以存储或读取.

let size = 10

var a = UnsafeMutablePointer.allocate(capacity: size)

for idx in 0..<10 {

a.advanced(by: idx).pointee = idx

}

a.deallocate(capacity: size)

除此之外,Swift有另一个结构,使这个变得更容易,我们来说说 UnsafeBufferPointer. 这个结构是Swift数组和指针之间的桥梁。如果我们从 UnsafePointer 构造出一个 UnsafeBufferPointer 我们将能够使用原生Swift类型的大部分数组函数,因为 UnsafeBufferPointer 实现了 Collection, Indexable and RandomAccessCollection swift 协议. 我们可以这样遍历内存:

// Using a and size from previous code

var b = UnsafeBufferPointer(start: a, count: size)

b.forEach({

print("\($0)" // Prints 0 to 9 that we fill previously

)})

当我们说 UnsafeBufferPointer 是 Swift 数组的桥梁时, 意味着我们可以很容易的从现有的数组中取出一个 UnsafeBufferPointer 类似的例子如下:

var a = [1, 2, 3, 4, 5, 6]

a.withUnsafeBufferPointer({ ptr in

ptr.forEach({ print("\($0)") }) // 1, 2, 3...

})

内存管理危险

我们已经看到了很多方法来引用原始内存,但我们不能忘记,我们正进入一个危险的地带。 重复出现的 Unsafe 关键词警告我们要小心使用它. 此外,我们在使用不安全引用的时候混合了两个世界. 让我们用一个例子来看看可能的危险:

var collectionPtr: UnsafeMutableBufferPointer?

func duplicateElements(inArray: UnsafeMutableBufferPointer) {

for i in 0..

inArray[i] *= 2

}

}

repeat {

var collection = [1, 2, 3]

collection.withUnsafeMutableBufferPointer({ collectionPtr = $0 })

} while false

duplicateElements(inArray: collectionPtr!) // Crash due to EXC_BAD_ACCESS

虽然这个示例不是真实的, 但它说明了在更复杂的代码中当使用指针快速分配变量时会发生什么。 这里, collection 是在块中创建的, 因此, 引用将在块结束后释放. 我们有意的保留了在原 collection 之后使用 collectionPtr 中的引用,因此它在尝试在 duplicateElements(inArray:)中使用它后崩溃。 如果我们想要使用swift分配的元素的指针,我们需要确保它们在我们想要使用它们时可用。 请记住,ARC将向离开作用域的任何引用添加一个标示,如果它在任何其他地方没有强引用,它将被释放。

克服Swift内存管理的一个解决方案是自己分配内存,就像我们在本文的一些示例中所做的,这消除了访问无效引用的问题,但它引入了另一个问题。 如果我们不手动释放我们分配的程序将有内存泄漏。

bitPattern用于具有固定值的指针

在结束这篇文章前,我想再介绍几个在Swift中使用指针的方法。其中很有用的一个就是在 C APIs 中使用带有数值的 void* 指针而不是一个内存地址. 通常这种情况发生在一个函数接受不同类型的参数时并且是一个带有 void*指针的值 如下所示:

void generic_function(int value_type, void* value);

generic_function(VALUE_TYPE_INT, (void *)2);

struct function_data data;

generic_function(VALUE_TYPE_STRUCT, (void *)&data);

如果我们想使用swift的第一个函数调用,我们需要使用一个特殊的构造函数,它将允许我们创建一个指向指定地址的指针。 我们所看到的所有函数都不允许你改变引号的地址,所以在这种情况下我们将使用 UnsafePointer(bitPattern:) .

generic_function(VALUE_TYPE_INT, UnsafeRawPointer(bitPattern: 2))

var data = function_data()

withUnsafePointer(to: &data, { generic_function(VALUE_TYPE_STRUCT, UnsafeRawPointer($0)) } )

不透明指针

最后我想说说有关于Swift类型中的透明指针. 象 userData 这样的参数在 C API 函数中非常普遍,用户数据将是一个 void* 指针, 它将保存一个随后将被使用的任意内存值。 一个常见的用例是处理函数时设置一些回调,当事件发生时将被调用。 在这种情况下,传递一个对Swift对象的引用将是有用的,所以我们可以从C回调调用它的方法。

我们可以使用一个常规的 UnsafeRawPointer 就像我们在文章中其它部分看到的 然而,正如我们也看到的,这样做可能会有内存管理的问题。如果我们传递给C语言函数指向一个我们以前不保留的对象的指针,它可以被释放并且程序将崩溃。

Swift有一个实用程序用来调节指向对象的指针,可以根据我们的需要保留或不保留对对象的引用。 这就是带有 passRetained() 参数的 Unmanaged 静态函数。我们将创建一个带有引用的对象, 所以我们可以确定当C语言使用它时, 它仍然存在. 在对象在回调的生命周期内已经被保留,我们也可以使用 passUnretained(). 两种方法都产生一个 Unmanaged的实例,通过调用 toOpaque() 函数将它转换为 UnsafeRawPointer 。

另一方面,我们可以使用反向api fromOpaque() and takeRetained() or  takeUnretained() 来将一个 UnsafeRawPointer 转换为类或结构体实例。

void set_callback(void (*functionPtr)(void*), void* userData));

struct CallbackUserData {

func sayHello() { print("Hello world!" ) }

}

func callback(userData: UnsafeMutableRawPointer) {

let callbackUserData = Unmanaged.fromOpaque(userData).takeRetainedValue()

callbackUserData.sayHello() // "Hello world!"

}

var userData = CallbackUserData()

let reference = Unmanaged.passRetained(userData).toOpaque()

set_callback(callback, reference)

总结

如你所见, 从Swift调用C代码是完全可行的,利用我们知道的工具,它很容易实现,不需要很多代码. Unsafe 和 Unmanaged API 比我在这篇文章里介绍的强大得多,但是我希望这个是你更深入了解的良好基础,倘若你有兴趣或需要。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容