「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。
- 本文主要介绍Swift中闭包的
本质。
1. IR语法
上一篇我们通过SIL编译知道闭包捕获了变量地址,当使用变量的时候会获取捕获变量的地址。当把闭包实例化时,闭包会持有捕获的变量。那么我们通过IR代码来观察数据的构成,首先看下IR的语法。
- 数组
/*
- elementnumber 数组中存放数据的数量
- elementtype 数组中存放数据的类型
*/
[<elementnumber> x <elementtype>]
/*
24个i8都是0
- iN:表示多少位的整型,即8位的整型 - 1字节
*/
alloca [24 x i8], align 8
- 结构体
/*
- T:结构体名称
- <type list> :列表,即结构体的成员列表
*/
//和C语言的结构体类似
%T = type {<type list>}
/*
- swift.refcounted:结构体名称
- %swift.type*:swift.type指针类型
- i64:64位整型 - 8字节
*/
%swift.refcounted = type { %swift.type*, i64}
- 指针类型
<type> *
i64*//64位的整型 - 8字节
getelementptr指令 在LLVM中获取数组和结构体的成员时通过getelementptr,语法规则如下:
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
这里我们看 LLVM 官网当中的一个例子:
struct munger_struct {
int f1;
int f2;
};
void munge(struct munger_struct *P) {
P[0].f1 = P[1].f1 + P[2].f2;
}
getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i6 getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i3
int main(int argc, const char * argv[]) { int array[4] = {1, 2, 3, 4};
int a = array[0];
return 0;
}
其中 int a = array[0] 这句对应的LLVM代码应该是这样的:
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i32 0, i32 0
//第一个0:相对于数组自身的偏移,即偏移0字节 0 * 4字节
//第二个0:相对于数组元素的偏移,即结构体第一个成员变量 0 * 4字节
- 总结
-
第一个索引不会改变返回的指针的类型,即
ptrval前面的*对应什么类型,返回的就是什么类型 -
第一个索引的
偏移量是由第一个索引的值和第一个ty指定的基本类型共同确定的 -
后面的索引是在数组或者结构体内进行索引
-
每增加一个索引,就会使得该
索引使用的基本类型和返回的指针类型去掉一层(例如 [4 x i32] 去掉一层是 i32)
-
2. IR分析
我们通过脚本把当前mian文件编译成ir代码
swiftc -emit-ir ${SRCROOT}/项目名/main.swift | xcrun swift-demangle > ./main.ll && open main.ll
运行后查看makeIncrementer方法
-
首先通过
swift_allocObject创建swift.refcounted结构体 -
然后将
swift.refcounted转换为<{ %swift.refcounted, [8 x i8] }>*结构体(即Box) -
取出结构体中
index等于1的成员变量,存储到[8 x i8]*连续的内存空间中 -
将
内嵌函数的地址存储到i8* 即void*地址中 -
最后返回一个结构体
函数的结构体定义:
3. 仿写
我们通过上面的分析可以仿写一个结构体,然后构造一个函数的结构体,将makeInc的地址绑定到结构体中
//数据结构 : 闭包的执行地址 + 捕获变量堆空间的地址
struct ClosureData<T>{
var ptr: UnsafeRawPointer
var object: UnsafePointer<T>
}
//HeapObject
struct HeapObject{
var metadata: UnsafeRawPointer
var refcount1: Int32
var refcount2: Int32
}
//Box 包含函数对象object和捕获值
struct Box<T>{
var object: HeapObject
var value: T
}
//定义的内嵌函数
struct NoMeanStruct{
var f: () -> Int
}
//转换
var f = NoMeanStruct(f: makeIncrementer())
//初始化的内存空间
let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
ptr.initialize(to: f)
//绑定内存ptr
let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1){
$0.pointee
}
print(ctx.ptr)
print(ctx.object.pointee)
打印结果
结论:所以当我们var makeInc = makeIncrementer()使用时,相当于给makeInc就是ClosureData结构体,其中关联了内嵌函数地址,以及捕获变量的地址,所以才能在上一个的基础上进行累加。
4. 总结
闭包本质就是一个引用类型,底层是一个结构体,保存了函数地址和捕获变量的地址。