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

背景
最近,腾讯 kuikly 开源了自己的 kotlin 编译器,支持了鸿蒙平台
查看了一些他们的源码后,发现,他们做的一些优化和适配比较有借鉴意义。其中,符号导出、字符串优化 和 鸿蒙平台的 ndk 适配这块,最值得学习
因为 kotlin 2.0.21 版本的 llvm 限制,我们当时使用的是 ohos 3.X
版本的 sdk。因此,native 能力适配,也是基于 ohos 3.X 版本中的头文件生成的 binding 代码,有很多新版本的功能用不了
借鉴了 kuikly 的代码后,也想继续使用 clang/llvm 12,但是 sysroot 头文件、链接目录等等,都找 ohos 5.X
版本的 sdk,这样就可以使用新版本的 ohos sdk 提供的功能了
枚举类型问题
因为我们的 kotlin 编译器 和 kuikly 还不太相同,所以最终修改的代码也不太一致
比如:
- 我们编译器内部存在两个版本的 llvm,11.1.0 和 12.0.1,只有编译 ohos 时才使用 llvm 12
kuikly 统一使用了 llvm12 - 我们处理 ohos sdk 依赖不太一致
反正总归核心就是修改 ClangArgs
相关参数,还有 Linker
的动态库路径
在修改过程中,我遇到了 stdlibc++ 的一些问题,特殊处理了一下。最终,成功编译了基于 ohos 5.x
版本的 kotlin 编译器。但是放到工程中,发现,一片红海报错
和之前基于旧版本的 ohos sdk 生成的 klib 对比,发现 丢失了一个枚举类型 napi_status
,进而其内部的 constant 也都不见了,如: napi_ok 等等
如下图(左边是 ohos 5.x 版本生成的 klib,右边是 ohos 3.x):
那么,解决问题,有两种方式
- 修改 napi_status 所有的代码,改为 UInt,直接传 int 值
- 想办法把 napi_status 再导出来
因为,我们生成鸿蒙代码时,还有一部分 ksp 自动生成的代码,而且枚举类型没了,直接传值比较恶心,所以最终还是打算走 方式 2
排查枚举类型丢失的原因
因为是在通过 cinterop 生成 klib 的过程中,发生的枚举类型丢失的问题,所以,要先了解 cinterop 的原理
简单介绍一下就是:
- cinterop 通过 clang api 解析 .h,生成 clang ast
- 基于 clang ast 生成 kotlin ir
- 基于 kotlin ir 生成 serialized ir 和 cstubs.bc
那显然,我们需要先看一下 clang 是否能正常的解析出 enum type,其次就是 clang ast 变成 kotlin ir 的时候,enum 还在不在
clang 解析的过程在 Index.kt
相关代码中,经过 debug 发现,其能正常解析出 enum
clang ast 生成 kotlin ir 在 StubIrElementBuilders.kt
相关代码中,其中 enum 的处理在 EnumStubBuilder,等过 debug 发现,基于 ohos 5.x 会丢失 enum
isStrictEnum
基于之前的分析,我们发现在 EnumStubBuilder 运行后,napi_status 这个枚举不见了
进一步 debug,发现其 isStrictEnum 判断返回了 false,进而走向了 Contants 分支,代码如下:
override fun build(): List<StubIrElement> {
if (!context.isStrictEnum(enumDef)) {
// 直接返回
return generateEnumAsConstants(enumDef)
}
val constructorParameter = FunctionParameterStub("value", baseType)
val valueProperty = PropertyStub(
name = "value",
type = baseType,
kind = PropertyStub.Kind.Val(PropertyAccessor.Getter.GetConstructorParameter(constructorParameter)),
modality = MemberStubModality.OPEN,
origin = StubOrigin.Synthetic.EnumValueField(enumDef),
isOverride = true)
val canonicalsByValue = enumDef.constants
.groupingBy { it.value }
.reduce { _, accumulator, element ->
if (element.isMoreCanonicalThan(accumulator)) {
element
} else {
accumulator
}
}
val (canonicalConstants, aliasConstants) = enumDef.constants.partition { canonicalsByValue[it.value] == it }
// 省略
}
同时,研究了 isStrictEnum 判断逻辑后,发现,我们可以在 def 中指定 strict enum 配置,不过问题的根因目前还是没有找到
那么,究竟为什么两个版本的 sdk 经过 clang 解析后,给出的 isStrictEnum 判断不一致呢?
isExplicitlyDefined 和 hasExpression
经过进一步 debug,发现两个版本的 isExplicitlyDefined 值不同,ohos 5.x 返回 true,而 3.x 返回 false,进而导致了 isStrictEnum 的结果不一样
而 isExplicitlyDefined 又受到 clang 解析的影响,代码如下:
private fun getEnumDefAt(cursor: CValue<CXCursor>): EnumDefImpl {
if (clang_isCursorDefinition(cursor) == 0) {
val definitionCursor = clang_getCursorDefinition(cursor)
if (clang_isCursorDefinition(definitionCursor) != 0) {
return getEnumDefAt(definitionCursor)
} else {
// FIXME("enum declaration without constants might be not a typedef, but a forward declaration instead")
return enumRegistry.getOrPut(cursor) { createEnumDefImpl(cursor) }
}
}
return enumRegistry.getOrPut(cursor) {
val enumDef = createEnumDefImpl(cursor)
visitChildren(cursor) { childCursor, _ ->
if (clang_getCursorKind(childCursor) == CXCursorKind.CXCursor_EnumConstantDecl) {
val name = clang_getCursorSpelling(childCursor).convertAndDispose()
val value = clang_getEnumConstantDeclValue(childCursor)
// isExplicitlyDefined 此处被赋值,值为 childCursor.hasExpressionChild()
val constant = EnumConstant(name, value, isExplicitlyDefined = childCursor.hasExpressionChild())
enumDef.constants.add(constant)
}
CXChildVisitResult.CXChildVisit_Continue
}
enumDef
}
}
那么 enum 丢掉的过程就非常清晰了:
- clang 在解析 5.X sdk 的 napi_status 这个枚举类型时,isExplicitlyDefined = hasExpressionChild = true -> isStrictEnum = false
- clang 在解析 3.X sdk 的 napi_status 这个枚举类型时,isExplicitlyDefined = hasExpressionChild = false -> isStrictEnum = true
hasExpressionChild 为什么返回值会不一样呢?
通过查看 napi_status 所在头文件 js_native_api_types.h 后,恍然大悟,如下图:
发现 5.x 版本的头文件,新增了三个枚举类型,但是莫名给枚举类型赋值了(按照顺序就是 22 23 24,没必要赋值),进而造成了 hasExpressionChild 为 true
修复
经过以上分析,问题就很简单了,让 isStrictEnum 返回 true 即可
因为新增的枚举类型的顺序就是 22 23 24,所以可以直接把头文件里的赋值删掉,最终实现修复
当然,应该也可以强行添加 strict enum 配置(未尝试)
总结
之前对于 cinterop 这块了解的不是很详细
通过排查这个问题,对于 cinterop 原理了解的非常透彻
包括,klib 中的 kotlin ir 和 cstubs.bc 的生成过程和存在的必要性(为什么会有 cstubs.bc,其实是 binding 的 inline 代码)等等
虽然,依然是无用的知识~~
但是,也算是给自己的工作找了乐子~~