DC娱乐网

为超越JVM而生?深入理解Kotlin Native的梦想与可能

01概述 1.1 Kotlin 多平台的发展历程Kotlin 是一门静态类型的语言,最早以 100% 兼容 Java
01

概述

1.1 Kotlin 多平台的发展历程

Kotlin 是一门静态类型的语言,最早以 100% 兼容 Java 而闻名。从 2016 年 2 月正式发布以来,Kotlin 在很长一段时间里都是作为更好的 Java 或者一门更好的 JVM 语言而受到开发者喜爱的。然而,Kotlin 团队的梦想从一开始就不止步于 JVM。事实上,从发布于 2012 年的 [Kotlin M2 版本](https://blog.jetbrains.com/kotlin/2012/06/kotlin-m2-candidate/#js)开始,Kotlin 编译器就支持将 Kotlin 编译成 JavaScript,运行在 JavaScript 可以运行的任何环境中,这就是 Kotlin JS。Kotlin JS 最终在 2017 年 3 月的 [Kotlin 1.1](https://blog.jetbrains.com/kotlin/2017/03/kotlin-1-1/) 中稳定发布。不过由于当时 Kotlin JS 的工具链仍然不完善,甚至在后来还出现了较大的设计调整,因此早期的 Kotlin JS 并没有引起多数开发者的注意。接着很快在 2017 年 4 月, Kotlin 团队就公开了 Kotlin Native 的第一个预览版本和后续计划,这将成为 Kotlin 摆脱 JVM 的束缚和 Java 的影响的重要一步。如果有人说 Kotlin 就是 Java 的语法糖,从这一天开始,我们就可以告诉他,Kotlin JVM 只是 Kotlin 支持的目标平台之一。Kotlin 1.1 是存续时间最短的一个版本,因为 Kotlin 1.2 在同一年的 12 月就正式发布了。Kotlin 多平台特性(Kotlin Multiplatform,KMP)从 [1.2](https://blog.jetbrains.com/kotlin/2017/11/kotlin-1-2-released/) 开始预览,直到 6 年后的 [1.9.20](https://blog.jetbrains.com/kotlin/2023/11/kotlin-1-9-20-released/) 才进入稳定阶段。Kotlin 对多平台的支持,彻底将 Kotlin 转型为一门多平台静态类型的语言。Kotlin Native 运行时的不断完善和目标平台的不断扩展成为最近几年里 Kotlin 最重要的迭代路径之一。

1.2 Kotlin Native 简介

Kotlin Native 是指将 Kotlin 源代码编译为目标平台的本地二进制可执行程序或库,以类似于 C/C++、Go 等语言的方式运行在目标平台的原生环境中。与 Kotlin JVM 和 Kotlin JS 相比,Kotlin Native 在语言本身上没有什么特殊之处,只是目标产物不同而已。

Kotlin Native 支持多种平台,包括 Android(NDK)、iOS、Linux、Windows(MinGW)、macOS 等,可以覆盖绝大多数消费终端的开发场景。事实上,在早期的版本中,WebAssembly 也曾是 Kotlin Native 支持的平台之一,不过 Kotlin WASM 的后端编译器已经基于新版架构重写,成为与 Kotlin Native 并列的独立目标平台。

Kotlin Native 运行时提供了内存垃圾回收机制,使得 Kotlin Native 程序的开发体验与 Kotlin JVM 一致。Kotlin Native 还提供了与 C、Objective-C 的互调用接口,可以安全方便地实现跨语言调用,进而充分利用平台的原生能力。

本文将基于 Kotlin 2.0.0 版本从编译时和运行时两个角度介绍 Kotlin Native 的关键技术和核心特性。

02

编译与产物

Kotlin 编译器包含两个部分,即负责将 Kotlin 源代码编译成 Kotlin IR 的前端部分(Front-end)和将 Kotlin IR 编译成目标文件的后端部分(Back-end)。Kotlin Native 的编译流程如图所示:

接下来我们用一个非常简单的例子来展示各阶段的编译结果。

2.1 前端编译与 Kotlin IR

我们准备将这段非常简单的 Kotlin 源代码编译成一个 macOS 平台的可执行程序:fun main() { println("Hello World!!")}经过前端编译之后生成的 Kotlin IR 结构如下:FILE fqName:<root> fileName:Main.kt FUN name:main visibility:public modality:FINAL <> () returnType:kotlin.Unit BLOCK_BODY CALL 'public final fun println (message: kotlin.Any?): kotlin.Unit declared in kotlin.io' type=kotlin.Unit origin= message: CONST String type=kotlin.String value="Hello World!!"Kotlin IR(Kotlin Intermediate Representation)是 Kotlin 源码经过 Kotlin 前端编译器的语法解析、语义分析处理之后得到抽象语法树,并对抽象语法树进一步转换之后得到的只对目标代码的生成有意义的语法树。Kotlin IR 是前端编译器的产物,也是后端编译器的输入,它可以有效地屏蔽目标平台差异对 Kotlin 源代码的编译处理的影响。由此可见,这部分编译逻辑对于 Kotlin 的所有目标平台都是通用的。

2.2 后端编译与 LLVM IR

Kotlin Native 编译器会将 Kotlin IR 编译成 LLVM IR。我们将编译过程中生成的 LLVM bitcode 文件转换成 LLVM IR,摘取其中最核心的部分,如下所示:@main = alias i32 (i32, i8**), i32 (i32, i8**)* @Konan_maindefine i32 @Konan_main(...) #11 { %3 = tail call i32 @Init_and_run_start(...) ret i32 %3}define i32 @Init_and_run_start(...) ... { ... ; 创建 Kotlin Native 运行时 tail call void @Kotlin_initRuntimeIfNeeded() #9 ... ; 注意这里调用 @Konan_start %11 = invoke i32 @Konan_start(...) ... ... ; 销毁 Kotlin 运行时 call void @Kotlin_shutdownRuntime() ...}define internal i32 @Konan_start(...) ... { ...entry: ; 调用开发者定义的 main 函数 invoke void @"kfun:#main(){}"() #57 ... ...}define internal void @"kfun:#main(){}"() #6 !dbg !14199 { ...entry: ; 调用 println("Hello World!!") call void @"kfun:kotlin.io#println(kotlin.Any?){}"(...), ... ...}不难发现,在 main 函数中调用 println,参数是常量 @1012,类型是 ArrayHeader,大小是 13 x i16,其中 i16 就是 16 位的整型,即 13 x 16 bit,共 26 个字节。常量 @1012 的值如下:@1012 = internal unnamed_addr constant { ..., [13 x i16] [ i16 72, i16 101, i16 108, i16 108, i16 111, i16 32, i16 87, i16 111, i16 114, i16 108, i16 100, i16 33, i16 33 ] }

由于 Kotlin 的字符是采用 UTF-16 编码的,每个字符占两个字节,因此 13 个 i16 正好对应于 13 个字符。

LLVM IR 可以直接对应到最终可执行程序中的指令,因此我们可以非常完整地观察看到 Kotlin Native 的 main 函数调用前后分别做了哪些准备和清理工作。此外,阅读 LLVM IR 也对于我们理解和认识 Kotlin 对象的内存布局有很大的帮助。

最后,LLVM 编译器会将 LLVM IR 编译成对应平台的可执行程序或库,至此 Kotlin Native 的编译工作就全部完成了。

03

内存布局

3.1 基本数值类型的内存布局

基本数值类型的内存布局与 C/C++ 一致,在不涉及装箱操作时,占用内存的大小就是对应数值类型的定义的大小。例如 Int 类型占 4 字节,Double 类型占 8 字节。例如:val a = 1var b: Float = 2fvar c: Double = a * 2 + b.toDouble()val d = 'a'val e: Short = 28这段程序中包含 Char、Short、Int、Float、Double 五种基本数值类型的变量,它们的内存占用如图所示: