Kotlin/Native 部分枚举类型在 cinterop 导出后消失

背景
腾讯 Kuikly 开源了支持鸿蒙的 Kotlin 编译器,符号导出、字符串优化和鸿蒙 NDK 适配等很有参考价值。我们当时因 Kotlin 2.0.21 的 LLVM 限制用的是 OHOS 3.x SDK,native binding 基于 3.x 头文件生成,新版本能力用不上。借鉴 Kuikly 后想改用 Clang/LLVM 12,sysroot、链接目录等改用 OHOS 5.x SDK,以用到新能力。
一、现象:枚举类型丢失
我们和 Kuikly 的编译器差异包括:我们内部有两套 LLVM(11.1.0 和 12.0.1,仅编译 OHOS 用 12),他们统一用 12;OHOS SDK 依赖的处理也不完全一致。核心都是改 ClangArgs 和 Linker 的动态库路径。
改完后遇到 stdlibc++ 等问题,处理完终于用 OHOS 5.x 编过了。但放进工程后一片红:和基于旧版 OHOS SDK 生成的 klib 对比,发现 少了枚举类型 napi_status,其常量(如 napi_ok 等)也没了。
对比如下(左:OHOS 5.x 生成的 klib,右:OHOS 3.x):

可选方案:1)把所有 napi_status 改成 UInt 直接传 int;2)把 napi_status 再导出来。我们还有 KSP 生成的鸿蒙代码依赖枚举,改传值比较恶心,所以选 方式 2。
二、排查:为什么 5.x 会丢枚举
cinterop 流程大致是:解析 .h → 生成 Clang AST → 生成 Kotlin IR → 生成 serialized IR 和 cstubs.bc。所以要确认:Clang 能否解析出 enum;AST 转 Kotlin IR 时 enum 是否被保留。
2.1 Clang 解析与 EnumStubBuilder
Clang 解析在 Index.kt 相关代码里,debug 可知 能正常解析出 enum。AST 转 Kotlin IR 在 StubIrElementBuilders.kt,enum 由 EnumStubBuilder 处理;debug 发现 在 OHOS 5.x 下会丢 enum。
2.2 isStrictEnum 分支
EnumStubBuilder 里若 !context.isStrictEnum(enumDef),会走 Constants 分支,直接 generateEnumAsConstants,不再生成 enum 类型:
override fun build(): List<StubIrElement> {
if (!context.isStrictEnum(enumDef)) {
return generateEnumAsConstants(enumDef)
}
// 否则按 enum 生成:constructor、value 属性、canonicalConstants 等
// ...
}
def 里可以配 strict enum,但根因是:为什么 5.x 和 3.x 的 isStrictEnum 结果不一样?
2.3 isExplicitlyDefined 与 hasExpressionChild
继续 debug 发现:isExplicitlyDefined 在 5.x 为 true、3.x 为 false,进而导致 isStrictEnum 不同。isExplicitlyDefined 来自 Clang 解析时对每个 enum constant 的 hasExpressionChild:
// 遍历 enum constant 时
val constant = EnumConstant(
name,
value,
isExplicitlyDefined = childCursor.hasExpressionChild()
)
结论:
- 5.x:解析
napi_status时,isExplicitlyDefined = hasExpressionChild = true → isStrictEnum = false → 被当成 Constants,枚举类型消失。 - 3.x:hasExpressionChild = false → isStrictEnum = true → 正常生成 enum。
2.4 头文件差异
看 napi_status 所在头文件 js_native_api_types.h:

5.x 里新增了三个枚举常量,并且显式赋了值(按顺序本是 22、23、24,其实可以不写)。一旦写了显式赋值,Clang 就会认为有 expression child,hasExpressionChild 为 true,从而 isStrictEnum 为 false。
三、修复
根因是:头文件里给新增的三个枚举常量写了显式赋值,导致 hasExpressionChild == true → isStrictEnum == false → 被当成 Constants 导出,枚举类型就没了。
所以修复就是:删掉头文件里那三行的显式赋值(顺序本来就是 22、23、24,不需要写)。改完后 Clang 解析时 hasExpressionChild 变为 false,isStrictEnum 会恢复为 true,napi_status 会按枚举类型重新导出。
若不想改 SDK 头文件,可以尝试在 cinterop 的 def 里给该模块配 strict enum(未实测)。
四、总结
通过这次排查把 cinterop 的流程过了一遍:.h → Clang AST → Kotlin IR → klib(serialized IR + cstubs.bc),以及 cstubs.bc 作为 binding 内联代码的作用。虽然对日常可能是「无用知识」,但给工作找了点乐子。