Swfit进阶-14-Swift闭包的本质

245 阅读2分钟

「这是我参与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字节

image.png

  • 总结
    • 第一个索引不会改变返回的指针的类型,即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方法 image.png

  1. 首先通过swift_allocObject创建swift.refcounted结构体

  2. 然后将swift.refcounted转换为<{ %swift.refcounted, [8 x i8] }>*结构体(即Box)

  3. 取出结构体中index等于1的成员变量,存储到[8 x i8]*连续的内存空间中

  4. 内嵌函数的地址存储到i8* 即void*地址中

  5. 最后返回一个结构体

函数的结构体定义:

image.png

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)

打印结果

image.png

结论:所以当我们var makeInc = makeIncrementer()使用时,相当于给makeInc就是ClosureData结构体,其中关联了内嵌函数地址,以及捕获变量的地址,所以才能在上一个的基础上进行累加。

4. 总结

闭包本质就是一个引用类型,底层是一个结构体,保存了函数地址捕获变量的地址。