KSP For OHOS NAPI

kotlin/native 适配鸿蒙,除了编译器方面的修改之外,还有比较重要的一部分就是 napi 代码的自动生成了
kotlin/native 和 napi 的关系:
kotlin/native 实际产物就是对应平台二进制,在 ohos 上就是 linux elf
ohos 上层应用使用 arkts 开发,想调用 so 中的方法,有一层类似 jni 的机制,其实就是 nodejs 的 napi 机制
所以,想从 arkts 调用 kotlin/native 的代码,其实就是要自己写一层 napi
一个 ArkTs 调用 K/N 的例子
假如,我们在 kotlin/native 中存在一个方法 hello_world()
想从 arkts 中调用,则需要注册 napi
// 最终调用的 helloWorld
static napi_value helloWorld(napi_env env, napi_callback_info info) {
// call k/n function
hello_world();
// 如果涉及参数和返回值,还需要额外转换
// napi_get_value_XXX;
// napi_create_XXX;
return nullptr;
}
// init 注册 napi 方法
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"hello_world", nullptr, helloWorld, nullptr, nullptr, nullptr, napi_default, nullptr},
};
// 注册
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
}
// module register func 会在 napi_module_register 后被回调
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void *)0),
.reserved = {0},
};
// __attribute__((constructor)) 自动调用,触发 napi_module_register
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); }
由上可知,napi 的注册过程,需要写大量的 c++ 代码
如果涉及参数和返回值更加繁琐,需要将 arkts 传来的参数转换为 c/c++ 类型,然后 c/c++ 得到的返回值再转换为 arkts 类型
除此之外,在 arkts 侧,我们需要 d.ts
的声明文件,才能让编译器 happy,不报错
而且,这部分代码大多是模板代码,如果可以 自动生成
,那是极好的
KSP 自动生成 NAPI 代码的方式
KSP 就不多做介绍了,用于代替 kapt 来处理 kotlin 注解的一个工具。使用方法,参考官方文档即可。
但是,这里有一个非常烦心的问题:
假如,我们再 kotlin/native 中有一个成员方法如下:
class MMKV {
fun setString(key: String, value: String) {
}
}
要怎么给他生成 napi 呢?
手写 arkts
查看 ohos mmkv 的包,发现,ohos 上 mmkv 的 arkts 侧实现方式如下:
export class MMKV {
private nativeHandle: bigint;
private static g_rootDir: string
private constructor(handle: bigint) {
this.nativeHandle = handle;
}
// 静态方法初始化后,存 handle 即 native ptr
public static initializeWithPath(rootDir: string): string {
MMKV.g_rootDir = rootDir
native.initialize(rootDir, 0, null);
return MMKV.g_rootDir;
}
// 调用 setString 时,底层其实多传递了一个 handle
public setString(key: string, value: string, expiration?: number): boolean {
return native.setString(this.nativeHandle, key, value, expiration);
}
}
这样简单直接,但是,需要我们自己写 arkts 代码
试想,如果我们用 kotlin/native 开发出来的 so,使用 ksp 生成了 napi 代码以后,还要自己再加一层 arkts 的代码,着实有些麻烦,而且 napi 代码还要做相应的适配
对象映射
假如,我们 kotlin/native 的 mmkv 对象可以映射到一个 arkts 的对象上
arkts 调用对应的方法时,转而调用 kotlin/native 的 mmkv 对象的相应方法,那就不需要我们写任何的 arkts 侧的代码了,如下图:
这样,我们唯一需要添加的就是声明文件 d.ts 了
不过,理想很丰满,现实很骨感,这里显然有生命周期的问题
arkts 对象没有释放之前,kotlin 对象一定不可以释放,不然就会发生内存错误
除此之外,类型转换是个巨大问题,kotlin/native 侧的 mmkv object 如何映射到 arkts object,同时 arkts object 还得有相应的方法存在,可以调用
对象映射方案
参考了 kotlin/native 的一些分享,最终还是选择了对象映射的方案
这种方案的好处是:使用方傻瓜式使用,kotlin 中怎么调用,arkts 侧就怎么调用
难点在于:
- 如何处理对象映射问题
- 如何处理对象的生命周期
- d.ts 文件如何生成
Kotlin 中使用 NAPI
基于之前所说的 napi 的例子,我们知道,如果要注册一个 napi 我们需要写很多 c++ 代码,这部分代码通通调用的 napi 的函数,是一个标准 c 库
而 kotlin/native 提供了 cinterop,使用它,可以从 kotlin 中直接调用 c 方法
于是,我们可以使用 kotlin 代码来写 napi
同时,ksp 可以用于处理注解信息,再配合 kotlin poet,我们就可以实现,基于注解自动生成 kotlin 写的 napi 嗲嘛
对象映射问题
这其实是一个非常大的问题,我并没有很好的解法
基础类型
基础类型十分简单,我们只需做对应转换即可
也不用考虑生命周期问题,基础类型都是值
class 类型
class 类型需要创建 object
可以参考 napi_wrap 和 napi_unwrap,这个方法可以让 arkts 对象绑定一个 native 对象
同时,为了维护一一对应关系,还需要存储 k/n object 和 arkts object weak ref的 map 关系
然后在 arkts 侧的对象注册 finalize,当 arkts 对象回收时,unwrap 出 native 对象 free
泛型
目前,逆变协变什么的不知道怎么处理
普通泛型的话,因为 kotlin 和 arkts 中的泛型都是假的,所以我们转换的时候,都用 Any 来代替,然后强转
函数类型
目前没支持,还有 suspend 和 promise 的转换等等
对象生命周期问题
前面的 class 类型已经解释过了,详见文章开头的图
d.ts 生成问题
目前,是通过 ksp 汇总时生成的
KSP 问题
方案已经确定了,我们就需要使用 ksp 来实现对应的逻辑
这里没有什么特别多要说的地方,就是要写非常多的类型转换的 utils 方法,同时要维护一个 map 映射关系
有遇到几个奇怪的 ksp 的问题
-
ksp 无法跨模块收集信息
比如,我用 k/n 写了两个模块 a b,a 依赖 b,最终依靠 a 模块,想把所有的方法一次性汇总注册到 arkts 中(这又是另一个问题了,因为 如果两个模块分别打出两个 so,那么会重复打两份 k/n runtime,在运行时也会有两个 runtime 在运行,所以要打出一个 so,汇总注册)。
但是,ksp 只对当前模块负责,没法通过当前模块拿到依赖模块的信息(就是 b 模块通过 napi 生成的东西不可见)。
最终,通过一个奇怪的方法getDeclarationsFromPackage
能拿到,传递 package 包名··· -
ksp 无法获取到工程目录信息
比如,我要生成 index.d.ts 文件的时候,拿不到工程目录信息,要么就只能生成到 resource 目录下
但是,index.d.ts 对于 kotlin/native 本身是无意义的,arkts 侧才需要,resource 会被打包带进去
最终,干脆通过 generate file 的 absolute path,强行截取了一波工程目录,或者强制指定需要传递 index.d.ts 的路径参数··· -
ksp 会多次运行
当我们在处理注解信息的时候,ksp 会根据你的返回值,多次重复执行 processor,需要自己做判定