news 2026/9/10 7:38:43

如何理解 Kotlin inline 函数在 JVM 与 klib 后端内联语义的差异?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何理解 Kotlin inline 函数在 JVM 与 klib 后端内联语义的差异?

如何理解 Kotlin inline 函数在 JVM 与 klib 后端内联语义的差异?

【免费下载链接】kotlinThe Kotlin Programming Language.项目地址: https://gitcode.com/GitHub_Trending/ko/kotlin

当你维护一个包含inline函数的库,并且依赖库升级后该函数被改动、删除或不兼容地变更时,同一份下游代码在 JVM 和 klib 系后端(Native、JS 等)上的行为可能完全不同。kotlin 仓库的设计文档 Klib Inlining 正是针对这个问题:它给出了一个最小示例说明两个后端的行为差异,解释了差异来自编译流水线的哪个环节,并描述了 klib 后端计划向 JVM 语义靠拢的方案。理解这些内容能帮你在设计跨平台库的 API 升级时,提前判断inline函数改动会如何影响已发布的依赖方。

最小示例:依赖升级后两个后端各输出什么

文档用下面这个四模块示例说明分歧点:

// dependency-v1: inline fun depFun() = "lib.v1" // dependency-v2 inline fun depFun() = "lib.v2" // lib: depends on dependency-v1 fun libFun() = depFun() // main: depends on lib and dependency-v2 fun main() { println(libFun()) }

模块关系按注释划分:lib编译时只依赖dependency-v1,而main同时依赖libdependency-v2。文档明确给出这个示例在两个后端上的预期输出:

  • JVM 后端输出lib.v1:内联函数的代码在lib编译时就已经内联进libFun的字节码,之后的结果不会随链接时实际可见的函数版本而改变。
  • klib 系后端输出lib.v2:内联函数调用在 klib 中按普通调用保存,内联发生在依赖解析(linking)之后,因此取到的是链接时可见的v2版本。

文档特别指出,当函数只是换实现时还能得到“错误的版本”,如果函数被删除或不兼容地变更,问题会更严重。这正是库开发者需要同时面对两套兼容模型的原因。

差异根源:内联发生在流水线的哪一步

JVM 后端:内联在代码生成阶段作用于字节码

JVM 流水线在 Plugins 阶段之前与 klib 后端完全一致:源码经 Frontend 转成 FIR,Fir2Ir转成 IR(外部依赖的 FIR 转成 LazyIr),IrActualizer合并模块,再经过 Plugins。差别在于之后不走 klib 序列化:IR 直接进入 Lowerings 和代码生成。文档描述 JVM 代码生成阶段包含 Class File generation、Metadata generation、Bytecode Inliner 和 Coroutines processing 几部分,并且:

As opposed to non-jvm backends, inlining here happens after all other lowerings as a part of code generation. Moreover, it doesn't happen over IR, it happens over bytecode.

也就是说,JVM 上的内联发生在所有 lowering 之后、针对字节码进行。文档同时提到存在把内联迁移到 IR 上的计划,但具体实现不在该文档范围内。

klib 后端:两次编译,内联传统上推迟到第二次运行

klib 系后端的编译分两次运行:

  1. 第一次运行:把源码编译成 Klib。源码与依赖 klib 的 metadata 经 Frontend 转成 FIR,Fir2Ir把源模块的 FIR 转成 IR、把外部引用的 FIR 转成LazyIrIrActualizer合并模块,Plugins 处理后由 Klib Serializer 序列化成 klib。
  2. 第二次运行:把所有 klib(含第一次运行的产物)反序列化后交给irLinker(反序列化 + Linker,负责把声明引用匹配到具体声明),随后依次经过 Pre-inline lowerings、Ir Inliner、后续 Lowerings 和 Code Gen,产出最终二进制。

文档对三个中间产物的定义值得留意:

  • FIR:前端表示,每个声明可序列化/反序列化为 metadata;
  • LazyIr:基于 FIR 的 IR 替身,只含声明、不含函数体,用于避免反序列化依赖中用不到的部分;
  • IR:内联机制实际处理的中间表示。

