Kotlin/Native 给鸿蒙使用(二)

309 阅读9分钟

前言

在上一篇# Kotlin/Native 给鸿蒙使用(一)中,介绍了利用 Kotlin/Native 对 linux_arm64 提供的能力支持鸿蒙。通过这些能力,不仅可以访问系统底层能力,比如访问文件,线程,网络等,而且还能保证良好的性能。这让跨平台更容易和更友好。

因为 KMP 主要聚焦在 Android 和 iOS,所以集成了 Android 和 iOS 平台的很多原生能力,~/.konan/kotlin-native-prebuilt-macos-aarch64-2.1.10-RC2/klib/platform

.

├── android_arm64
│   ├── org.jetbrains.kotlin.native.platform.android
...省略
│   ├── org.jetbrains.kotlin.native.platform.egl # OpenGL EGL
│   ├── org.jetbrains.kotlin.native.platform.gles # OpenGL ES
...省略

├── ios_arm64
│   ├── org.jetbrains.kotlin.native.platform.ARKit # AR
│   ├── org.jetbrains.kotlin.native.platform.AVFAudio # 音频
│   ├── org.jetbrains.kotlin.native.platform.AVFoundation # 多媒体
...省略

那么,KMP 能不能集成鸿蒙平台的原生能力,如访问鸿蒙日志,OpenGL能力等。在 Kotlin/Native 中是可以的,通过利用 Kotlin 与 C 语言的互操作性,以及提供的 cinterop 工具,不仅能访问鸿蒙平台的 Native 能力,而且还能直接生成符合 Node-API 规范的 .so

cinterop 工具

在 Kotlin/JS 中有Dukat 工具Karakum 工具,将.d.ts转换为 Kotlin。在 Kotlin/Native 中有 cinterop 工具,将 C 库 转换为 Kotlin。

下面以在 macos_arm64 生成 curl 库的 Kotlin 绑定为例。(Kotlin/Native 项目模版。)

定义一个文件 curl.def查看def文件定义规则):

