前言
之前在《Kotlin Multiplatform 跨平台支持鸿蒙》和《Kotlin Multiplatform 封装鸿蒙API》中介绍了利用 Kotlin/JS 能力支持鸿蒙,通过把逻辑放在 commonMain 中,实现 Android、iOS、Harmony 共享逻辑。在这个过程中,遇到一些限制,比如访问文件、数据库、多线程、多媒体、网络功能等,不能编写出一个共用 API 让 Android、iOS、Harmony 平台都调用,只有定义接口让各平台实现才能适配共享逻辑。为了突破限制,利用 Kotlin/Native 从底层访问系统能力以达到更丝滑、更简单和更容易的方式支持跨平台共享逻辑。
鸿蒙 NDK
要进行 Native 开发,Android 有 NDK,鸿蒙也有 NDK。
鸿蒙 NDK 前置知识包括:Linux C语言编程知识、CMake使用知识、Node Addons开发知识以及Clang/LLVM编译器使用知识。
现在需要复用已有C或C++库,也就是在鸿蒙项目中依赖三方库( *.so)-Kotlin/Native 产物。主要了解一下鸿蒙 Native 通用能力、目标架构和 Node-API。
通用能力:鸿蒙基于 linux 内核,可以使用 libc,libc++,zlib,OpenGL,以及 POSIX 标准 等,所以完全支持文件操作、线程、网络等能力。
目标架构:linux_arm64。鸿蒙手机、平板等移动设备使用 arm64 架构。
Node-API:Java/Kotlin 代码与 Native(C/C++)代码进行交互需要 JNI,ArkTS/JS 代码与 Native(C/C++)代码需要 Node-API。鸿蒙 NDK 和 Android NDK 的角色相似,Node-API 的角色和 JNI 的角色相似。
毕昇编译器简介
毕昇编译器是基于LLVM开源软件开发的一款用于C/C++等语言的native编译器,能将C/C++代码工程编译链接成可以在设备上运行的二进制。在无需改动用户代码的条件下,相比业界主流的开源LLVM或GCC编译器,毕昇编译器能提供更强大的优化能力,使编译链接出来的二进制的运行时长更短、指令数更少,帮助提升应用在设备上的运行流畅度。
如果在鸿蒙中依赖三方库( *.so),那么三方库最好使用毕昇编译器编译。鸿蒙 llvm 路径: /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm。
交叉编译时指定 target 和 sysroot:使用 --target=aarch64-linux-ohos 参数通知编译器生成相应架构下符合HarmonyOS ABI的二进制文件:linux_arm64;使用 --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot 参数告知编译器HarmonyOS系统头文件的所在位置。
Kotlin/Native
对 Linux 提供的基础能力
Kotlin/Native 对 Linux 提供的基础能力可以在 ~/.konan/kotlin-native-prebuilt-macos-aarch64-2.1.10-RC2/klib 文件目录中找到,包括 common(标准库)和 platform(平台库)。在 platform 中支持 linux_arm32_hfp、linux_arm64、linux_x64。
linux_arm64 目录包含以下 klib:
org.jetbrains.kotlin.native.platform.builtin:C 标准库(内存、数学、字符串处理)org.jetbrains.kotlin.native.platform.iconv:字符编码转换org.jetbrains.kotlin.native.platform.linux:Linux 专用接口(文件监听、高性能 I/O)org.jetbrains.kotlin.native.platform.posix:POSIX 标准接口(文件、线程、网络通信)org.jetbrains.kotlin.native.platform.zlib:数据压缩与解压
通过 klib 就能访问这些能力,例如 org.jetbrains.kotlin.native.platform.posix 提供了 platform.posix.* API,通过 import platform.posix.* 就可以访问文件、线程、网络等能力。
通过 @CName 指定导出符号
下载 Kotlin/Native 项目模版,将 Kotlin 版本更新到最新。
首先通过 org.jetbrains.kotlin.native.platform.posix 写一个简单的文件读取 FileUtil.kt:
import kotlinx.cinterop.ExperimentalForeignApi
import platform.posix.EOF
import platform.posix.fclose
import platform.posix.fgetc
import platform.posix.fopen
import platform.posix.fputs
@OptIn(ExperimentalForeignApi::class)
fun writeFile(filePath: String, content: String) {
val file = fopen(filePath, "w") ?: throw Exception("File cannot be opened")
fputs(content, file)
fclose(file)
}
@OptIn(ExperimentalForeignApi::class)
fun readFile(filePath: String): String {
val file = fopen(filePath, "r") ?: throw Exception("File cannot be opened")
val content = mutableListOf<Char>()
var c: Int
while (true) {
c = fgetc(file)
if (c == EOF) break
content.add(c.toChar())
}
fclose(file)
return content.joinToString("")
}
FileUtil.kt 放在 nativeMain 中,可以给 Android、iOS、Harmony 使用。
在 linuxArm64Main 中给 Harmony 使用,新建立 HarmonyFileUtil.kt:
import kotlin.experimental.ExperimentalNativeApi
@OptIn(ExperimentalNativeApi::class)
@CName("writeFile")
fun harmonyWriteFile(filePath: String, content: String) {
return writeFile(filePath, content)
}
@OptIn(ExperimentalNativeApi::class)
@CName("readFile")
fun harmonyReadFile(filePath: String): String {
return readFile(filePath)
}
@CName 注解的作用是使顶层函数可从 C/C++ 代码中访问。@CName(externName="writeFile") 指定导出的符号为 writeFile,@CName(externName="readFile") 指定导出的符号为 readFile。
对于 Android,在 androidNativeArm64Main 中建立 AndroidFileUtil.kt 并生成符合 JNI 规范的 .so,通过符号 Java_com_wj_mylibrary_NativeLib_writeFile 和 Java_com_wj_mylibrary_NativeLib_readFile 访问相应函数。
回到鸿蒙,鸿蒙是否能直接生成符合 Node-API 规范的 .so?利用 Kotlin/Native 的 C Interop 和 @CName 是可以做到的。
编译目标架构
在 build.gradle.kts 中配置:
kotlin {
val linuxTargets = listOf(linuxArm64())
linuxTargets.forEach {
it.binaries {
executable()
sharedLib {
baseName = "kn"
}
}
}
}
给鸿蒙使用,指定 linuxArm64() 即可。在命令行执行 ./gradlew linkReleaseSharedLinuxArm64 --info,在 build/bin/linuxArm64/releaseShared 目录下会生成动态库 libkn.so 和头文件 libkn_api.h。
头文件 libkn_api.h 中包含外部函数声明,如 extern const char* readFile(const char* filePath); 和 extern void writeFile(const char* filePath, const char* content);,以及类型映射和入口函数 extern libkn_ExportedSymbols* libkn_symbols(void);。通过添加 @CName 注解可以直接访问 writeFile 和 readFile,或者通过 libkn_symbols() 函数访问 harmonyWriteFile 和 harmonyReadFile。
查看 libkn.so 信息,显示为 ARM 64 位架构编译的动态共享库。查看依赖时,发现 libkn.so 依赖多个库,如 libresolv.so.2、libm.so.6 等。在 FileUtil.kt 中只使用了文件操作功能,明显不需要那么多库。
在 gradle.build.kts 中添加按需链接 -as-needed:
val linuxTargets = listOf(linuxArm64())
linuxTargets.forEach {
it.binaries {
executable {
this.compilation.compileTaskProvider.configure {
this.compilerOptions.freeCompilerArgs.addAll(
listOf("-linker-options", "-as-needed")
)
}
}
sharedLib {
baseName = "kn"
}
}
}
编译后依赖减少,但仍依赖 libgcc_s.so.1。由于鸿蒙使用毕昇编译器(基于LLVM),libkn.so 不能依赖 libgcc_s.so.1,否则在鸿蒙中使用会崩溃。要让 libkn.so 不依赖 libgcc_s.so.1,可以选择静态链接或使用鸿蒙毕昇编译器交叉编译。这里选择静态链接。
Kotlin/Native 项目构建时,编译工具链配置文件 konan.properties 中的 linkerGccFlags 配置强制链接 libgcc_s。去除依赖 libgcc_s.so.1,在 gradle.build.kts 修改 linkerGccFlags:
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 {
baseName = "kn"
}
}
}
再编译后,libkn.so 依赖进一步减少,符合要求,可以在鸿蒙中使用。
鸿蒙接入 Kotlin/Native So
首先在鸿蒙项目中创建 Native C++ 模块 khn。将 libkn.so 放在 khn/libs/arm64-v8a/ 目录下,将 libkn_api.h 放在 khn/libs/arm64-v8a/include 目录下。然后修改配置文件 CMakeLists.txt:
cmake_minimum_required(VERSION 3.5.0)
project(khn)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
get_filename_component(PROJECT_MAIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY)
get_filename_component(PROJECT_SRC_DIR "${PROJECT_MAIN_DIR}" DIRECTORY)
get_filename_component(PROJECT_DIR "${PROJECT_SRC_DIR}" DIRECTORY)
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
add_library(khn SHARED napi_init.cpp)
target_include_directories(khn PUBLIC ${PROJECT_DIR}/libs/arm64-v8a/include)
target_link_libraries(khn PUBLIC libace_napi.z.so)
target_link_libraries(khn PUBLIC ${PROJECT_DIR}/libs/arm64-v8a/libkn.so)
接下来在 napi_init.cpp 中利用 Node_API 实现 writeFile 和 readFile 功能:
#include "napi/native_api.h"
#include "libkn_api.h"
#include <cstring>
static napi_value NAPI_Global_writeFile(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);
size_t filePathLength = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &filePathLength);
char *filePath = new char[filePathLength + 1];
std::memset(filePath, 0, filePathLength + 1);
napi_get_value_string_utf8(env, args[0], filePath, filePathLength + 1, &filePathLength);
size_t contentLength = 0;
napi_get_value_string_utf8(env, args[1], nullptr, 0, &contentLength);
char *content = new char[contentLength + 1];
std::memset(content, 0, contentLength + 1);
napi_get_value_string_utf8(env, args[1], content, contentLength + 1, &contentLength);
writeFile(filePath, content);
delete[] filePath;
delete[] content;
return nullptr;
}
static napi_value NAPI_Global_readFile(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
size_t filePathLength = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &filePathLength);
char *filePath = new char[filePathLength + 1];
std::memset(filePath, 0, filePathLength + 1);
napi_get_value_string_utf8(env, args[0], filePath, filePathLength + 1, &filePathLength);
const char *content = readFile(filePath);
napi_value result = nullptr;
napi_status status = napi_create_string_utf8(env, content, strlen(content), &result);
delete[] filePath;
if (status != napi_ok) {
return nullptr;
};
return result;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"writeFile", nullptr, NAPI_Global_writeFile, nullptr, nullptr, nullptr, napi_default, nullptr},
{"readFile", nullptr, NAPI_Global_readFile, 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 = "khn",
.nm_priv = ((void *)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterKhnModule(void) { napi_module_register(&demoModule); }
通过 #include "libkn_api.h" 访问 writeFile 和 readFile 或 harmonyWriteFile 和 harmonyReadFile。
实现功能后,在 khn/src/main/cpp/types/libkhn/Index.d.ts 定义方法导出:
export const writeFile: (filePath: string, content: string) => void;
export const readFile: (filePath: string) => string;
在 khn/src/main/ets/pages/KotlinNative.ets 再包装一下:
import { readFile, writeFile } from 'libkhn.so';
export function nativeWriteFile(filePath: string, content: string): void {
writeFile(filePath, content);
}
export function nativeReadFile(filePath: string): string {
return readFile(filePath);
}
将 KotlinNative.ets 给其它模块使用,khn/Index.ets 导出。鸿蒙项目中任意模块依赖 khn 模块:
"dependencies": {
"khn": "file:../khn",
}
在模块中使用测试代码,点击按钮后,通过 Kotlin/Native 生成的 libkn.so 在 Harmony 平台成功运行,实现文件读写功能。
总结
通过 Kotlin/Native 直接访问系统底层文件、网络、多媒体、多线程等功能,可以突破 Kotlin/Android、Kotlin/iOS、Kotlin/JS 上层的限制,达到真正的一个API在 Android、iOS、Harmony 平台使用,而且还能保证良好的性能。
在 Kotlin/Native 给鸿蒙使用的过程中,由于 Kotlin/Native 构建 linux_arm64 与鸿蒙 Native 构建 linux_arm64 编译工具链的不同,导致 Kotlin/Native 生成的动态库在鸿蒙平台运行会有找不到符号、找不到依赖库的问题。虽然在去除依赖上能解决问题,但是最好还是可以使用鸿蒙 NDK 编译工具链进行交叉编译。