Kotlin Native (KN)
Kotlin/Native | Kotlin Documentation
KN是一种技术,可以将Kotlin的代码编译为原生的二进制库,无需依赖虚拟机即可运行,主要包括下面两部分的内容:
● 基于LLVM的Kotlin编译器的后端 
● Kotlin标准库的原生实现

KN 是 Kotlin Multiplatform 的一部分,KN让 Kotlin 代码可以在更多的目标平台运行。
目标平台(Target Platform) 指代码编译后最终运行的硬件和操作系统环境
| 处理器指令集架构 | x86、x86_64、arm32、arm64 |
|---|---|
| 二进制文件的组织规范 | ELF(Linux)、Mach-O(macOS/iOS)、PE(Windows) |
| 操作系统特有的接口 | 不同的系统调用接口(IO、多线程) |
编译

将 Kotlin IR 转换为 LLVM IR,最终通过LLVM生成对应平台的可执行文件。
处理器指令集架构 以及 二进制文件格式 的差异被处理
Runtime
运行时库,抹平操作系统的差异,为Kotlin标准库提供底层功能实现 

可以参考C/C++运行时库理解,可以自行搜索
编译器跟runtime是相互配合的,runtime 提供的能力,需要编译器来桥接,编译器也需要知道哪些功能可以怎么桥接才能完成编译。
Helloworld
创建一个 Hello.kt 文件,内容如下:
kotlin
interface HelloAble {
fun makeIt(): String
}
open class Hello(open var name: String) {
var helloable: HelloAble? = null
open fun greetPrefix(): String {
return "Hello"
}
fun greet(): String {
return ("${greetPrefix()} $name")
}
fun triggleInterface() {
helloable?.makeIt()
}
companion object {
fun createObj(): Hello {
return Hello("luyu")
}
}
}
open class Helloworld(override var name: String) : Hello(name) {
override fun greetPrefix(): String {
val s = super.greetPrefix()
return "$s world"
}
}
fun main() {
val h = Helloworld("cindy")
println(h.greet())
h.triggleInterface()
}
执行命令:
scala
kotlinc-native Hello.kt -g -Xno-inline -p program -o Hello
● kotlinc-native:编译Kotlin文件
● -g:带调试信息
● -p program:编译为可执行程序,还可以编译为动态库,静态库,framework等
● -o Hello:输出文件名为Hello
在我目前的Mac电脑上,生成下面两个产物: 
可以直接运行,不依赖虚拟机环境

启动过程

使用MachOView打开Hello.kexe,搜索main函数,发现函数符号名为kfun:#main(){},可以使用lldb调试看看:

查看堆栈:


需要先初始化runtime,创建参数
调到我们的main函数
对应的runtime源码

对象创建
main函数反编译结果

整体跟C++的机制比较像,成员方法编译为全局的C函数,对象实例是一个C结构体; 调用成员方法就是调用全局函数,并把对象作为第一个参数传入
通过 MachOView 可以看到编译后所有的对应的函数

我们打断点到init方法

清楚的看到有两个参数,第一个参数是this对象,第二个参数是name,分别通过 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 0 寄存器跟 x0寄存器 跟 </math>x0寄存器跟x1 寄存器传递
我们给name传的值是字符串"cindy",验证下是否正确
● x/8gx $x1 读取 $x1 地址对应的内存中的内容,g:8个字节为一组 x:以16进制显示 重复 8 次
● x/8hx 0x1000b22c0 读取0x1000b22c0地址对应的内存中的内容 h:两个字节为一组 x:以16进制显示 重复 8 次 似乎看到了ASCII 吗
● 以字符串显示,发现了确实是cindy,Kotlin每个字符大小是两个字节


编译器已经把源码中的 "cindy" 字符串编译为了放在全局数据区的一个 String 对象(全局常量),C结构体定义如下 
c
struct StringHeader {
TypeInfo* typeInfoOrMeta_; uint32_t count_;
int32_t hashCode_;
uint16_t flags_;
alignas(KChar) char data_[];
}
<math xmlns="http://www.w3.org/1998/Math/MathML"> x 1 是字符串 " c i n d y " ,编译器生成的全局常量, x1是字符串"cindy",编译器生成的全局常量, </math>x1是字符串"cindy",编译器生成的全局常量,x0是Helloworld对象吗?怎么来的?
可以看main函数中调用 init方法之前的一段代码


● 从数据段获取了Helloworld的类型的信息 kclass:Helloworld
● 调用 AllocInstance 分配内存
● 调用 UpdateStackRef 将分配好的内存的地址保存到栈上
类型信息
kclass:Helloworld 是一个地址,里面存的内容是什么?为什么创建对象需要它?
通过查看源码,对应的类型是 TypeInfo 的结构体,存储一个类的信息(类比与OC的类对象)

对应的定义如下(截图不完整):
● 这些信息本身都是在编译期的常量,编译器组织起来放在了数据段(不可变),运行时可以读取使用
● 很多跟OC类似的信息,父类型、实例大小、成员变量的便宜、遵守的协议等等