关键在于:旧模型中内联发生在第二次运行、链接完成之后。这就是为什么 klib 系后端能“看到”dependency-v2的新实现,而 JVM 后端看到的仍是lib字节码里固化的v1

为什么官方更倾向 JVM 模型

文档列出了四条理由:

  1. JVM 行为存在时间更长,更难改变;
  2. klib 模型在 JVM 上无法合理实现,而 JVM 模型可以在 klib 上实现;
  3. JVM 模型更符合直觉——人们把inline函数当作高层宏来理解;
  4. JVM 模型更简单,见 inline-to-crossinline 示例。

第四条背后的问题在于,内联函数比普通函数多出几种声明变更维度:类型参数可以在 reified 与非 reified 之间转换、lambda 参数可以在 inline/noinline/crossinline 之间变化、inline关键字本身可以增删。klib 兼容模型需要为这些组合逐一回答“链接时怎么办”;而 JVM 上链接时实际上已不存在内联函数(少数 corner case 除外),它们此时表现得和普通过程函数一样,无法再被改变,心智模型因此简单很多。例如该示例提出的问题:一个在 v1 中用l()直接调用、调用方依赖非局部返回的代码,遇到库在 v2 中把参数改成noinline,还能链接成功吗?这类问题在 JVM 模型下不会出现。

目标状态:klib 后端把内联提前到序列化之前

文档声明 “We are aiming to change the pipeline in the near future”,目标流水线相对旧模型的四个核心变化:

  1. 内联在 Klib 序列化之前发生——这是主要目标,让 klib 产物中的调用固化为已内联的代码,与 JVM 行为一致;
  2. Pre-inline lowerings 必须移到序列化之前,这给它们带来额外限制(见下节);
  3. Klib 序列化必须能处理部分未链接的 IR(部分引用仍是 unbound 的IdSignature,链接被推迟到第二次运行);
  4. IR 内联必须能处理内联函数体内部未链接的引用。前提是调用点处的 IR 总是已链接的——因此内联一个函数前,必须先把它内部所有内联函数的调用点内联掉。

文档用 step-by-step-basic 示例 演示了新模型下main模块编译时run { 42 }的处理过程:前端解析出调用、依赖lib的函数以 LazyIr(无函数体)形式加载,反序列化出run的函数体(此时体内引用是 unbound 符号,不能先跑 Linker),随后在调用点展开内联,最终foo的函数体里出现内联展开后的process调用,并用IrInlinedFunctionBlock节点承载调试信息。

Pre-inline lowerings:必须在序列化前完成、且结果会被固化

文档以 Native 为例列出需要在内联前完成的 lowering:

  • typeOfintrinsic 的处理(见 typeOf 示例);
  • Array(size: Int, init: (Int) -> Int)构造器处理,这是语言中唯一允许的内联构造器;
  • lateinit 字段处理——isInitializedintrinsic 需要访问私有字段,必须在类作用域内完成;
  • outer this 处理(见 outer-this 示例);
  • Shared variable lowerings,它是 local declarations lowering 的前置条件;
  • 局部声明的处理(涉及匿名对象,见 anonymouse-objects 示例);
  • 指向带 reified 类型实参的内联函数的函数引用的特殊处理。

这些 lowering 移到序列化前有两个新限制:一是它们存在上下文里的数据在第二次运行时不可用(如 LateInit 和 OuterThis 这类 lowering 存了状态),需要去掉这些状态或把 lowering 移回内联之后;二是语义层面的——这些 lowering 的结果会被序列化进 klib,意味着它们不能被回溯修改,新增或变更 lowering 时必须同时兼容新旧两个版本的 klib,且不能改变公开可见的签名(否则把模块当依赖用时会在 LazyIr 上重新执行它们)。因此文档的结论是:pre-inline lowerings 应尽量避免。另外,type erasure 目前发生在内联过程中,因为内联函数反序列化之后上下文不足,文档认为它本应是一个独立的 lowering。

