目录

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 依赖的处理也不完全一致。核心都是改 ClangArgsLinker 的动态库路径。

改完后遇到 stdlibc++ 等问题,处理完终于用 OHOS 5.x 编过了。但放进工程后一片红:和基于旧版 OHOS SDK 生成的 klib 对比,发现 少了枚举类型 napi_status,其常量(如 napi_ok 等)也没了。

对比如下(左:OHOS 5.x 生成的 klib,右:OHOS 3.x):

/img/in-post/ohos-sdk.jpg

可选方案: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

/img/in-post/ohos-sdk-napi-status-h.jpg

5.x 里新增了三个枚举常量,并且显式赋了值(按顺序本是 22、23、24,其实可以不写)。一旦写了显式赋值,Clang 就会认为有 expression child,hasExpressionChild 为 true,从而 isStrictEnum 为 false。


三、修复

根因是:头文件里给新增的三个枚举常量写了显式赋值,导致 hasExpressionChild == trueisStrictEnum == 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 内联代码的作用。虽然对日常可能是「无用知识」,但给工作找了点乐子。