可以读取信息看下

有 3 个成员变量:objOffsetsCount_ = 3,便宜地址如下:

relativeName_ = Helloworld

看看ExtendedTypeInfo



三个变量的名字分别是:name helloable name
instanceSize_ = 0x20 占用32个字节
三个成员变量,三个指针,每个指针占用8个字节,这里为啥是32个字节?
对象

返回的对象使用ObjHeader来表示

所有的Kotlin对象,都有一个成员叫 typeInfoOrMeta_,指向自己的类对象 (类比OC中的isa指针)
小结
对象创建的流程大概如下:
● 编译器根据类的定义生成类信息(TypeInfo),并放在全局数据段(__DATA_CONST.__const)
● 代码中遇到类实例化,则将代码翻译为以下几步
● 从数据段取到类信息
● 调用 AllocInstance 分配内存,分配内存的大小 从 TypeInfo 中获取
● 并把前 8 个字节的内容填写为TypeInfo的地址
● 调用class的构造方法初始化成员变量
方法调用
初始化方法
会先调用父类的构造方法

父类设置name属性

子类设置name属性

greet方法


直接调用函数,并且把上面初始化的对象作为第一个参数传入
重载greetPrefix
查看函数,greetPrefix 有三个方法,除了父类与子类的实现个一个外,还有一个trampoline的辅助函数

打断点看到,第一个参数也是我们上面创建的对象实例

逻辑如下:
通过对象(读取前八个字节里的地址对应的内容)获取对应的类对象(TypeInfo)
类对象中有虚函数表,虚函数表里有函数地址,通过偏移读取到目标函数的地址,然后跳转到具体的函数

查看虚函数表

由于我们没有重载toString等方法的实现,所以虚函数表中对应的函数地址是父类的函数的地址。
与OC交互
重新将产物编译为 framework
scala
kotlinc-native Hello.kt -g -Xno-inline -Xstatic-framework -p framework -o Hello
得到如下产物:

可以集成到 Xcode 中运行了

生成的头文件也都可以调用了

main函数
在二进制的符号表中搜索#main,发现多了一个包装函数

objc2kotlin_kfun:#main(){} 中会调用 kfun:#main(){}

objc2kotlin_kfun:#main(){} 函数不对应任何一句原代码,完全由编译器生成
objc2kotlin前缀表示 从objc调用kotlin的函数

kfun:#main(){} 的逻辑不变(创建对象,方法调用)

成员方法
查看二进制文件符号,发现每个成员方法也生成了对应的objc2kotlin前缀的函数,例如:

在KN编译代码中找到了"objc2kotlin"

编译器如何通过生成 objc2kotlin_ 前缀的方法?
objc2kotlin_xxx方法是在OC侧调用,所以其接受的参数以及返回值都是 OC 的对象
objc2kotlin_xxx 要"复用" xxx(KN kotlin 方法) 的逻辑
根据相关位置的代码以及编译结果看,大概的流程为以下三步:
-
转换 OC对象 -> Kotlin对象(struct ObjHeader)

-
调用kotlin函数,获得Kotlin对象的返回值

-
将返回值转回 OC 对象(还有其他分支,暂不关心)

比如:


由于是init方法,返回值就是传入的self对象,所以不需要第三部转换,可以通过下面这个函数来看下返回值的情况


OC Kotlin 交互
Kotlin_ObjCExport_refFromObjC
查看源码可知是调用objc_msgSend,target是obj,SEL 是toKotlin:


任意一个对象能响应这个方法吗?
可以响应,runtime给NSObject添加了分类方法

当然对于 OC 的原生类有具体的处理:


还有一个特殊的KotlinBase的类,是 所有从Kotlin生成的OC的类的 基类
比如:HelloHello,是从Kotlin的Hello类生成的,其基类就是 KotlinBase

这个类有一个特殊的成员变量 refHolder
在toKotlin:中,返回值就是从refHolder中获取的,所以需要看下这个refHolder什么时候赋值的,其内容是什么?
情况1

● 94:分配内存,创建 OC 的实例对象(Oojb)
● 96:获取到HelloHello类对应的Kotlin Hello类的TypeInfo
○ OC的HelloHello 是 编译器根据 Hello 生成的,编译器跟runtime有办法获取到,具体获取方法暂不介绍
● 109:通过 Hello类的TypeInfo,创建一个Hello类的实例对象(Kobj),并把 Oobj 绑定到 Kobj 上
○ 绑定:通过Kobj可以轻松的(成本很低的)获取到Oobj
● 112:把 Kobj 放在 Oobj 的 refHolder 成员变量里
串一下这段代码后面发生的事情:

● alloc 过程:有两个分配内存的过程(创建了两个对象),OC 侧跟Kotlin侧个创建了一个对象,并且把两个对象绑定起来了(互相持有引用),通过其中任意一个对象,可以轻松获取到对侧的对象
● init 过程:给上述Alloc获取的对象发消息,SEL='initWithName',对应的实现是objc2kotlin_kfun:Hello#<init>(kotlin.String){}函数,函数中self对象跟NSString 通过 toKotlin: 转换为 Kotlin 侧的对象,并作为调用 kfun:Hello#<init>(kotlin.String){}函数的参数
问题
上述过程好像只是把Kobj初始化了,Oobj初始化成功了吗?
Oobj有成员变量吗?

