让 Kotlin 走进 .NET 的世界
项目介绍
这是一个 Kotlin/CLR 后端编译器, 通过复用官方编译器实现基本前端编译, 使用 C# Assembly API 实现程序集解析, 用于提供符号以支持 Kotlin 调用 C#, 后端编译至 C# 源码, 再由用户手动通过 Roslyn 编译完成完整的编译流程
基本功能
- 由于项目刚刚起步, 很多语法还未支持, 标准库也未完善
基本的标准库调用
kotlin
fun main() {
println("Hello World!")
Console.WriteLine("Hello .NET!")
}
类型映射
kotlin.Any <-> System.Object
kotlin.Int <-> System.Int32
kotlin.Byte <-> System.SByte
...
面向对象
kotlin
open class Shape
class Rectangle(val height: Double, val length: Double): Shape() {
val perimeter = (height + length) * 2
}
- 生成的 C#
c#
public class Shape : global::System.Object
{
public Shape() : base()
{
}
}
public sealed class Rectangle : global::Shape
{
public Rectangle(global::System.Double height, global::System.Double length) : base()
{
this.height = height;
this.length = length;
this.perimeter = ((this.height) + (this.length)) * (2);
}
public global::System.Double height { get; }
public global::System.Double length { get; }
public global::System.Double perimeter { get; }
}
KMP
尽管目前并不能作为 Gradle 插件使用, 也没有 IDE 支持, 但你依然可以与现有 KMP 项目一同使用, 且支持 KMP
在 Arguments 处提供属性 commonSources 即可将 commonMain 视为 common 进行编译:
kotlin
CLRCompilerArguments().apply {
freeArgs += "src/commonMain/kotlin"
freeArgs += "src/clrMain/kotlin"
commonSources = arrayOf("src/commonMain/kotlin")
...
}
原理
- 使用官方 kotlin-compiler-embeddable, 复用官方编译器组件进行 Configuration, Frontend, Fir2Ir 编译
- 编写 Kotlin/CLR 后端编译器将 Kotlin IR 降级并生成 C# 源码
- 使用 C# 编写的 AssemblyResolver 解析 .dll 文件, 并将其传递给 Kotlin/CLR 编译器
- 使用 C# 编写的标准库和 AssemblyResolver 为 Kotlin/CLR 提供标准库
- 通过 ClrSymbolProvider 提供 C# 的符号解析, 使得 Kotlin/CLR 可以使用 C# 的类型和方法
诞生历程
我几年前经常玩一款游戏 蔚蓝(Celeste), 这是一款由 .NET C# 和 XNA/FNA 开发的 2D 横板游戏, 我在游玩一段时间后接触到了 Mod, 希望了解 Mod 开发, 在当时我就希望能使用 Kotlin 开发一个 Mod, 我尝试了 IKVM 但 IKVM 作为 Mod 貌似无法启动, 在当时便想到通过 Kotlin/Native 实现, 但由于不熟悉便就此放弃
去年我一个朋友拉我入坑了另一款游戏 星露谷物语(Stardew Valley), 这也是一款由 .NET C# 和 XNA/FNA 开发的 2D 俯视角游戏, 她当时在开发一个 Mod, 恰巧她们团队的 Coder 跑路了, 我就被拉来做她的 Coder
随后我写了一段时间的 C#, 但作为一个写了一年的 Kotliner, 实在是不能习惯 C#, 此时我又想起了 Kotlin/Native, 并做了一期视频和两条动态介绍(分享一下如何在C#中使用Kotlin/Native开发, C#使用Kotlin - 结构体传递, C#使用Kotlin - 对象传递优化)
但也遇到了一些问题: 遇到异常直接 Fatal Error且没有任何可用信息, 不管是 Kotlin 还是 C# 侧指针都用的太多了, 维护成本很大, 且开发体验极差
随后我想到了做一个编译器, 在了解了一点编译原理知识后开始了首次尝试, 使用 C# 和 ANTLR 做了一个简易的 Kotlin 解释器, 但项目架构简陋, 后面的开发会变得很难, 且无法直接支持和 C# 进行互调用, 又因为不习惯 C#, 便放弃了这个项目
再往后, 我尝试用 Kotlin 编写一个编译器, 也就是这个项目, 但由于对编译原理知识的缺乏, 对于语义分析等没有什么了解, 也很难再继续写下去, 于是又放弃了
再往后, 就是这篇文章的主角 Kotlin/CLR 后端编译器, 通过复用 Kotlin 官方编译器进行前端编译和 Fir2Ir, 而我只需要实现 CLR 后端, 以及符号提供等就行, 开发难度变得很低
.NET 有什么好处?
得益于 .NET 原生支持值类型和指针, 在内存严格情况下能有更低的内存占用
Kotlin 一直是类型擦除 + 具象化泛型, 在使用上总归差点感觉, 而 .NET 真泛型就能很好解决这一痛点
目前游戏开发(Unity, Godot, Unreal Engine w/ UnrealSharp) 多是 C# 脚本, 支持 .NET 或许能让 Kotlin 在游戏领域有一席之地
在桌面开发上, Kotlin 现有方案是 Compose Desktop(Awt), 在性能, 内存和特性支持上表现较差, 且不够成熟, GTK Kotlin Binding 学习资料较少, 但 C# 这块有非常多的框架(WinForm, WPF, UWP, MAUI, Xamarin, Avalonia, Uno Platform...), 如果支持 .NET 后在桌面领域 Kotlin 也能实现无缝迁移
我做这个项目有什么好处?
我写代码一直是出于兴趣, 我想做这个项目, 这就是我的动力
其次, 在 KMP 非官方目标方向上, 该项目或许可以成为一个参考, 激发更多 Kotlin 开发者尝试自己心中的目标, 为 Kotlin 生态发展出一份力
遇到的问题
其他 KMP 目标都是直接编译到产物, 而 CLR 目前却选择编译到半成品(C# 源码), 在开工之前就想过直接编译到产物, 但由于 Roslyn 只能使用 .NET 系语言使用, 而官方编译器使用 Kotlin/JVM 编写, 很难迁移至 Kotlin/CLR, 而跨进程传递 IR 工作量过大, 而编译到 IL 难度也十分大, 综合考虑后编译到 C# 是目前最快见效, 性价比最高的方案
Kotlin 语言一直采用类型擦除, 而 .NET 是真泛型, 这在互操作体验上会造成一定的割裂, 但好在 Kotlin 有 reified 具象化泛型, 可以通过不 inline 的 reified 实现真泛型
协程 C# 有相应的 await + async, 但具体的 CoroutineScope, Continuation 等可能是一个问题, 这在目前还没有想到如何解决(其实是我不懂 Kotlin 协程和 C# 的异步导致的)
C# 有 Struct 和指针, 这在 Kotlin/JVM 上是没有对应的, 但在了解到 Kotlin/Native 后, 我得到了答案:
Multi-Field Value Class 可以作为 Struct 的实现方案, Pointer<T> 则可以作为指针的实现方案, 对于 unsafe 使用如下:
kotlin
@OptIn(Unsafe::class)
fun unsafeFun(): Pointer<Int> {
val variable = 10
return variable.pointer
}
fun main() {
unsafe {
val ptr = unsafeFun()
println("address: $ptr, value: ${ptr.value}")
}
}
KMP 依赖也是一个问题, 目前想到的解决方法是 Kotlin/CLR 插件自动检测仓库有没有 CLR 目标的清单及产物, 没有时会报错, 但开发者也可通过手动引入 CLR 目标的扩展依赖来补全 KMP 依赖
kotlin
implementation("cn.yurin.ktor:ktor-client-core-clr:3.1.3" completion "io.ktor:ktor-client-core:3.1.3")
展望未来
目前而言, 由于编译器不完善无法编译 Kotlin 标准库, 标准库依然是使用 C# 编写, 在编译器完善后将迁移至 Kotlin 标准库
在目前设想中, 未来会改为直接编译至 IL/DLL, 也会通过 Gradle 插件作为官方 KMP 插件扩展插件使用, 且支持 KMP, 且能直接在 gradle build file 内引入 nuget 依赖
kotlin
implementation(nuget("Newtownsoft.Json", "13.0.3"))
由于 C# 及其生态链并不被 IDEA 支持, Kotlin 及其生态链也不被 Rider/Visual Studio 支持, 因此考虑到开发体验, 还需要推出 IDEA 的 C#, msbuild, nuget 等插件, 并在此基础上推出 Kotlin/CLR 插件实现最佳开发体验
在 Kotlin/CLR 基本完善后, 尝试将一些主流库移植到 CLR, 如 Ktor, Exposed, Compose Multiplatform...