迁移安排与需要留意的 corner case

关于存量 klib,文档给出的迁移策略(并明确标注This is draft decision, more investigation is required)是:已存在的、未按新方案准备的内联函数 klib 保持原样,仍然在 klib 链接之后内联。文档把这归为需要接受的技术债,理由是链接前内联的约束更严格,能处理它的代码应当也能处理已链接的 IR。

文档同时列出一组 corner case,说明实现时不能丢失这些情况(并建议第一遍阅读可以跳过):

  • typeOf:intrinsic 与内联的交互;
  • 与匿名对象的交互;
  • 访问私有声明:内联会突破模块边界访问调用方看不到的声明;
  • 从 Java 调用;
  • inline override;
  • 使用调用点不可见的声明:例如libA提供fooAlibBinline fun fooB() = fooA()实现依赖libA,而libCfun fooC() = fooB()只依赖libB。在 JVM 上内联invokestatic libAKt.foo时不关心目标是否存在,不是问题;现行非 JVM 后端内联时能看到全部传递依赖,也不是问题;但改成编译期对 IR 内联后,fooAlibC的编译不可见,就会成为问题——而且 JVM 上无法靠 IrLinker 事后补救。

如何核对两种后端的行为

该设计文档本身不提供构建命令,验证方式就是复现开头的四模块示例:按注释拆分模块,保证lib只链接dependency-v1main同时链接libdependency-v2,分别在 JVM 目标与 klib 系目标上编译并运行main。文档给出的预期结果是 JVM 输出lib.v1、klib 系后端输出lib.v2——注意这是文档陈述的预期输出,用于判断你观察到的行为属于哪套语义模型,而不是一个必须逐字匹配的日志模板。

判断你手上的库走哪套模型时,需要结合迁移现状:新方案在文档中属于“近期目标”+草稿级决策,而旧 klib 会明确保留链接后内联。也就是说,一个已发布 klib 的inline函数调用,其固化的版本取决于该 klib 按哪套流水线编译,这正是库 API 升级前要先想清楚兼容模型的原因。

【免费下载链接】kotlinThe Kotlin Programming Language.项目地址: https://gitcode.com/GitHub_Trending/ko/kotlin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 7:37:08

Multica 自建部署怎么开启 Prometheus 指标并保护 /metrics 端点

Multica 自建部署怎么开启 Prometheus 指标并保护 /metrics 端点 【免费下载链接】multica Make humans and AI agents work as one team — open-source and self-hostable. 项目地址: https://gitcode.com/GitHub_Trending/mu/multica 在自建 Multica 时,默…

作者头像 李华
网站建设 2026/9/10 7:36:42

车载智能互联盒子怎么选?从原理到实操的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 7:36:17

Humanizer技能详解:去除AI味,让文字更有温度

做内容这些年,我越来越觉得“humanizer”不是一个软件的名字,而是一套基本功。最近这个词又上了热搜,连带“humanizer skill”一起被大量讨论,很多人以为它是什么黑科技,其实拆开来看,就是把人机感过重的文…

作者头像 李华
网站建设 2026/9/10 7:34:37

Ruff 的版本号规则怎么理解:minor 版本引入哪些不兼容变更

Ruff 的版本号规则怎么理解:minor 版本引入哪些不兼容变更 【免费下载链接】ruff An extremely fast Python linter and code formatter, written in Rust. 项目地址: https://gitcode.com/GitHub_Trending/ru/ruff 如果你把项目的 Ruff 从 0.15.x 升到 0.16…

作者头像 李华
网站建设 2026/9/10 7:32:38

AI Engineering 怎么拿最划算?二手、租赁、借书 3 条路一次讲清

AI Engineering 怎么拿最划算?二手、租赁、借书 3 条路一次讲清 【免费下载链接】aie-book [WIP] Resources for AI engineers. Also contains supporting materials for the book AI Engineering (Chip Huyen, 2025) 项目地址: https://gitcode.com/GitHub_Trend…

作者头像 李华