Kotlin/CLR 让Kotlin走进.NET世界

让 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")
    ...
}

原理

  1. 使用官方 kotlin-compiler-embeddable, 复用官方编译器组件进行 Configuration, Frontend, Fir2Ir 编译
  2. 编写 Kotlin/CLR 后端编译器将 Kotlin IR 降级并生成 C# 源码
  3. 使用 C# 编写的 AssemblyResolver 解析 .dll 文件, 并将其传递给 Kotlin/CLR 编译器
  4. 使用 C# 编写的标准库和 AssemblyResolver 为 Kotlin/CLR 提供标准库
  5. 通过 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...

相关推荐
androidwork23 分钟前
深入解析内存抖动:定位与修复实战(Kotlin版)
android·kotlin
移动开发者1号6 小时前
ReLinker优化So库加载指南
android·kotlin
移动开发者1号6 小时前
剖析 Systrace:定位 UI 线程阻塞的终极指南
android·kotlin
移动开发者1号6 小时前
深入解析内存抖动:定位与修复实战(Kotlin版)
android·kotlin
Try0219 小时前
Kotlin中Lambda表达式妙用:超越基础语法的力量
kotlin
泓博12 小时前
KMP(Kotlin Multiplatform)改造(Android/iOS)老项目
android·ios·kotlin
移动开发者1号12 小时前
使用Baseline Profile提升Android应用启动速度的终极指南
android·kotlin
移动开发者1号12 小时前
解析 Android Doze 模式与唤醒对齐
android·kotlin
Devil枫14 小时前
Kotlin扩展函数与属性
开发语言·python·kotlin
菠萝加点糖14 小时前
Kotlin Data包含ByteArray类型
android·开发语言·kotlin