Kotlin/Native 给鸿蒙使用(二)

前言

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

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

java 复制代码
.

├── 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文件定义规则):

ini 复制代码
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 复制代码
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 库:

kotlin 复制代码
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 目录会生成产物:

erlang 复制代码
.
├── 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 对应类型。如:

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

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

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

C Kotlin 说明 例子
T* CPointer<T> C 指针映射为 CPointer<T>,具体类型由 T 决定 int* arr = ...;val arr: CPointer<IntVar> = ...
char* String C 字符串转换为 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 枚举或整型值。如:

C Kotlin 说明
enum Kotlin enumInt C 枚举默认映射为 Int,可通过配置生成 Kotlin 枚举类

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

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

空指针:被表示为 null

分配和释放内存

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

kotlin 复制代码
import kotlinx.cinterop.*

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

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

kotlin 复制代码
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

java 复制代码
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 复制代码
//构建二进制文件
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:

kotlin 复制代码
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依赖:

less 复制代码
./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

java 复制代码
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 复制代码
//构建二进制文件
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 为:

c 复制代码
#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 模块注册

新建 src/linuxArm64Main/kotlin/NAPIInit.kt

kotlin 复制代码
@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 初始化

kotlin 复制代码
@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 功能

kotlin 复制代码
@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

c 复制代码
//...省略

//声明外部函数
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 依赖:

less 复制代码
./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

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

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

c 复制代码
#include "libkn_api.h"


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

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

ts 复制代码
export const add: (a: number, b: number) => number;

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

ts 复制代码
export { add } from 'libhn.so';

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

json 复制代码
"dependencies": {
  "khn": "file:../khn",
},

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

ts 复制代码
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,在控制台中查看:

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

总结

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

相关推荐
鸿蒙布道师2 小时前
鸿蒙NEXT开发动画案例5
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
橙子199110167 小时前
在 Kotlin 中什么是委托属性,简要说说其使用场景和原理
android·开发语言·kotlin
androidwork7 小时前
Kotlin Android LeakCanary内存泄漏检测实战
android·开发语言·kotlin
康康这名还挺多12 小时前
鸿蒙HarmonyOS list优化一: list 结合 lazyforeach用法
数据结构·list·harmonyos·lazyforeach
晚秋大魔王15 小时前
OpenHarmony 开源鸿蒙南向开发——linux下使用make交叉编译第三方库——nettle库
linux·开源·harmonyos
python算法(魔法师版)18 小时前
.NET 在鸿蒙系统上的适配现状
华为od·华为·华为云·.net·wpf·harmonyos
bestadc20 小时前
鸿蒙 UIAbility组件与UI的数据同步和窗口关闭
harmonyos
zhangphil20 小时前
Kotlin高阶函数多态场景条件判断与子逻辑
kotlin
枫叶丹421 小时前
【HarmonyOS Next之旅】DevEco Studio使用指南(二十二)
华为·harmonyos·deveco studio·harmonyos next
乱世刀疤1 天前
深度 |国产操作系统“破茧而出”:鸿蒙电脑填补自主生态空白
华为·harmonyos