目录

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 还不太相同,所以最终修改的代码也不太一致

比如:

  1. 我们编译器内部存在两个版本的 llvm,11.1.0 和 12.0.1,只有编译 ohos 时才使用 llvm 12
    kuikly 统一使用了 llvm12
  2. 我们处理 ohos sdk 依赖不太一致

反正总归核心就是修改 ClangArgs 相关参数,还有 Linker 的动态库路径

在修改过程中,我遇到了 stdlibc++ 的一些问题,特殊处理了一下。最终,成功编译了基于 ohos 5.x 版本的 kotlin 编译器。但是放到工程中,发现,一片红海报错

和之前基于旧版本的 ohos sdk 生成的 klib 对比,发现 丢失了一个枚举类型 napi_status,进而其内部的 constant 也都不见了,如: 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

排查枚举类型丢失的原因

因为是在通过 cinterop 生成 klib 的过程中,发生的枚举类型丢失的问题,所以,要先了解 cinterop 的原理

简单介绍一下就是:

  1. cinterop 通过 clang api 解析 .h,生成 clang ast
  2. 基于 clang ast 生成 kotlin ir
  3. 基于 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 丢掉的过程就非常清晰了:

  1. clang 在解析 5.X sdk 的 napi_status 这个枚举类型时,isExplicitlyDefined = hasExpressionChild = true -> isStrictEnum = false
  2. clang 在解析 3.X sdk 的 napi_status 这个枚举类型时,isExplicitlyDefined = hasExpressionChild = false -> isStrictEnum = true

hasExpressionChild 为什么返回值会不一样呢?
通过查看 napi_status 所在头文件 js_native_api_types.h 后,恍然大悟,如下图:

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

发现 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 代码)等等

虽然,依然是无用的知识~~

但是,也算是给自己的工作找了乐子~~