headers = curl/curl.h # 指定要导入的 C/C++ 头文件
headerFilter = curl/* # 防止引入不相关的头文件
compilerOpts.osx = -I/opt/homebrew/include # 指定 macOS 平台编译器的选项
linkerOpts.osx = -L/opt/homebrew/lib -lcurl # 指定 macOS 平台链接器的选项,在`/opt/homebrew/lib` 目录下找 curl

把该文件放在 src 目录下:src/nativeInterop/cinterop/curl.def,在 build.gradle.kts 中配置:

kotlin {
    applyDefaultHierarchyTemplate()
    macosArm64().apply {
        this.compilations["main"].cinterops {
            val curl by creating {
            defFile(project.file("./src/nativeInterop/cinterop/curl.def").path)
                packageName("curl")
            }
        }
        this.binaries {
            executable {
                entryPoint = "main"
            }
            sharedLib {
                baseName = "kn"
            }
        }
    }
}

同步项目。如果在项目中使用 curl 库找不索引,可以看下项目目录/.kotlin/metadata/kotlinCInteropLibraries文件夹下有没有 curl 相关 klib,没有就重启动一下 AndroidStudio。在 AndroidStudio 中,可以通过 klib 查看 curl 库生成的 Kotlin 代码。

在 Main.kt 中使用 curl 库:

import curl.CURLOPT_URL
import curl.curl_easy_cleanup
import curl.curl_easy_init
import curl.curl_easy_perform
import curl.curl_easy_setopt
import kotlin.experimental.ExperimentalNativeApi
import kotlinx.cinterop.*


@OptIn(ExperimentalForeignApi::class)
fun main() {
    memScoped {
        val curl = curl_easy_init()
        if (curl != null) {
            val url = "https://www.bilibili.com"
            curl_easy_setopt(curl, CURLOPT_URL, url)
            curl_easy_perform(curl)
            curl_easy_cleanup(curl)
        } else {
            println("Failed to initialize curl")
        }
    }
}

在命令行执行 ./gradlew linkMacosArm64 --info,在 build/bin/macosArm64 目录会生成产物:

.
├── debugExecutable
│   ├── KotlinNativeTemplate.kexe
│   └── KotlinNativeTemplate.kexe.dSYM
├── debugShared
│   ├── libkn.dylib
│   ├── libkn.dylib.dSYM
│   └── libkn_api.h
├── debugTest
│   ├── test.kexe
│   └── test.kexe.dSYM
├── releaseExecutable
│   ├── KotlinNativeTemplate.kexe
│   └── KotlinNativeTemplate.kexe.dSYM
└── releaseShared
    ├── libkn.dylib
    ├── libkn.dylib.dSYM
    └── libkn_api.h

查看是否可运行,可以执行一下 KotlinNativeTemplate.kexe。或在命令行执行./gradlew runDebugExecutableMacosArm64./gradlew runReleaseExecutableMacosArm64

Kotlin 与 C 互操作性

把 C 库 libcurl 生成 Kotlin 相关的 klib,依赖于 Kotlin 与 C 的互操作性。

类型映射

在 Kotlin 代码中调用 C,需要类型映射。下面简单介绍一下,具体可查看文档

基础类型:有符号、无符号整型和浮点类型,被映射到具有相同位宽的 Kotlin 对应类型。如:

CKotlin说明例子
intInt32 位整数int i = 42; → val i: Int = 42
unsigned intUInt无符号32 位整数unsigned int ui = 1000; → val ui: UInt = 1000u

其它 char, short, long, float, double, bool 类型等类似。

指针和数组:被映射到 CPointer<T>?。如:

CKotlin说明例子
T*CPointer<T>C 指针映射为 CPointer<T>,具体类型由 T 决定int* arr = ...; → val arr: CPointer<IntVar> = ...
char*StringC 字符串转换为 Kotlin String(需内存管理)char* s = "Hello"; → val str: String = s.toKString()
T[] 数组CPointer<TVar>C 数组通过指针访问int arr[10]; → val arrPtr: CPointer<IntVar> = ...
函数指针CFunction<ArgsType>函数指针通过 CFunction 类型表示,需用 staticCFunction 包装 Kotlin 函数int (*callback)(int); → val callback: CFunction<(Int) -> Int> = ...

枚举:被映射到 Kotlin 枚举或整型值。如:

CKotlin说明
enumKotlin enum 或 IntC 枚举默认映射为 Int,可通过配置生成 Kotlin 枚举类

结构体和联合体:被映射到通过点符号(即 someStructInstance.field1)访问字段的类型。

typedef:被表示为 typealias(类型别名)。

空指针:被表示为 null

分配和释放内存

可以使用NativePlacement手动分配和释放内存:

import kotlinx.cinterop.*

@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
fun main() {
    val size: Long = 0
    val buffer = nativeHeap.allocArray<ByteVar>(size)
    nativeHeap.free(buffer)
}

分配内存,往往忘记释放内存。可以使用memScoped { }自动释放内存:

import kotlinx.cinterop.*
import platform.posix.*

@OptIn(ExperimentalForeignApi::class)
val fileSize = memScoped {
    val statBuf = alloc<stat>()
    val error = stat("/", statBuf.ptr)
    statBuf.st_size
}

集成鸿蒙 hilog

在鸿蒙 Native 使用 hilog,动态库是:libhilog_ndk.z.so,头文件是hilog/log.h。定义文件 src/nativeInterop/cinterop/hilog.def

headers = hilog/log.h bits/alltypes.h
compilerOpts.linux = -I/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -I/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos
linkerOpts.linux = -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos -lhilog_ndk.z

在 build.gradle.kts 中配置:

//构建二进制文件
kotlin {
    applyDefaultHierarchyTemplate()
    //生成linux平台下的 .so
    val linuxTargets = listOf(linuxArm64())
    linuxTargets.forEach {
        it.binaries {
            executable {
                entryPoint = "main"
                this.compilation.compileTaskProvider.configure {
                    this.compilerOptions.freeCompilerArgs.addAll(
                        listOf(
                            "-Xoverride-konan-properties=linkerGccFlags=-lgcc",
                            "-linker-options", "-as-needed",
                        )
                    )
                }
            }
            sharedLib {
                //生成 libkn.so
                baseName = "kn"
            }
        }
        it.compilations["main"].cinterops {
            val hilog by creating {
                defFile(project.file("./src/nativeInterop/cinterop/hilog.def").path)
                packageName("hilog")
            }
        }
    }
}

封装 hilog:

import hilog.LOG_APP
import hilog.LOG_DEBUG
import hilog.LOG_ERROR
import hilog.LogLevel
import kotlinx.cinterop.ExperimentalForeignApi

object HILog {
    private const val DOMAIN = 0u

    @OptIn(ExperimentalForeignApi::class)
    fun d(tag: String, msg: String) {
        hilog.OH_LOG_Print(LOG_APP, LOG_DEBUG, DOMAIN, tag, "%{public}s", msg)
    }

    @OptIn(ExperimentalForeignApi::class)
    fun e(tag: String, msg: String) {
        hilog.OH_LOG_Print(LOG_APP, LOG_ERROR, DOMAIN, tag, "%{public}s", msg)
    }

    @OptIn(ExperimentalForeignApi::class)
    fun isLoggable(tag: String, level: LogLevel) {
        hilog.OH_LOG_IsLoggable(DOMAIN, tag, level)
    }
    //...省略
}

在命令行执行:./gradlew linkLinuxArm64 生成产物 libkn.solibkn_api.h,查看libkn.so依赖:

./aarch64-linux-musl-readelf -d libkn.so

Dynamic section at offset 0x95718 contains 27 entries:

  Tag        Type                         Name/Value

 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]

 0x0000000000000001 (NEEDED)             Shared library: [libhilog_ndk.z.so]

 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 
 //...省略

依赖 hilog 库 libhilog_ndk.z.so

集成鸿蒙 napi

在 Kotlin/Native 集成的 Android 平台原生能力中,通过 org.jetbrains.kotlin.native.platform.android klib 就能访问 JNI,然后直接生成符合 JNI 规范的 .so。同样,通过集成鸿蒙 napi,就能直接生成符合 Node-API 规范的 .so

在鸿蒙 Native 使用 napi,动态库是:libace_napi.z.so,头文件是napi/native_api.h napi/common.h。定义文件 src/nativeInterop/cinterop/napi.def

headers = napi/native_api.h napi/common.h
compilerOpts.linux = -I/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include -I/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/include/aarch64-linux-ohos
linkerOpts.linux = -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos -lace_napi.z

在 build.gradle.kts 中配置:

//构建二进制文件
kotlin {
    applyDefaultHierarchyTemplate()
    //生成linux平台下的 .so
    val linuxTargets = listOf(linuxArm64())
    linuxTargets.forEach {
        //...省略
        it.compilations["main"].cinterops {
            val hilog by creating {
                defFile(project.file("./src/nativeInterop/cinterop/hilog.def").path)
                packageName("hilog")
            }
            val napi by creating {
                defFile(project.file("./src/nativeInterop/cinterop/napi.def").path)
                packageName("napi")
            }
        }
    }
}

现在已经生成了 libace_napi.z 库的 Kotlin 绑定,就可以在 Kotlin 中使用 napi。

用 Kotlin 实现 napi_init.cpp

在鸿蒙项目中创建 Native C++ 模块 hn(任意命名),napi_init.cpp 为:

#include "napi/native_api.h"

static napi_value Add(napi_env env, napi_callback_info info)
{
    size_t argc = 2;
    napi_value args[2] = {nullptr};

    napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);

    napi_valuetype valuetype0;
    napi_typeof(env, args[0], &valuetype0);

    napi_valuetype valuetype1;
    napi_typeof(env, args[1], &valuetype1);

    double value0;
    napi_get_value_double(env, args[0], &value0);

    double value1;
    napi_get_value_double(env, args[1], &value1);

    napi_value sum;
    napi_create_double(env, value0 + value1, &sum);

    return sum;

}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    napi_property_descriptor desc[] = {
        { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr }
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "hn",
    .nm_priv = ((void*)0),
    .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void RegisterHnModule(void)
{
    napi_module_register(&demoModule);
}

用 Kotlin 实现 napi_module 模块注册

截屏2025-03-11 11.49.58.png

新建 src/linuxArm64Main/kotlin/NAPIInit.kt

@OptIn(ExperimentalForeignApi::class)
val demoModule: napi_module = memScoped {
    val module = alloc<napi_module>()
    module.nm_version = 1 // nm版本号,默认值为1
    module.nm_flags = 0u // nm标识符
    module.nm_filename = null // 文件名,暂不关注,使用默认值即可
    val func: napi.napi_addon_register_func = staticCFunction { p1: napi_env?, p2: napi_value? ->
        Init(p1!!, p2!!)
    }
    module.nm_register_func = func  // 指定nm的入口函数,这里是 Init
    module.nm_modname = "hn".cstr.getPointer(this) // 指定ArkTS页面导入的模块名,这里是 hn
    module.nm_priv = null // 暂不关注,使用默认即可
    for (i in 0 until 4) {
        module.reserved[i] = null // 暂不关注,使用默认值即可
    }
    module
}

@CName("KNInit")
@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
fun register() {
    //napi native模块注册
    napi_module_register(demoModule.readValue())
}

通过 @CName("KNInit") 注解,让鸿蒙能访问 KNInit,也就能进行模块注册。

在实现的过程中,在 项目目录/.kotlin/metadata/kotlinCInteropLibraries文件夹找到 napi 相关 klib,通过 klib 查看 napi 库生成的 Kotlin 代码,然后对比 napi_init.cpp,并根据 Kotlin 与 C 互操作性,就能编写出 Kotlin 实现。

用 Kotlin 实现 Init 初始化

@OptIn(ExperimentalNativeApi::class, ExperimentalForeignApi::class)
@CName("Init")
fun Init(env: napi_env, exports: napi_value): napi_value = memScoped {
    // 定义属性描述符
    val desc = allocArray<napi_property_descriptor>(1).apply {
        this[0].utf8name = "add".cstr.getPointer(memScope)
        val func: napi.napi_callback = staticCFunction { p1: napi_env?, p2: napi_callback_info? ->
            Add(p1!!, p2!!)
        }
        this[0].method = func
        this[0].attributes = napi_default
        this[0].data = null
    }
    // 注册属性到 exports
    napi_define_properties(env, exports, 1u, desc)
    return exports
}

Init 函数 在 napi_init.cpp 中是 EXTERN_C_START ...Init... EXTERN_C_END,所以也需要添加@CName("Init")注解。Init 的作用是初始化并进行接口映射,比如接口 Add → add。

用 Kotlin 实现 Add 功能

@OptIn(ExperimentalForeignApi::class)
fun Add(env: napi_env, info: napi_callback_info): napi_value = memScoped {
    HILog.e("Native", "Add function called!")
    val argc = alloc<size_tVar>().apply { value = 2uL }
    val args = allocArray<napi_valueVar>(2).apply {
        this[0] = null
        this[1] = null
    }
    // 获取参数
    napi_get_cb_info(env, info, argc.ptr, args, null, null)
    // 检查类型
    val valuetype0 = alloc<napi_valuetype.Var>()
    val valuetype1 = alloc<napi_valuetype.Var>()
    napi_typeof(env, args[0], valuetype0.ptr)
    napi_typeof(env, args[1], valuetype1.ptr)
    // 转 double
    val value0 = alloc<DoubleVar>()
    val value1 = alloc<DoubleVar>()
    napi_get_value_double(env, args[0], value0.ptr)
    napi_get_value_double(env, args[1], value1.ptr)
    HILog.e("Native", "${value0.value}+${value1.value}=${value0.value + value1.value}")
    // 计算和
    val sum = alloc<napi_valueVar>()
    napi_create_double(env, value0.value + value1.value, sum.ptr)
    return sum.value!!
}

Add 功能参照napi_init.cpp中的 Add,并根据 Node-API 规范编写。

在 Add 函数里面使用封装的 hilog: HILog

构建产物

现在已经在 Kotlin/Native 中集成了 hilog 和 napi,在命令行执行./graldew linkLinuxArm64 生成产物 libkn.so libkn_api.h

查看头文件libkn_api.h

//...省略

//声明外部函数
extern void* Init(void* env, void* exports);
extern void KNInit();

typedef struct {
  //...省略  
  struct {
    struct {
      struct {
        libkn_KType* (*_type)(void);
        libkn_kref_HILog (*_instance)();
        void (*d)(libkn_kref_HILog thiz, const char* tag, const char* msg);
        void (*e)(libkn_kref_HILog thiz, const char* tag, const char* msg);
        void (*isLoggable)(libkn_kref_HILog thiz, const char* tag, libkn_KUInt level);
      } HILog;
      const char* (*harmonyReadFile)(const char* filePath);
      void (*harmonyWriteFile)(const char* filePath, const char* content);
      void* (*get_demoModule)();
      void* (*Add)(void* env, void* info);
      void* (*Init_)(void* env, void* exports);
      void (*register_)();
      const char* (*readFile_)(const char* filePath);
      void (*writeFile_)(const char* filePath, const char* content);
      void (*main)();
    } root;
  } kotlin;
} libkn_ExportedSymbols;
//声明外部函数:入口
extern libkn_ExportedSymbols* libkn_symbols(void);

查看动态库 libkn.so 依赖:

./aarch64-linux-musl-readelf -d libkn.so

Dynamic section at offset 0x95718 contains 27 entries:

  Tag        Type                         Name/Value

 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]

 0x0000000000000001 (NEEDED)             Shared library: [libhilog_ndk.z.so]

 0x0000000000000001 (NEEDED)             Shared library: [libace_napi.z.so]

 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]

依赖 hilog 库 libhilog_ndk.z.so 和 napi 库libace_napi.z.so

鸿蒙接入 Kotlin/Native So

截屏2025-03-11 14.51.37.png

将 libkn.so 放在 khn/libs/arm64-v8a/ 目录下,将 libkn_api.h 放在 khn/libs/arm64-v8a/include 目录下,并修改配置文件CMakeLists.txt,同# Kotlin/Native 给鸿蒙使用(一)

此时,在 napi_init.cpp 中,删除其它代码,只保留 RegisterHnModule

#include "libkn_api.h"


extern "C" __attribute__((constructor)) void RegisterHnModule(void)
{
    KNInit();
}

接下来在 src/main/cpp/types/libhn/Index.d.ts 定义方法导出:

export const add: (a: number, b: number) => number;

add 给其它模块使用,hn/Index.ets:

export { add } from 'libhn.so';

鸿蒙项目中任意模块依赖 hn 模块:

"dependencies": {
  "khn": "file:../khn",
},

在模块中使用(测试代码):

import { hilog } from '@kit.PerformanceAnalysisKit';
import { add } from 'hn/Index'

@Entry
@Component
struct Index {
  @State message: string = 'Kotlin/Native';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            hilog.error(0x0000, 'Native', 'Test NAPI 2 + 3 = %{public}d', add(2, 3));
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

点击 Text Kotlin/Native,在控制台中查看:

截屏2025-03-11 15.06.26.png

可以看到 Kotlin/Native 封装的 hilog: HILog,以及封装 napi 生成符合 Node-API 规范的 libkn.so在 Harmony 平台成功运行。

总结

在 Kotlin/Native 中,除了使用 linux_arm64 基础能力支持鸿蒙外,还可利用 Kotlin 与 C 语言的互操作性,以及提供的 cinterop 工具,把鸿蒙平台的原生能力集成进来,如日志,OpenGL等,这进一步提升了 Kotlin/Native 的能力:设计合理的架构,全面覆盖 Android,iOS, Harmony 平台。