output:
ini
ivar count = 0; property count = 0; method count = 8;
其实HelloHello这个类没有实例变量,其父类有一个成员变量:refHolder

可以认为OC 侧的对象是 Kotlin侧对象的一个包装,自己不保存任何数据,对OC 侧做的操作(函数调用)都完全转发给Kotlin侧来实现
● OC函数是对Kotlin函数的包装
● OC对象是对Kotlin对象的包装
情况2
Kotlin_ObjCExport_refToRetainedObjC

● 462:获取Kobj绑定的Oobj
○ 之前绑定过,获取成功直接返回
● 下面的逻辑:需要走新建对象并绑定
○ 1. 通过TypeInfo获取到对应的OC class
○ 2. 调用 createRetainedWrapper 方法

● 分配内存,创建 Oobj
● 双向绑定
下面的情况属于这种情况,先创建Kotlin对象,然后创建OC对象

情况3
不是从Kotlin class 生成的OC 的类的实例对象转Kotlin对象
● MyObject 从Kotlin 的class生成的
● 如果有一个类集成了 'HelloHello',这个类也不是从Kotlin class 映射过来的

转Kotlin对象走Kotlin_ObjCExport_convertUnmappedObjCObject

动态生成一个TypeInfo,然后单向绑定,只有从Kotlin对象可以找到OC对象

动态生成一个TypeInfo里会根据父类,遵守的协议设置虚函数表(这些虚标是编译器提前为每个类型准备好的),对应的实现函数是 以kotlin2objc为前缀的这些函数:

这些函数是跟上面介绍的objc2kotlin的作用相反
● kotlin2objc函数 在Kotlin侧调用(绑定在TypeInfo的虚表中);objc2kotlin函数在OC侧调用,绑定在OC类对应的SEL上
● kotlin2objc 将参数转为OC对象,然后给OC对象发消息,获取返回值转为Kotlin对象
● objc2kotlin 将参数转为Kotlin对象,然后调用Kotlin的函数,获取返回值转为OC对象


背后的过程是
● OC_37
○ 创建 MyObject 的实例对象 O1
○ 调用 h1 的 setHelloable 的方法 参数是 h1(self) SEL O1 函数对应的实现是 objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){}
○ objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){} 将 h1 跟 O1 转为 对应的 Kotlin 对象 Kh1 KO1
○ h1 -> Kh1 的转换读取refHolder即可,因为 h1 是通过 createRetainedWrapper 方法创建的,创建的时候已经绑定了kotlin对象
○ O1 -> KO1 的过程: 通过 Kotlin_ObjCExport_convertUnmappedObjCObject实现,过程中会动态创建 TypeInfo,通过 O1 遵守的HelloHelloAble协议(OC的runtime可以获取到) 找到Kotlin侧 HelloAble 协议对应的虚表,并设置到动态创建的TypeInfo上,然后通过创建的TypeInfo创建一个 KO1 对象,
○ 然后将 KO1 设置到 Kh1 对应的成员变量上
● OC_38
○ 调用 h1 的 triggleInterface 方法,方法的实现是 objc2kotlin_kfun:Hello#triggleInterface(){}
○ 获取 h1 对应的 Kh1
○ 调用 kotlin 侧的 triggleInterface 方法
● Kotlin_18
○ 读取 Kh1 的成员变量helloable,读取到的对象是 KO1
○ 调用 KO1 makeIt 方法 kfun:HelloAble#makeIt(){}kotlin.String-trampoline -trampoline 后缀的方法是需要通过 TypeInfo 读虚表(此TypeInfo是上面动态创建的,创建的时候设置好了虚表)来确定调用最终的函数为kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String
○ kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String 中会把 KO1 转为 O1 对象,调用 O1 对象的 makeIt 方法,通过msgSend 实现,获取到返回值通过toKotlin: 方法转为Kotlin侧的对象
总结
● KN 处理 Kotlin 的逻辑整体跟C++的方式比较类似
● refHolder、toKotlin:、objc2kotlin_xx、kotlin2objc_xx 实现了与OC的交互
附录
objc2kotlin_xx 与 kotlin2objc_xx 方法如何绑定
● 每个class 每个 interface 编译器都会未其生成 TypeInfo
● 如果需要跟OC交互,TypeInfo中有个变量

● objc2kotlin_xx 与 kotlin2objc_xx 就存在这些列表里,runtime在运行时会根据类型匹配,动态绑定这些函数入口
● 在OC类的initialize方法中,会给OC类添加(通过 OC 的 runtime) directAdapters,classAdapters,implementedInterfaces_
● 在动态创建TypeInfo 的过程中,则会使用到kotlinVtable,kotlinItable,reverseAdapters