Commit ae1d29d1 authored by jiangyh's avatar jiangyh

Initial commit

parents
*.iml
.kotlin
.gradle
**/build/
xcuserdata
!src/**/build/
local.properties
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
## 项目介绍
该项目是一个 Kotlin Multiplatform(KMP)示例工程,目标是在保持一套共享业务逻辑的前提下,分别通过各平台原生方式进行集成与展示,支持 Android / iOS / HarmonyOS 三端。
## 工程结构与关键模块
- `composeApp/`:KMP 共享业务与平台桥接
- `commonMain/`:平台无关逻辑(如 `appInfo()`
- `androidMain/``iosMain/``ohosArm64Main/`:各平台的 `actual` 实现与桥接代码
- iOS:
- `MainViewController.kt` 用于向 Swift 暴露 `UIViewController`
- `App_ios.kt` 暴露 `iosUiText()`(含 `Arith.add/sub` 结果)
- Harmony:
- `OhosEntry.kt` 暴露 `@CName("getKotlinUiText")` 供 NAPI 调用
- `StaticLibDemo.kt` 演示调用 C 静态库 `libsample.a`(包含算术运算与嵌入图片资源获取)
- `OhosPlatform.kt` 平台标识实现
- `lib-arith/`:跨平台算术库(add/sub)
- 仅在 `androidMain``iosMain``ohosArm64Main` 依赖(不放在 `commonMain`
- `lib-base/``lib-analytics/`:独立的 KMP 业务模块
- 通过 `build.gradle.kts` 独立管理,在 `settings.gradle.kts` 统一注册
- `iosApp/`:SwiftUI App,链接 `ComposeApp.framework`
- `harmonyapp/`:ArkTS App,NAPI 桥接 `libkn.so` 与 UI 展示
## 环境准备
- Android Studio:选择 Gradle JDK 为 JDK 17(IDE 内配置,无需写死路径)
- Kotlin/Java 版本建议:
- Kotlin `jvmToolchain(17)` 用于编译工具链
- 应用 `jvmTarget``compileOptions` 建议统一为 Java 11
- iOS:Xcode 15+,建议使用通用 `FRAMEWORK_SEARCH_PATHS`,避免绑定到具体版本目录
- Harmony:DevEco Studio 6.0,ArkTS + NAPI,需可用的 aarch64 交叉工具链
## 构建与运行(Android)
- 运行(AS 中选择 Android 目标设备,正常运行)
## 构建与运行(iOS)
- 生成模拟器框架(arm64 模拟器)
```bash
./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64
```
- 生成真机框架(arm64 真机)
```bash
./gradlew :composeApp:linkDebugFrameworkIosArm64
```
- Xcode 链接框架
- Linked Frameworks and Libraries 添加 `ComposeApp.framework`,设置为 “Embed & Sign”
- 推荐 `FRAMEWORK_SEARCH_PATHS`
- `$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(PLATFORM_NAME)`
- 避免使用带版本号目录(例如 `iphonesimulator18.2`),并在 `project.pbxproj``Config.xcconfig` 中将 `IPHONEOS_DEPLOYMENT_TARGET` 设为你需要的最低版本(如 `17.0`
- Swift 调用示例(`ContentView.swift`
```bash
# Swift 中示例调用:
# import ComposeApp
# Text(ComposeAppApp_iosKt.iosUiText())
```
## 构建与运行(HarmonyOS)
- 命令一:仅构建共享库(不拷贝到 Harmony 工程)
- 作用:编译 KMP 的 Harmony 目标,生成 `libkn.so``libkn_api.h`
- 产物目录:`composeApp/build/bin/ohosArm64/debugShared/`
```bash
./gradlew :composeApp:linkDebugSharedOhosArm64
```
- 命令二:构建并发布到 Harmony 工程(自动拷贝)
- 作用:先执行上面的构建,再把产物拷贝到 `harmonyapp/entry` 便于 ArkTS/NAPI 直接使用
- 拷贝目标:
- 头文件:`harmonyapp/entry/src/main/cpp/include/libkn_api.h`
- 动态库:`harmonyapp/entry/libs/arm64-v8a/libkn.so`
```bash
./gradlew :composeApp:publishDebugBinariesToHarmonyApp
```
### StaticLib 集成与验证(libarith.a)
- 生成与链接
- `entry/src/main/cpp/CMakeLists.txt` 中定义 `add_library(arith STATIC arith/arith.cpp)`,构建时会生成 `libarith.a` 并链接到 `libentry.so`
- `add_library(entry SHARED napi_init.cpp)`
- `target_link_libraries(entry PRIVATE arith)`
- `napi_init.cpp``Add` 方法通过静态库函数 `arith_add` 进行计算,ArkTS 页面以 `testNapi.add(2, 3)` 验证 staticLib 集成。
- 产物位置
- `libarith.a` 是构建中间产物,不会被拷贝到 `entry/libs/`,常见位置为 `entry` 模块的本地构建目录,例如:
- `harmonyapp/entry/**/obj/arm64-v8a/libarith.a`(具体路径由 Hvigor/CMake 版本决定)
- 可用命令定位:
```bash
find harmonyapp/entry -name "libarith.a"
```
### Stdlib 演示(Kotlin 标准库:List 等)
- commonMain 中的演示函数(`composeApp/src/commonMain/kotlin/com/example/testdemo/StdlibDemo.kt`
- `stdlibListDemo()``List` 过滤、`map``joinToString`
- `stdlibAggregateDemo()``groupBy` 统计长度、`map/sorted/joinToString` 聚合展示
## 常见问题与解决方案
### HarmonyOS 静态库链接报错
**现象:**
在执行 `hvigor build` 或链接阶段报错:
```
ld.lld: error: .../libsample.a: archive member '/' is neither ET_REL nor LLVM bitcode
clang++: error: linker command failed with exit code 1
```
**原因:**
macOS 默认的 `ar` 工具生成的静态库格式(BSD format)与 HarmonyOS 工具链(期望 GNU format)不兼容。
**解决:**
在 Gradle 构建脚本中强制使用 HarmonyOS SDK 自带的 `llvm- `composeApp/build.gradle.kts` 配置示例:
```kotlin
val llvmAr = "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/BiSheng/bin/llvm-ar"
exec {
if (file(llvmAr).exists()) {
commandLine(llvmAr, "rcs", staticLib, objFile)
} else {
commandLine("ar", "rcs", staticLib, objFile)
}
}
```
### StaticLib 集成与验证(klib + .a)
- `@CName("getStdlibListDemo") fun getStdlibListDemo(): String = stdlibListDemo()`
- `@CName("getStdlibAggregateDemo") fun getStdlibAggregateDemo(): String = stdlibAggregateDemo()`
- NAPI 封装(`harmonyapp/entry/src/main/cpp/napi_init.cpp`)
- 通过 `dlopen("libkn.so")` + `dlsym(...)` 获取上述符号
- 导出 ArkTS 可调用的 `getStdlibListDemo()`、`getStdlibAggregateDemo()` 方法
- ArkTS 类型声明
- `harmonyapp/entry/src/main/cpp/types/libentry/Index.d.ts` 必须包含:
- `export const getStdlibListDemo: () => string;`
- `export const getStdlibAggregateDemo: () => string;`
- 页面调用示例(`harmonyapp/entry/src/main/ets/pages/Index.ets`):
- `const l: string = testNapi.getStdlibListDemo();`
- `const a: string = testNapi.getStdlibAggregateDemo();`
### StaticLib 集成与验证(klib + .a)
- 目标
通过 Kotlin/Native cinterop 将 `.a` 静态库合入到 `ohosArm64` 目标的 klib 中,并在最终 `libkn.so` 导出 `@CName("getStaticLibSum")`,供鸿蒙端 NAPI/ArkTS 验证。
- 文件与配置
- C 静态库源码与头文件
- 头文件:[composeApp/native-libs/sample/include/sample.h](kmptcp_kotlin_sample/composeApp/native-libs/sample/include/sample.h)
- 源码:[composeApp/native-libs/sample/src/sample.c](kmptcp_kotlin_sample/composeApp/native-libs/sample/src/sample.c)
- cinterop 定义
[composeApp/src/nativeInterop/cinterop/sample.def](kmptcp_kotlin_sample/composeApp/src/nativeInterop/cinterop/sample.def)
关键项:`staticLibraries = libsample.a`、`libraryPaths = ${projectDir}/composeApp/native-libs/ohosArm64`、`compilerOpts = -I.../include`
- Kotlin 调用与导出
[composeApp/src/ohosArm64Main/kotlin/com/example/testdemo/StaticLibDemo.kt](kmptcp_kotlin_sample/composeApp/src/ohosArm64Main/kotlin/com/example/testdemo/StaticLibDemo.kt)
- `@CName("getStaticLibSum") fun getStaticLibSum(a: Int, b: Int): Int`
- 通过 cinterop 绑定的 `sample_add` 调用 `.a` 中实现
- Gradle 构建任务
[composeApp/build.gradle.kts](kmptcp_kotlin_sample/composeApp/build.gradle.kts)
- `ohosArm64` 下新增 `cinterops { sample { defFile(".../sample.def") } }`
- 任务 `buildSampleStaticLib` 使用 `clang -arch arm64 -fPIC` 与 `ar rcs` 生成 `libsample.a`
- 将 `linkDebugSharedOhosArm64` 等链接任务依赖 `buildSampleStaticLib`
- NAPI 导出
[harmonyapp/entry/src/main/cpp/napi_init.cpp](kmptcp_kotlin_sample/harmonyapp/entry/src/main/cpp/napi_init.cpp)
- `dlsym(..., "getStaticLibSum")` 并封装为 `NAPI_Global_getStaticLibSum`
- 在 `Init` 中注册 `{ "getStaticLibSum", ..., NAPI_Global_getStaticLibSum }`
- ArkTS 类型声明
[harmonyapp/entry/src/main/cpp/types/libentry/Index.d.ts](kmptcp_kotlin_sample/harmonyapp/entry/src/main/cpp/types/libentry/Index.d.ts)
- `export const getStaticLibSum: (a: number, b: number) => number;`
- 页面调用
[harmonyapp/entry/src/main/ets/pages/Index.ets](kmptcp_kotlin_sample/harmonyapp/entry/src/main/ets/pages/Index.ets)
- 将 `refreshSum()` 改为调用 `testNapi.getStaticLibSum(2, 3)` 并显示结果
- 构建与验证
- 生成静态库 `.a`
```bash
./gradlew :composeApp:buildSampleStaticLib -x test
```
成功后产物位于:`composeApp/native-libs/ohosArm64/libsample.a`
- 链接 ohosArm64 并发布到鸿蒙工程
```bash
./gradlew :composeApp:linkDebugSharedOhosArm64 -x test
./gradlew :composeApp:publishDebugBinariesToHarmonyApp -x test
```
- 运行 HAP,页面“静态库计算”卡片应显示:`add(2, 3) = 5`
### StaticLib 复用到 Harmony CMake(libsample.a)
- 目标
复用 `composeApp` 侧构建的 `libsample.a`,在 Harmony 原生 `CMakeLists.txt` 中以 IMPORTED 静态库的方式链接到 `libentry.so`,并通过 NAPI/ArkTS 直接调用 `sample_add`。
- 拷贝与产物位置
- Gradle 任务 `publishDebugBinariesToHarmonyApp` / `publishReleaseBinariesToHarmonyApp` 在拷贝 `libkn.so` 的同时,额外拷贝:
- 源:`composeApp/native-libs/ohosArm64/libsample.a`
- 目标:`harmonyapp/entry/src/main/cpp/libs/libsample.a`
- 配置位置:[composeApp/build.gradle.kts](kmptcp_kotlin_sample/composeApp/build.gradle.kts#L130-L147)
- CMake 链接
- 在 [harmonyapp/entry/src/main/cpp/CMakeLists.txt](kmptcp_kotlin_sample/harmonyapp/entry/src/main/cpp/CMakeLists.txt#L11-L20) 中新增 IMPORTED 静态库,并链接到 `entry`:
- `add_library(sample STATIC IMPORTED)`
- `set_target_properties(sample PROPERTIES IMPORTED_LOCATION "${NATIVERENDER_ROOT_PATH}/libs/libsample.a")`
- `target_link_libraries(entry PRIVATE arith sample)`
- NAPI 封装
- 在 [harmonyapp/entry/src/main/cpp/napi_init.cpp](kmptcp_kotlin_sample/harmonyapp/entry/src/main/cpp/napi_init.cpp) 中:
- 声明 C 接口:`extern "C" int sample_add(int a, int b);`
- 新增封装函数 `NAPI_Global_getSampleAdd`,内部调用 `sample_add` 完成加法并返回 `napi_value`
- 在 `Init` 中注册 `{ "getSampleAdd", ..., NAPI_Global_getSampleAdd }`
- ArkTS 类型与页面调用
- 类型声明:[harmonyapp/entry/src/main/cpp/types/libentry/Index.d.ts](kmptcp_kotlin_sample/harmonyapp/entry/src/main/cpp/types/libentry/Index.d.ts):
- `export const getSampleAdd: (a: number, b: number) => number;`
- 页面调用:[harmonyapp/entry/src/main/ets/pages/Index.ets](kmptcp_kotlin_sample/harmonyapp/entry/src/main/ets/pages/Index.ets):
- 状态字段:`@State sampleMsg: string = '等待 Sample 静态库结果...';`
- UI 展示:在“静态库计算”卡片中新增 `sample_add(4, 5) = ${this.sampleMsg}`
- 刷新逻辑:`refreshSampleAdd()` 中调用 `testNapi.getSampleAdd(4, 5)` 并将结果写入 `sampleMsg`
- 注意事项
- 首次执行 Kotlin/Native 任务若提示 `~/.konan/.../.lock (Operation not permitted)`,需为用户库目录授予写权限或在 IDE 中运行构建。
- `.a` 必须使用 `-fPIC` 以便安全地合入共享库。
- 若在模拟器验证,需要提供 x64 架构的 `.a` 与 `libkn.so`。
### ohosArm64 SharedLib / StaticLib 产物(libkn.so / libkn.a)
- 目标
在 `ohosArm64` 目标下同时生成共享库 `libkn.so` 和静态库 `libkn.a`,二者都链接 `libsample.a`,便于在不同场景复用。
- Gradle 配置
- 位置:[composeApp/build.gradle.kts](kmptcp_kotlin_sample/composeApp/build.gradle.kts#L13-L31)
- 在 `ohosArm64 { binaries { ... } }` 下声明:
- `sharedLib { baseName = "kn"; linkerOpts("-L.../native-libs/ohosArm64", "--whole-archive", "-lsample", "--no-whole-archive") }`
- `staticLib { baseName = "kn"; linkerOpts("-L.../native-libs/ohosArm64", "--whole-archive", "-lsample", "--no-whole-archive") }`
- 构建命令与产物位置
- 调试版共享库:
```bash
./gradlew :composeApp:linkDebugSharedOhosArm64 -x test
```
- 产物:`composeApp/build/bin/ohosArm64/debugShared/libkn.so`
- 调试版静态库:
```bash
./gradlew :composeApp:linkDebugStaticOhosArm64 -x test
```
- 产物:`composeApp/build/bin/ohosArm64/debugStatic/libkn.a`
- 发布版静态库:
```bash
./gradlew :composeApp:linkReleaseStaticOhosArm64 -x test
```
- 产物:`composeApp/build/bin/ohosArm64/releaseStatic/libkn.a`
- 使用建议
- `libkn.so`:通过 Harmony NAPI(`dlopen/dlsym`)供 ArkTS 调用(当前工程已集成)。
- `libkn.a`:可在其他原生 CMake/NDK 工程中静态链接 KMP 导出的符号(需要自行设置 include 路径与链接选项)。
### 常见问题与排查
- `dlopen/dlsym libkn.so|<symbol> 失败`
- 确认 `libkn.so` 已拷贝到 `harmonyapp/entry/libs/arm64-v8a/`(真机)或 `x86_64/`(模拟器)。
- 确认架构匹配(arm64-v8a vs x86_64)。
- 确认 Kotlin 侧导出使用了 `@CName("...")`,符号名与 NAPI 的 `dlsym` 一致。
- 构建后重新安装 HAP(避免加载旧版本)。
- ArkTS 类型报错或属性不存在
- 以 `src/main/cpp/types/libentry/Index.d.ts` 为主,补齐 NAPI 导出函数的声明(`add`、`getKotlinUiText`、`getStdlibListDemo`、`getStdlibAggregateDemo`)。
- 页面调用时加显式类型(`const v: number = ...`),满足 lint 规则。
- `libarith.a` 未找到
- 这是构建中间产物,被链接进 `libentry.so`;需要在本地构建目录中查找:
```bash
find harmonyapp/entry -name "libarith.a"
```
- 运行效果验证
- 执行 `hvigor build` 后安装运行 HAP,页面展示:
- Kotlin 文案:`getKotlinUiText()` 的返回内容
- 静态库加法结果:`add(2, 3) = 5`
### 类型声明与编译规则(ArkTS)
- 模块类型文件
- ArkTS 编译器使用 `harmonyapp/entry/src/main/cpp/types/libentry/Index.d.ts` 作为 NAPI 导出类型源。确保其中包含:
- `export const getKotlinGreeting: () => string;`
- `export const getKotlinUiText: () => string;`
- `export const add: (a: number, b: number) => number;`(用于 staticLib 的加法)
- 显式类型要求
- 受 `arkts-no-any-unknown` 规则影响,页面中建议使用显式类型注解。例如:
```ts
const v: number = testNapi.add(2, 3);
```
## 常见问题与排查
- iOS “import ComposeApp: no such module”
- 未链接当前架构框架或搜索路径错误。检查 `FRAMEWORK_SEARCH_PATHS` 与 `Embed & Sign`
- iOS 模拟器版本被锁到 18.2
- 在 `project.pbxproj` 有 `IPHONEOS_DEPLOYMENT_TARGET = 18.2;`;建议迁移到 `Config.xcconfig` 设置通用版本(如 `17.0`)
- KMP iOS C-Interop(UIKit)缺少 Opt-In
- 在 `MainViewController.kt` 顶部添加 `@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)`
- Harmony NAPI 接口 undefined 或类型不匹配
- 确认 `napi_init.cpp` 已注册并导出 `add`、`getKotlinUiText`;`CMakeLists.txt` 包含对应源并链接静态库。
- 确认 `src/main/cpp/types/libentry/Index.d.ts` 已包含 `add` 的类型定义;页面调用使用显式类型(如 `const v: number`)。
- Gradle JDK 路径不应写死
- 删除 `gradle.properties` 的 `org.gradle.java.home`,在 IDE 中选择 JDK 17;在 Gradle 脚本里使用 `jvmToolchain(17)` 保证一致性
- Failed to transform kotlin-native-prebuilt-2.2.21-ohos-01-macos-x86_64.tar.gz问题是 Gradle/Kotlin 插件错误地检测成了 x86_64
- 在 `gradle.properties`中添加`systemProp.os.arch=aarch64`,强制使用arm版本。
## 问题总结与修复记录
- ~/.konan 权限导致锁文件不可写
- 现象:`java.io.FileNotFoundException: ~/.konan/.../.lock (Operation not permitted)`
- 原因:用户目录下的 Kotlin/Native 缓存锁文件权限异常(可能之前用 sudo 构建)
- 修复建议:
- `./gradlew --stop && pkill -f kotlin`
- `sudo chown -R $(whoami) ~/.konan && sudo chmod -R u+w ~/.konan`
- 如有隔离属性:`xattr -r -d com.apple.quarantine ~/.konan 2>/dev/null`
- cinterop 头文件找不到(sample.h file not found)
- 现象:`fatal error: 'sample.h' file not found`
- 原因:.def 中 headers 写死或相对路径不受 -I 解析;临时编译单元无法定位头文件
- 修复:
- 将头文件放置在源码路径:[include/sample.h](kmptcp_kotlin_sample/composeApp/src/nativeInterop/cinterop/include/sample.h)
- .def 中使用相对声明:[sample.def](kmptcp_kotlin_sample/composeApp/src/nativeInterop/cinterop/sample.def):
- `headers = sample.h`
- `headerFilter = sample.h`
- Gradle 配置 includeDirs 与 compilerOpts:
- [build.gradle.kts:ohosArm64.cinterops](kmptcp_kotlin_sample/composeApp/build.gradle.kts#L13-L19)
- 避免写死绝对路径(/Users/...)
- 措施:
- .def 使用相对 headers 与 headerFilter
- Gradle 通过 `${project.projectDir}` 拼接 -I/-L,在构建时解析
- README 链接使用相对/项目路径引用,避免主机路径写死
- 静态库链接位置与 cinterop 警告
- 现象:cinterop 警告 `-linker-option(s) not supported by cinterop`
- 结论:链接选项应在二进制阶段添加,不在 cinterop 阶段
- 修复:在 sharedLib 二进制配置添加:
- `linkerOpts("-L${project.projectDir}/native-libs/ohosArm64", "-lsample")`
- 位置:[build.gradle.kts:binaries.sharedLib](kmptcp_kotlin_sample/composeApp/build.gradle.kts#L16-L19)
- JDK 版本不匹配导致 Gradle 插件解析失败
- 现象:`Dependency requires at least JVM runtime version 11. This build uses a Java 8 JVM`
- 修复:确保 Gradle/IDE 使用 JDK 17(或至少 11)
- 命令行示例:`JAVA_HOME=/path/to/jdk-17 ./gradlew ...`
- IDE 中设置 Gradle JDK 为 17
- Kotlin/Native API 使用需 Opt-In
- 现象:编译错误提示需要 `ExperimentalNativeApi` 或 `ExperimentalForeignApi`
- 修复:在 Kotlin 文件顶端添加:
- `@file:OptIn(ExperimentalNativeApi::class, ExperimentalForeignApi::class)`
- 位置:[StaticLibDemo.kt](kmptcp_kotlin_sample/composeApp/src/ohosArm64Main/kotlin/com/example/testdemo/StaticLibDemo.kt#L1-L13)
- Kotlin/Native 生成 C 代码的编译器告警
- 现象:`non-void function does not return a value`、`C99 designated initializer` 混合等警告
- 结论:为 Kotlin/Native 自动生成代码的已知告警,不影响当前功能;可忽略
- 构建结果
- 已成功执行:
- `:composeApp:cinteropSampleOhosArm64`
- `:composeApp:linkDebugSharedOhosArm64`
- `:composeApp:publishDebugBinariesToHarmonyApp`
- 页面验证:`getStaticLibSum(2, 3)` 正常显示 5
- 运行时崩溃 SIGSEGV (0x00000000000d89e0 Not mapped)
- 现象:调用静态库方法时 Crash,堆栈显示在 `libkn.so` 内偏移处崩溃,nm 查看 `sample_add` 符号为 `U` (Undefined)
- 原因:
1. macOS 上 `clang` 默认生成 Mach-O 格式目标文件,而 HarmonyOS 需要 ELF 格式。链接器忽略了格式错误的 .a 文件。
2. 静态库符号被链接器视为未使用而剥离(dead stripping)。
- 修复:
- 编译静态库时指定 `-target aarch64-linux-gnu` 以生成 ELF。
- 链接时使用 `--whole-archive` 强制包含静态库所有符号:
- `linkerOpts(..., "--whole-archive", "-lsample", "--no-whole-archive")`
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}
\ No newline at end of file
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.gradle.process.ExecOperations
import org.gradle.kotlin.dsl.support.serviceOf
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
}
kotlin {
// 让 Kotlin 使用 JDK 17 运行编译任务
jvmToolchain(17)
androidTarget()
ohosArm64 {
compilations.getByName("main") {
cinterops {
val sample by creating {
defFile(project.file("src/nativeInterop/cinterop/sample.def"))
includeDirs(project.file("src/nativeInterop/cinterop/include"))
compilerOpts("-I${project.projectDir}/src/nativeInterop/cinterop/include")
}
}
}
binaries {
sharedLib {
baseName = "kn"
// outputDirectory = file("customer-output")
// outputFile 是只读属性,由 outputDirectory 和 baseName 决定
// println("Output file path: ${outputFile.absolutePath}")
freeCompilerArgs += listOf("-Xbinary=sanitizer=address")
// 链接静态库目录和库名,使用 --whole-archive 强制包含静态库所有符号
linkerOpts("-L${project.projectDir}/native-libs/ohosArm64", "--whole-archive", "-lsample", "--no-whole-archive")
// export(project(":lib-arith")) {
// transitiveExport = false
// }
}
staticLib {
baseName = "kn"
linkerOpts("-L${project.projectDir}/native-libs/ohosArm64", "--whole-archive", "-lsample", "--no-whole-archive")
}
}
}
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
val commonMain by getting
val iosMain by creating {
dependsOn(commonMain)
dependencies {
implementation(project(":lib-arith"))
}
}
val iosSimulatorArm64Main by getting {
dependsOn(iosMain)
}
val ohosMain by creating {
dependsOn(commonMain)
dependencies {
api(project(":lib-arith"))
implementation(project(":lib-analytics"))
}
}
val ohosArm64Main by getting {
dependsOn(ohosMain)
}
androidMain.dependencies {
implementation(libs.androidx.activity.compose)
implementation(project(":lib-arith"))
}
commonMain.dependencies {
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
android {
namespace = "com.example.kotlin_sample"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.kotlin_sample.kotlin_sample"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
// 移除 Compose 调试工具
}
// 构建示例静态库(arm64)
val buildSampleStaticLib by tasks.registering {
group = "native"
description = "Build sample static library (.a) for ohosArm64"
val projectDir = project.projectDir
val projectDirPath = projectDir.absolutePath
val includeDir = "$projectDirPath/native-libs/sample/include"
val outDir = "$projectDirPath/native-libs/ohosArm64"
val staticLibPath = "$outDir/libsample.a"
val srcDirPath = "$projectDirPath/native-libs/sample/src"
val outDirFile = File(outDir)
val staticLibFile = File(staticLibPath)
val srcDirFile = File(srcDirPath)
val konanUserDir = System.getProperty("user.home") + "/.konan"
val konanDependenciesDir = File("$konanUserDir/dependencies")
val execOps = project.serviceOf<ExecOperations>()
doLast {
val objFileToDelete = File("$outDir/sample.o")
outDirFile.mkdirs()
if (staticLibFile.exists()) {
staticLibFile.delete()
}
if (objFileToDelete.exists()) {
objFileToDelete.delete()
}
// 查找 Kotlin Native 下载的 LLVM 工具链
val llvmDir = konanDependenciesDir.listFiles()?.firstOrNull {
it.name.startsWith("llvm-") && it.name.contains("ohos")
} ?: throw GradleException("Kotlin Native LLVM toolchain for OHOS not found in ${konanDependenciesDir.absolutePath}")
val isWindows = org.apache.tools.ant.taskdefs.condition.Os.isFamily(org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS)
val exeSuffix = if (isWindows) ".exe" else ""
// 动态查找 Clang 编译器:优先找带 ohos 前缀的,否则找通用的 clang
val binDir = File(llvmDir, "bin")
val clangExecutable = binDir.listFiles()?.firstOrNull {
it.name.startsWith("aarch64-") && it.name.contains("ohos") && it.name.endsWith("clang$exeSuffix")
} ?: File(binDir, "clang$exeSuffix")
val arExecutable = File(binDir, "llvm-ar$exeSuffix")
if (!clangExecutable.exists()) {
throw GradleException("Clang compiler not found at ${clangExecutable.absolutePath}")
}
println("Host OS: ${System.getProperty("os.name")}")
println("Using clang: ${clangExecutable.absolutePath}")
println("Using ar: ${arExecutable.absolutePath}")
val objectFiles = mutableListOf<String>()
// 编译 .c 和 .S 文件
srcDirFile.listFiles { _, name -> name.endsWith(".c") || name.endsWith(".S") }?.forEach { srcFile ->
val objFile = "$outDir/${srcFile.nameWithoutExtension}.o"
val objFileFile = File(objFile)
if (objFileFile.exists()) {
objFileFile.delete()
}
println("Compiling ${srcFile.name}...")
execOps.exec {
// 使用 OHOS SDK clang 编译,它会自动处理 target 和 sysroot
commandLine(clangExecutable.absolutePath, "-c", "-fPIC", "-O2", "-I", includeDir, srcFile.absolutePath, "-o", objFile)
}
objectFiles.add(objFile)
}
if (objectFiles.isEmpty()) {
throw GradleException("No source files (.c, .S) found in $srcDirPath")
}
execOps.exec {
// 使用 llvm-ar 创建 archive,确保格式兼容 lld
commandLine(listOf(arExecutable.absolutePath, "rcs", staticLibPath) + objectFiles)
}
}
}
arrayOf("debug", "release").forEach { type ->
val capitalizedType = type.replaceFirstChar { it.uppercase() }
tasks.register<Copy>("publish${capitalizedType}BinariesToHarmonyApp") {
group = "harmony"
dependsOn("link${capitalizedType}SharedOhosArm64")
into(rootProject.file("harmonyApp"))
val binaryPath = "build/bin/ohosArm64/${type}Shared"
val binaryDir = file(binaryPath)
from("$binaryPath/libkn_api.h") {
into("entry/src/main/cpp/include/")
}
from("$binaryPath/libkn.so") {
into("entry/libs/arm64-v8a/")
}
from("native-libs/ohosArm64/libsample.a") {
into("entry/src/main/cpp/libs/")
}
doFirst {
if (!binaryDir.exists()) {
throw GradleException("未找到产物目录: ${binaryDir.absolutePath}")
}
}
}
tasks.named("link${capitalizedType}SharedOhosArm64").configure {
dependsOn(buildSampleStaticLib)
}
}
#ifndef KONAN_LIBKN_H
#define KONAN_LIBKN_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
typedef bool libkn_KBoolean;
#else
typedef _Bool libkn_KBoolean;
#endif
typedef unsigned short libkn_KChar;
typedef signed char libkn_KByte;
typedef short libkn_KShort;
typedef int libkn_KInt;
typedef long long libkn_KLong;
typedef unsigned char libkn_KUByte;
typedef unsigned short libkn_KUShort;
typedef unsigned int libkn_KUInt;
typedef unsigned long long libkn_KULong;
typedef float libkn_KFloat;
typedef double libkn_KDouble;
typedef float __attribute__ ((__vector_size__ (16))) libkn_KVector128;
typedef void* libkn_KNativePtr;
struct libkn_KType;
typedef struct libkn_KType libkn_KType;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Byte;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Short;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Int;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Long;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Float;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Double;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Char;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Boolean;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Unit;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_UByte;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_UShort;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_UInt;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_ULong;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_com_example_arith_Arith;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_com_example_kotlin_sample_Platform;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_com_example_kotlin_sample_Greeting;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_com_example_kotlin_sample_OhosPlatform;
extern const char* getKotlinAnalyticsInfo();
extern const char* getKotlinGreeting();
extern const char* getKotlinUiText();
extern void triggerCpuFreeze();
extern libkn_KInt triggerKotlinHeapOverflow();
extern libkn_KInt triggerKotlinHeapUseAfterFree();
extern libkn_KInt triggerKotlinNpe();
extern libkn_KInt triggerKotlinOutOfBounds();
extern libkn_KInt triggerKotlinStackBufferOverflow();
extern void triggerKotlinToCCrash();
extern void* getStaticLibImage();
extern libkn_KInt getStaticLibImageSize();
extern libkn_KInt getStaticLibSum(libkn_KInt a, libkn_KInt b);
extern const char* getStaticLibVersion();
typedef struct {
/* Service functions. */
void (*DisposeStablePointer)(libkn_KNativePtr ptr);
void (*DisposeString)(const char* string);
libkn_KBoolean (*IsInstance)(libkn_KNativePtr ref, const libkn_KType* type);
libkn_kref_kotlin_Byte (*createNullableByte)(libkn_KByte);
libkn_KByte (*getNonNullValueOfByte)(libkn_kref_kotlin_Byte);
libkn_kref_kotlin_Short (*createNullableShort)(libkn_KShort);
libkn_KShort (*getNonNullValueOfShort)(libkn_kref_kotlin_Short);
libkn_kref_kotlin_Int (*createNullableInt)(libkn_KInt);
libkn_KInt (*getNonNullValueOfInt)(libkn_kref_kotlin_Int);
libkn_kref_kotlin_Long (*createNullableLong)(libkn_KLong);
libkn_KLong (*getNonNullValueOfLong)(libkn_kref_kotlin_Long);
libkn_kref_kotlin_Float (*createNullableFloat)(libkn_KFloat);
libkn_KFloat (*getNonNullValueOfFloat)(libkn_kref_kotlin_Float);
libkn_kref_kotlin_Double (*createNullableDouble)(libkn_KDouble);
libkn_KDouble (*getNonNullValueOfDouble)(libkn_kref_kotlin_Double);
libkn_kref_kotlin_Char (*createNullableChar)(libkn_KChar);
libkn_KChar (*getNonNullValueOfChar)(libkn_kref_kotlin_Char);
libkn_kref_kotlin_Boolean (*createNullableBoolean)(libkn_KBoolean);
libkn_KBoolean (*getNonNullValueOfBoolean)(libkn_kref_kotlin_Boolean);
libkn_kref_kotlin_Unit (*createNullableUnit)(void);
libkn_kref_kotlin_UByte (*createNullableUByte)(libkn_KUByte);
libkn_KUByte (*getNonNullValueOfUByte)(libkn_kref_kotlin_UByte);
libkn_kref_kotlin_UShort (*createNullableUShort)(libkn_KUShort);
libkn_KUShort (*getNonNullValueOfUShort)(libkn_kref_kotlin_UShort);
libkn_kref_kotlin_UInt (*createNullableUInt)(libkn_KUInt);
libkn_KUInt (*getNonNullValueOfUInt)(libkn_kref_kotlin_UInt);
libkn_kref_kotlin_ULong (*createNullableULong)(libkn_KULong);
libkn_KULong (*getNonNullValueOfULong)(libkn_kref_kotlin_ULong);
/* User functions. */
struct {
struct {
struct {
struct {
struct {
struct {
libkn_KType* (*_type)(void);
libkn_kref_com_example_arith_Arith (*_instance)();
libkn_KInt (*add)(libkn_kref_com_example_arith_Arith thiz, libkn_KInt a, libkn_KInt b);
libkn_KInt (*sub)(libkn_kref_com_example_arith_Arith thiz, libkn_KInt a, libkn_KInt b);
} Arith;
} arith;
struct {
struct {
libkn_KType* (*_type)(void);
libkn_kref_com_example_kotlin_sample_Greeting (*Greeting)();
const char* (*greet)(libkn_kref_com_example_kotlin_sample_Greeting thiz);
} Greeting;
struct {
libkn_KType* (*_type)(void);
const char* (*get_name)(libkn_kref_com_example_kotlin_sample_Platform thiz);
} Platform;
struct {
libkn_KType* (*_type)(void);
libkn_kref_com_example_kotlin_sample_OhosPlatform (*OhosPlatform)();
const char* (*get_name)(libkn_kref_com_example_kotlin_sample_OhosPlatform thiz);
} OhosPlatform;
const char* (*appInfo)();
const char* (*getKotlinAnalyticsInfo_)();
const char* (*getKotlinGreeting_)();
const char* (*getKotlinUiText_)();
const char* (*ohosUiText)();
void (*triggerCpuFreeze_)();
libkn_KInt (*triggerKotlinHeapOverflow_)();
libkn_KInt (*triggerKotlinHeapUseAfterFree_)();
libkn_KInt (*triggerKotlinNpe_)();
libkn_KInt (*triggerKotlinOutOfBounds_)();
libkn_KInt (*triggerKotlinStackBufferOverflow_)();
void (*triggerKotlinToCCrash_)();
void* (*getStaticLibImage_)();
libkn_KInt (*getStaticLibImageSize_)();
libkn_KInt (*getStaticLibSum_)(libkn_KInt a, libkn_KInt b);
const char* (*getStaticLibVersion_)();
libkn_kref_com_example_kotlin_sample_Platform (*getPlatform)();
} kotlin_sample;
} example;
} com;
} root;
} kotlin;
} libkn_ExportedSymbols;
extern libkn_ExportedSymbols* libkn_symbols(void);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* KONAN_LIBKN_H */
#ifndef SAMPLE_H
#define SAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
int sample_add(int a, int b);
const char* sample_version(void);
void trigger_c_crash(void);
const unsigned char* get_sample_image(void);
int get_sample_image_size(void);
#ifdef __cplusplus
}
#endif
#endif
.section .rodata
.balign 16
sample_image_data:
.incbin "native-libs/sample/src/resources/logo.png"
sample_image_end:
.section .text
.global get_sample_image
.global get_sample_image_size
.balign 4
// 实现 get_sample_image 函数
// const unsigned char* get_sample_image(void)
get_sample_image:
adrp x0, sample_image_data
add x0, x0, :lo12:sample_image_data
ret
// 实现 get_sample_image_size 函数
// int get_sample_image_size(void)
get_sample_image_size:
// 计算长度:sample_image_end - sample_image_data
// 加载 end 地址
adrp x0, sample_image_end
add x0, x0, :lo12:sample_image_end
// 加载 start 地址
adrp x1, sample_image_data
add x1, x1, :lo12:sample_image_data
// 相减
sub x0, x0, x1
ret
#include "sample.h"
int sample_add(int a, int b) {
return a + b;
}
const char* sample_version(void) {
return "sample-1.0";
}
void trigger_c_crash(void) {
volatile int *p = 0;
*p = 42;
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:exported="true"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
package com.example.kotlin_sample
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val message = com.example.kotlin_sample.appInfo()
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setPadding(24, 24, 24, 24)
}
val msgView = TextView(this).apply {
text = message
textSize = 24f
}
val resultAdd = com.example.arith.Arith.add(7, 3)
val resultSub = com.example.arith.Arith.sub(7, 3)
val addView = TextView(this).apply {
text = "Add is ${resultAdd}, Sub is ${resultSub}"
textSize = 24f
}
root.addView(msgView)
root.addView(addView)
setContentView(root)
}
}
\ No newline at end of file
package com.example.kotlin_sample
import android.os.Build
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<resources>
<string name="app_name">Kotlinsample</string>
</resources>
\ No newline at end of file
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="450dp"
android:height="450dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M56.25,18V46L32,60 7.75,46V18L32,4Z"
android:fillColor="#6075f2"/>
<path
android:pathData="m41.5,26.5v11L32,43V60L56.25,46V18Z"
android:fillColor="#6b57ff"/>
<path
android:pathData="m32,43 l-9.5,-5.5v-11L7.75,18V46L32,60Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="23.131"
android:centerY="18.441"
android:gradientRadius="42.132"
android:type="radial">
<item android:offset="0" android:color="#FF5383EC"/>
<item android:offset="0.867" android:color="#FF7F52FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M22.5,26.5 L32,21 41.5,26.5 56.25,18 32,4 7.75,18Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="44.172"
android:startY="4.377"
android:endX="17.973"
android:endY="34.035"
android:type="linear">
<item android:offset="0" android:color="#FF33C3FF"/>
<item android:offset="0.878" android:color="#FF5383EC"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m32,21 l9.526,5.5v11L32,43 22.474,37.5v-11z"
android:fillColor="#000000"/>
</vector>
\ No newline at end of file
package com.example.kotlin_sample
/**
* 跨平台共享逻辑函数
* 它调用了期望函数 getPlatform(),并根据不同平台的实现返回对应的文字
*/
fun appInfo(): String {
// 调用 Platform.kt 中定义的 expect 函数
val platform = getPlatform()
return "Hello World! \n" +
"Current Platform: ${platform.name} \n" +
"This message is shared across Android, iOS, and HarmonyOS."
}
\ No newline at end of file
package com.example.kotlin_sample
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
\ No newline at end of file
package com.example.kotlin_sample
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
\ No newline at end of file
package com.example.kotlin_sample
import kotlin.test.Test
import kotlin.test.assertEquals
class ComposeAppCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}
\ No newline at end of file
package com.example.kotlin_sample
import kotlin.test.Test
import kotlin.test.assertTrue
class PlatformExpectActualCommonTest {
@Test
fun getPlatformReturnsNonBlankNameOnAllPlatforms() {
assertTrue(getPlatform().name.isNotBlank(), "Platform name should not be blank")
}
}
package com.example.kotlin_sample
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class StdlibTest {
@Test
fun testListOperations() {
// 前置条件:准备原始数据
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 步骤:执行链式操作
// 1. filter: 过滤偶数
// 2. map: 数值 * 10
// 3. sortedDescending: 降序排列
// 4. take: 取前3个
val result = numbers
.filter { it % 2 == 0 } // [2, 4, 6, 8, 10]
.map { it * 10 } // [20, 40, 60, 80, 100]
.sortedDescending() // [100, 80, 60, 40, 20]
.take(3) // [100, 80, 60]
// 验证结果
val expected = listOf(100, 80, 60)
assertEquals(expected, result, "List chain operations (filter/map/sort) should match expected result")
}
@Test
fun testComplexObjectList() {
data class User(val name: String, val age: Int)
// 前置条件:准备对象列表
val users = listOf(
User("Alice", 25),
User("Bob", 30),
User("Charlie", 20)
)
// 步骤:查找最年长的用户名称
val oldestUserName = users.maxByOrNull { it.age }?.name
// 验证
assertEquals("Bob", oldestUserName, "Should find the user with max age")
}
@Test
fun testStringManipulation() {
// 前置条件
val raw = " Kotlin Multiplatform "
// 步骤
val result = raw.trim().uppercase().replace("MULTIPLATFORM", "KMP")
// 验证
assertEquals("KOTLIN KMP", result, "String trimming and replacement should work")
}
}
package com.example.kotlin_sample
import platform.UIKit.UIDevice
class IOSPlatform : Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
actual fun getPlatform(): Platform = IOSPlatform()
\ No newline at end of file
package com.example.kotlin_sample
import com.example.arith.Arith
fun iosUiText(): String {
val base = appInfo()
val add = Arith.add(7, 3)
val sub = Arith.sub(7, 3)
return "$base\nAdd is $add, Sub is $sub"
}
fun appInfoForIos(): String = appInfo()
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
package com.example.kotlin_sample
import platform.UIKit.UIViewController
import platform.UIKit.UIScreen
import platform.UIKit.UILabel
import platform.UIKit.NSTextAlignmentCenter
fun MainViewController(): UIViewController {
val vc = UIViewController()
val label = UILabel(frame = UIScreen.mainScreen.bounds)
label.text = appInfo()
label.textAlignment = NSTextAlignmentCenter
label.numberOfLines = 0
vc.view.addSubview(label)
return vc
}
\ No newline at end of file
package com.example.kotlin_sample
import platform.UIKit.UIDevice
class IOSPlatform : Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
actual fun getPlatform(): Platform = IOSPlatform()
\ No newline at end of file
#ifndef SAMPLE_H
#define SAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
int sample_add(int a, int b);
const char* sample_version(void);
void trigger_c_crash(void);
const unsigned char* get_sample_image(void);
int get_sample_image_size(void);
#ifdef __cplusplus
}
#endif
#endif
headers = sample.h
package = sample
headerFilter = sample.h
@file:OptIn(ExperimentalNativeApi::class, ExperimentalForeignApi::class)
package com.example.kotlin_sample
import kotlinx.cinterop.ExperimentalForeignApi
import kotlin.experimental.ExperimentalNativeApi
import com.example.arith.Arith
import com.example.analytics.analyticsInfo
import kotlinx.cinterop.*
import platform.posix.*
import sample.*
import kotlin.native.concurrent.*
// 既然去掉了 Compose,我们先通过 NAPI 返回一个简单的标识,证明 Kotlin 代码已成功运行
@CName("getKotlinGreeting")
fun getKotlinGreeting(): String {
return Greeting().greet() + " (HarmonyOS Native)"
}
@CName("getKotlinUiText")
fun getKotlinUiText(): String = ohosUiText()
fun ohosUiText(): String {
val base = appInfo()
val add = Arith.add(7, 3)
val sub = Arith.sub(7, 3)
return "$base\nAdd is $add, Sub is $sub"
}
@CName("getKotlinAnalyticsInfo")
fun getKotlinAnalyticsInfo(): String = analyticsInfo()
@CName("triggerKotlinHeapOverflow")
fun triggerKotlinHeapOverflow(): Int {
val size = 16
val ptr = malloc(size.convert())!!.reinterpret<ByteVar>()
memset(ptr, 0, (size + 8).convert())
val result = ptr[1].toInt()
free(ptr)
return result
}
@CName("triggerCpuFreeze")
fun triggerCpuFreeze(): Unit {
println("Starting CPU Freeze for 10 seconds...")
val startTime = time(null)
// 强制阻塞 10 秒,确保触发 LIFECYCLE_TIMEOUT
// 使用 volatile 防止编译器优化循环
while (time(null) - startTime < 10) {
// Busy loop
}
println("ArkTS Freeze Done")
}
@CName("triggerKotlinStackBufferOverflow")
fun triggerKotlinStackBufferOverflow(): Int = memScoped {
println("triggerKotlinStackBufferOverflow called")
val subscript = 43
val buffer = allocArray<ByteVar>(42)
buffer[subscript] = 42.toByte()
println("address: $buffer")
return@memScoped 0
}
@CName("triggerKotlinHeapUseAfterFree")
fun triggerKotlinHeapUseAfterFree(): Int {
println("triggerKotlinHeapUseAfterFree called")
val size = 32
val ptr = malloc(size.convert())!!
println("Allocated address: $ptr")
free(ptr)
println("Freed address, now trying to memset use-after-free...")
// 释放后写入,使用 memset 防止编译器优化移除代码,且 memset 通常会被 ASAN 拦截检查
memset(ptr, 0xEE, 1.convert())
return 0
}
@CName("triggerKotlinNpe")
fun triggerKotlinNpe(): Int {
println("triggerKotlinNpe called")
val str: String? = null
return str!!.length
}
@CName("triggerKotlinOutOfBounds")
fun triggerKotlinOutOfBounds(): Int {
println("triggerKotlinOutOfBounds called")
val list = listOf(1, 2, 3)
return list[10]
}
@CName("triggerKotlinToCCrash")
fun triggerKotlinToCCrash(): Unit {
println("triggerKotlinToCCrash called")
trigger_c_crash()
}
\ No newline at end of file
@file:OptIn(ExperimentalNativeApi::class, ExperimentalForeignApi::class)
package com.example.kotlin_sample
import kotlinx.cinterop.ExperimentalForeignApi
import kotlin.experimental.ExperimentalNativeApi
import kotlinx.cinterop.toKString
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.UByteVar
import sample.sample_add
import sample.sample_version
import sample.get_sample_image
import sample.get_sample_image_size
@CName("getStaticLibSum")
fun getStaticLibSum(a: Int, b: Int): Int = sample_add(a, b)
@CName("getStaticLibVersion")
fun getStaticLibVersion(): String = sample_version()?.toKString() ?: "unknown"
@CName("getStaticLibImage")
fun getStaticLibImage(): CPointer<UByteVar>? = get_sample_image()
@CName("getStaticLibImageSize")
fun getStaticLibImageSize(): Int = get_sample_image_size()
package com.example.kotlin_sample
import kotlin.test.BeforeTest
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import platform.posix.*
import platform.PerformanceAnalysisKit.HiLog.*
private fun hilogInfo(tag: String, message: String) {
// 使用 printf 确保 hdc shell 可见
printf("[$tag][INFO] $message\n")
// 同时写入 HiLog
OH_LOG_Print(
0u,
4u,
0x0000u, // domain
tag,
"%{public}s", // fmt
message // args
)
}
class OhosPlatformTest {
@BeforeTest
fun setup() {
hilogInfo("OhosPlatformTest", "Starting test...")
}
@AfterTest
fun tearDown() {
hilogInfo("OhosPlatformTest", "Test finished.")
}
@Test
fun testGetPlatform() {
hilogInfo("OhosPlatformTest", "Running testGetPlatform")
val platformName = getPlatform().name
hilogInfo("OhosPlatformTest", "Platform name: $platformName")
assertEquals("HarmonyOS", platformName)
}
}
package com.example.kotlin_sample
// composeApp/src/ohosArm64Main/kotlin/com/example/testdemo/Platform.ohos.kt
class OhosPlatform : Platform {
override val name: String = "HarmonyOS"
}
actual fun getPlatform(): Platform = OhosPlatform()
\ No newline at end of file
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
kotlin.mpp.applyDefaultHierarchyTemplate=false
#Gradle
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true
[versions]
agp = "8.11.2"
android-compileSdk = "36"
android-minSdk = "24"
android-targetSdk = "36"
androidx-activity = "1.12.0"
androidx-appcompat = "1.7.1"
androidx-core = "1.17.0"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.9.6"
androidx-testExt = "1.3.0"
junit = "4.13.2"
kotlin = "2.2.21-ez-101"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test
/.appanalyzer
\ No newline at end of file
{
"app": {
"bundleName": "com.example.harmonyapp",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name",
"asanEnabled": true
}
}
{
"string": [
{
"name": "app_name",
"value": "harmonyApp"
}
]
}
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
\ No newline at end of file
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.0.0(20)",
"compatibleSdkVersion": "6.0.0(20)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"nativeCompiler": "BiSheng",
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
\ No newline at end of file
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@security/no-unsafe-aes": "error",
"@security/no-unsafe-hash": "error",
"@security/no-unsafe-mac": "warn",
"@security/no-unsafe-dh": "error",
"@security/no-unsafe-dsa": "error",
"@security/no-unsafe-ecdsa": "error",
"@security/no-unsafe-rsa-encrypt": "error",
"@security/no-unsafe-rsa-sign": "error",
"@security/no-unsafe-rsa-key": "error",
"@security/no-unsafe-dsa-key": "error",
"@security/no-unsafe-dh-key": "error",
"@security/no-unsafe-3des": "error"
}
}
\ No newline at end of file
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test
\ No newline at end of file
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
},
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "-DOHOS_ENABLE_ASAN=ON",
"cppFlags": "",
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
},
"nativeLib": {
"debugSymbol": {
"strip": true,
"exclude": []
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}
\ No newline at end of file
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}
\ No newline at end of file
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
\ No newline at end of file
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"libentry.so@src/main/cpp/types/libentry": "libentry.so@src/main/cpp/types/libentry"
},
"packages": {
"libentry.so@src/main/cpp/types/libentry": {
"name": "libentry.so",
"version": "1.0.0",
"resolved": "src/main/cpp/types/libentry",
"registryType": "local"
}
}
}
\ No newline at end of file
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"libentry.so": "file:./src/main/cpp/types/libentry"
}
}
\ No newline at end of file
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(harmonyapp)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
add_library(sample STATIC IMPORTED)
set_target_properties(sample PROPERTIES
IMPORTED_LOCATION "${NATIVERENDER_ROOT_PATH}/libs/libsample.a")
# Link libkn.so directly
add_library(kn SHARED IMPORTED)
set_target_properties(kn PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libkn.so"
)
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_libraries(entry PRIVATE sample kn)
#ifndef KONAN_LIBKN_H
#define KONAN_LIBKN_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
typedef bool libkn_KBoolean;
#else
typedef _Bool libkn_KBoolean;
#endif
typedef unsigned short libkn_KChar;
typedef signed char libkn_KByte;
typedef short libkn_KShort;
typedef int libkn_KInt;
typedef long long libkn_KLong;
typedef unsigned char libkn_KUByte;
typedef unsigned short libkn_KUShort;
typedef unsigned int libkn_KUInt;
typedef unsigned long long libkn_KULong;
typedef float libkn_KFloat;
typedef double libkn_KDouble;
typedef float __attribute__ ((__vector_size__ (16))) libkn_KVector128;
typedef void* libkn_KNativePtr;
struct libkn_KType;
typedef struct libkn_KType libkn_KType;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Byte;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Short;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Int;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Long;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Float;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Double;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Char;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Boolean;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_Unit;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_UByte;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_UShort;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_UInt;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_kotlin_ULong;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_com_example_kotlin_sample_Platform;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_com_example_kotlin_sample_Greeting;
typedef struct {
libkn_KNativePtr pinned;
} libkn_kref_com_example_kotlin_sample_OhosPlatform;
extern const char* getKotlinAnalyticsInfo();
extern const char* getKotlinGreeting();
extern const char* getKotlinUiText();
extern void triggerCpuFreeze();
extern libkn_KInt triggerKotlinHeapOverflow();
extern libkn_KInt triggerKotlinHeapUseAfterFree();
extern libkn_KInt triggerKotlinNpe();
extern libkn_KInt triggerKotlinOutOfBounds();
extern libkn_KInt triggerKotlinStackBufferOverflow();
extern void triggerKotlinToCCrash();
extern void* getStaticLibImage();
extern libkn_KInt getStaticLibImageSize();
extern libkn_KInt getStaticLibSum(libkn_KInt a, libkn_KInt b);
extern const char* getStaticLibVersion();
typedef struct {
/* Service functions. */
void (*DisposeStablePointer)(libkn_KNativePtr ptr);
void (*DisposeString)(const char* string);
libkn_KBoolean (*IsInstance)(libkn_KNativePtr ref, const libkn_KType* type);
libkn_kref_kotlin_Byte (*createNullableByte)(libkn_KByte);
libkn_KByte (*getNonNullValueOfByte)(libkn_kref_kotlin_Byte);
libkn_kref_kotlin_Short (*createNullableShort)(libkn_KShort);
libkn_KShort (*getNonNullValueOfShort)(libkn_kref_kotlin_Short);
libkn_kref_kotlin_Int (*createNullableInt)(libkn_KInt);
libkn_KInt (*getNonNullValueOfInt)(libkn_kref_kotlin_Int);
libkn_kref_kotlin_Long (*createNullableLong)(libkn_KLong);
libkn_KLong (*getNonNullValueOfLong)(libkn_kref_kotlin_Long);
libkn_kref_kotlin_Float (*createNullableFloat)(libkn_KFloat);
libkn_KFloat (*getNonNullValueOfFloat)(libkn_kref_kotlin_Float);
libkn_kref_kotlin_Double (*createNullableDouble)(libkn_KDouble);
libkn_KDouble (*getNonNullValueOfDouble)(libkn_kref_kotlin_Double);
libkn_kref_kotlin_Char (*createNullableChar)(libkn_KChar);
libkn_KChar (*getNonNullValueOfChar)(libkn_kref_kotlin_Char);
libkn_kref_kotlin_Boolean (*createNullableBoolean)(libkn_KBoolean);
libkn_KBoolean (*getNonNullValueOfBoolean)(libkn_kref_kotlin_Boolean);
libkn_kref_kotlin_Unit (*createNullableUnit)(void);
libkn_kref_kotlin_UByte (*createNullableUByte)(libkn_KUByte);
libkn_KUByte (*getNonNullValueOfUByte)(libkn_kref_kotlin_UByte);
libkn_kref_kotlin_UShort (*createNullableUShort)(libkn_KUShort);
libkn_KUShort (*getNonNullValueOfUShort)(libkn_kref_kotlin_UShort);
libkn_kref_kotlin_UInt (*createNullableUInt)(libkn_KUInt);
libkn_KUInt (*getNonNullValueOfUInt)(libkn_kref_kotlin_UInt);
libkn_kref_kotlin_ULong (*createNullableULong)(libkn_KULong);
libkn_KULong (*getNonNullValueOfULong)(libkn_kref_kotlin_ULong);
/* User functions. */
struct {
struct {
struct {
struct {
struct {
struct {
libkn_KType* (*_type)(void);
libkn_kref_com_example_kotlin_sample_Greeting (*Greeting)();
const char* (*greet)(libkn_kref_com_example_kotlin_sample_Greeting thiz);
} Greeting;
struct {
libkn_KType* (*_type)(void);
const char* (*get_name)(libkn_kref_com_example_kotlin_sample_Platform thiz);
} Platform;
struct {
libkn_KType* (*_type)(void);
libkn_kref_com_example_kotlin_sample_OhosPlatform (*OhosPlatform)();
const char* (*get_name)(libkn_kref_com_example_kotlin_sample_OhosPlatform thiz);
} OhosPlatform;
const char* (*appInfo)();
const char* (*getKotlinAnalyticsInfo_)();
const char* (*getKotlinGreeting_)();
const char* (*getKotlinUiText_)();
const char* (*ohosUiText)();
void (*triggerCpuFreeze_)();
libkn_KInt (*triggerKotlinHeapOverflow_)();
libkn_KInt (*triggerKotlinHeapUseAfterFree_)();
libkn_KInt (*triggerKotlinNpe_)();
libkn_KInt (*triggerKotlinOutOfBounds_)();
libkn_KInt (*triggerKotlinStackBufferOverflow_)();
void (*triggerKotlinToCCrash_)();
void* (*getStaticLibImage_)();
libkn_KInt (*getStaticLibImageSize_)();
libkn_KInt (*getStaticLibSum_)(libkn_KInt a, libkn_KInt b);
const char* (*getStaticLibVersion_)();
libkn_kref_com_example_kotlin_sample_Platform (*getPlatform)();
} kotlin_sample;
} example;
} com;
} root;
} kotlin;
} libkn_ExportedSymbols;
extern libkn_ExportedSymbols* libkn_symbols(void);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* KONAN_LIBKN_H */
// file-level functions in napi_init.cpp
#include "napi/native_api.h"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <pthread.h>
extern "C" int getStaticLibSum(int a, int b);
extern "C" const unsigned char* getStaticLibImage();
extern "C" int getStaticLibImageSize();
extern "C" int sample_add(int a, int b);
extern "C" const unsigned char* get_sample_image(void);
extern "C" int get_sample_image_size(void);
static napi_value TriggerHeapBufferOverflow(napi_env env, napi_callback_info /*info*/) {
char *buffer;
buffer = (char *)malloc(10);
*(buffer + 11) = 'n'; // Overflow
*(buffer + 12) = 'n'; // Overflow
char ret = buffer[1]; // Read valid memory (though uninitialized)
free(buffer);
napi_value result;
napi_create_int32(env, (int32_t)ret, &result);
return result;
}
static int g_shared_var = 0;
static void* ThreadFunc(void* arg) {
for (int i = 0; i < 100000; ++i) {
g_shared_var++;
}
return nullptr;
}
static napi_value TriggerTsan(napi_env env, napi_callback_info /*info*/) {
g_shared_var = 0;
pthread_t t1, t2;
pthread_create(&t1, nullptr, ThreadFunc, nullptr);
pthread_create(&t2, nullptr, ThreadFunc, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
napi_value result;
napi_create_int32(env, (int32_t)g_shared_var, &result);
return result;
}
static napi_value TriggerHwasanUseAfterFree(napi_env env, napi_callback_info /*info*/) {
char* p = (char*)malloc(16);
free(p);
p[0] = 'x';
int32_t ret = (int32_t)p[0];
napi_value result;
napi_create_int32(env, ret, &result);
return result;
}
static napi_value NAPI_Global_getSampleImage(napi_env env, napi_callback_info info) {
const unsigned char* data = get_sample_image();
int size = get_sample_image_size();
void* bufferData;
napi_value arrayBuffer;
napi_create_arraybuffer(env, size, &bufferData, &arrayBuffer);
if (size > 0 && data != nullptr && bufferData != nullptr) {
memcpy(bufferData, data, size);
}
return arrayBuffer;
}
static napi_value NAPI_Global_getKotlinSampleImage(napi_env env, napi_callback_info info) {
// 调用 Kotlin 导出的函数
const unsigned char* data = getStaticLibImage();
int size = getStaticLibImageSize();
void* bufferData;
napi_value arrayBuffer;
napi_create_arraybuffer(env, size, &bufferData, &arrayBuffer);
if (size > 0 && data != nullptr && bufferData != nullptr) {
memcpy(bufferData, data, size);
}
return arrayBuffer;
}
// Extern declarations for linked libkn.so functions
extern "C" {
const char* getKotlinGreeting();
const char* getKotlinUiText();
const char* getKotlinAnalyticsInfo();
int getStaticLibSum(int a, int b);
int triggerKotlinHeapOverflow();
int triggerKotlinHwasanError();
void triggerCpuFreeze();
}
// 新增 NAPI 包装
static napi_value NAPI_Global_getStaticLibSum(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);
double a = 0, b = 0;
napi_get_value_double(env, args[0], &a);
napi_get_value_double(env, args[1], &b);
int r = getStaticLibSum(static_cast<int>(a), static_cast<int>(b));
napi_value out;
napi_create_int32(env, r, &out);
return out;
}
static napi_value NAPI_Global_getKotlinAnalyticsInfo(napi_env env, napi_callback_info /*info*/) {
const char* s = getKotlinAnalyticsInfo();
napi_value out;
napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &out);
return out;
}
static napi_value NAPI_Global_getKotlinGreeting(napi_env env, napi_callback_info /*info*/) {
const char* s = getKotlinGreeting();
napi_value out;
napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &out);
return out;
}
static napi_value NAPI_Global_getKotlinUiText(napi_env env, napi_callback_info /*info*/) {
const char* s = getKotlinUiText();
napi_value out;
napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &out);
return out;
}
static napi_value NAPI_Global_triggerKotlinHeapOverflow(napi_env env, napi_callback_info /*info*/) {
int result = triggerKotlinHeapOverflow();
napi_value out;
napi_create_int32(env, result, &out);
return out;
}
static napi_value NAPI_Global_triggerCpuFreeze(napi_env env, napi_callback_info /*info*/) {
triggerCpuFreeze();
return nullptr;
}
static napi_value NAPI_Global_getSampleAdd(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);
double a = 0;
double b = 0;
napi_get_value_double(env, args[0], &a);
napi_get_value_double(env, args[1], &b);
int r = sample_add(static_cast<int>(a), static_cast<int>(b));
napi_value out;
napi_create_int32(env, r, &out);
return out;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"getKotlinGreeting", nullptr, NAPI_Global_getKotlinGreeting, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getKotlinUiText", nullptr, NAPI_Global_getKotlinUiText, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getKotlinAnalyticsInfo", nullptr, NAPI_Global_getKotlinAnalyticsInfo, nullptr, nullptr, nullptr, napi_default, nullptr},
// 新增:静态库合入的 Kotlin 导出
{"getStaticLibSum", nullptr, NAPI_Global_getStaticLibSum, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getSampleAdd", nullptr, NAPI_Global_getSampleAdd, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getSampleImage", nullptr, NAPI_Global_getSampleImage, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getKotlinSampleImage", nullptr, NAPI_Global_getKotlinSampleImage, nullptr, nullptr, nullptr, napi_default, nullptr},
// ASAN Trigger
{"triggerHeapBufferOverflow", nullptr, TriggerHeapBufferOverflow, nullptr, nullptr, nullptr, napi_default, nullptr},
{"triggerHwasanUaf", nullptr, TriggerHwasanUseAfterFree, nullptr, nullptr, nullptr, napi_default, nullptr},
{"triggerTsan", nullptr, TriggerTsan, nullptr, nullptr, nullptr, napi_default, nullptr},
{"triggerKotlinHeapOverflow", nullptr, NAPI_Global_triggerKotlinHeapOverflow, nullptr, nullptr, nullptr, napi_default, nullptr},
{"triggerCpuFreeze", nullptr, NAPI_Global_triggerCpuFreeze, 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 = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
napi_module_register(&demoModule);
}
\ No newline at end of file
export const getKotlinGreeting: () => string;
export const getKotlinUiText: () => string;
export const getKotlinAnalyticsInfo: () => string;
// 新增:stdlib 演示
export const getStdlibListDemo: () => string;
export const getStdlibAggregateDemo: () => string;
export const getStaticLibSum: (a: number, b: number) => number;
export const getSampleAdd: (a: number, b: number) => number;
export const getSampleImage: () => ArrayBuffer;
export const getKotlinSampleImage: () => ArrayBuffer;
export const triggerHeapBufferOverflow: () => number;
export const triggerKotlinStackBufferOverflow:() => number;
export const triggerHwasanUaf: () => number;
export const triggerTsan: () => number;
export const triggerKotlinHeapOverflow: () => number;
export const triggerKotlinHeapUseAfterFree: () => number;
export const triggerKotlinDoubleFree: () => number;
export const triggerKotlinFreeNonAllocated: () => number;
export const triggerCpuFreeze: () => void;
{
"name": "libentry.so",
"types": "./Index.d.ts",
"version": "1.0.0",
"description": "Please describe the basic information."
}
\ No newline at end of file
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
\ No newline at end of file
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
const DOMAIN = 0x0000;
export default class EntryBackupAbility extends BackupExtensionAbility {
async onBackup() {
hilog.info(DOMAIN, 'testTag', 'onBackup ok');
await Promise.resolve();
}
async onRestore(bundleVersion: BundleVersion) {
hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
await Promise.resolve();
}
}
\ No newline at end of file
import * as testNapi from 'libentry.so';
import { common, Want } from '@kit.AbilityKit';
import { image } from '@kit.ImageKit';
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@State kotlinMsg: string = '正在加载 Kotlin 数据...';
@State analyticsMsg: string = '正在加载 Analytics 数据...';
@State sumMsg: string = '等待计算结果...';
@State sampleMsg: string = '等待 Sample 静态库结果...';
@State freezeResult: string = '等待触发卡死...';
@State sampleImageC: PixelMap | undefined = undefined;
@State sampleImageKotlin: PixelMap | undefined = undefined;
build() {
Scroll() {
Column() {
// 标题区
Text('KMP · HarmonyOS 演示')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.margin({ top: 24, bottom: 6 })
Text('SharedLib + StaticLib 示例')
.fontSize(14)
.fontColor(Color.Gray)
.margin({ bottom: 16 })
// Kotlin 输出卡片
Column() {
Text('Kotlin 输出')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Text(this.kotlinMsg)
.fontSize(16)
.fontColor(Color.Blue)
}
.padding(16)
.margin({ bottom: 12 })
.border({ width: 1, color: Color.Gray, radius: 12 })
.backgroundColor(Color.White)
// 4个独立lib的输出
Column() {
Text('4个独立lib的输出')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Text(this.analyticsMsg)
.fontSize(16)
.fontColor(Color.Blue)
}
.padding(16)
.margin({ bottom: 12 })
.border({ width: 1, color: Color.Gray, radius: 12 })
.backgroundColor(Color.White)
// 静态库运算卡片
Column() {
Text('静态库计算')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Text('add(2, 3) = ' + this.sumMsg)
.fontSize(16)
Text('sample_add(4, 5) = ' + this.sampleMsg)
.fontSize(16)
}
.padding(16)
.margin({ bottom: 12 })
.border({ width: 1, color: Color.Gray, radius: 12 })
.backgroundColor(Color.White)
// 静态库资源图片
Column() {
Text('静态库资源图片')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Text('Via C NAPI:')
.fontSize(14)
.margin({ bottom: 4 })
if (this.sampleImageC) {
Image(this.sampleImageC)
.width(100)
.height(100)
.objectFit(ImageFit.Contain)
.margin({ bottom: 12 })
} else {
Text('加载中...')
.margin({ bottom: 12 })
}
Text('Via Kotlin NAPI:')
.fontSize(14)
.margin({ bottom: 4 })
if (this.sampleImageKotlin) {
Image(this.sampleImageKotlin)
.width(100)
.height(100)
.objectFit(ImageFit.Contain)
} else {
Text('加载中...')
}
}
.padding(16)
.margin({ bottom: 12 })
.border({ width: 1, color: Color.Gray, radius: 12 })
.backgroundColor(Color.White)
// 操作按钮
Row() {
Button('刷新全部')
.onClick(() => this.refreshAll())
.backgroundColor(Color.Blue)
.fontColor(Color.White)
.padding({ left: 18, right: 18 })
.margin({ top: 6 })
.border({ width: 0, radius: 18 })
}
.justifyContent(FlexAlign.Center)
.margin({ bottom: 24 })
// Sanitizer 测试区
Column() {
Text('UI 卡死演示')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Text(this.freezeResult)
.fontSize(16)
.fontColor(Color.Red)
.margin({ bottom: 8 })
Row() {
Button('触发主线程卡死')
.onClick(() => {
this.triggerArkTSFreeze();
})
.backgroundColor(Color.Red)
.fontColor(Color.White)
.padding({ left: 18, right: 18 })
.margin({ top: 6 })
.border({ width: 0, radius: 18 })
}
}
.padding(16)
.margin({ bottom: 24 })
.border({ width: 1, color: Color.Gray, radius: 12 })
.backgroundColor(Color.White)
// Sanitizer Page Entry
Column() {
Text('Sanitizer 验证')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Button('进入 Sanitizer 验证页面')
.onClick(() => {
router.pushUrl({ url: 'pages/SanitizerPage' });
})
.fontSize(16)
.width('100%')
.margin({ top: 6 })
}
.padding(16)
.margin({ bottom: 24 })
.border({ width: 1, color: Color.Gray, radius: 12 })
.backgroundColor(Color.White)
}
.width('100%')
.padding({ left: 20, right: 20 })
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
aboutToAppear() {
this.refreshAll();
}
refreshAll() {
this.refreshGreeting();
this.refreshAnalytics();
this.refreshSum();
this.refreshSampleAdd();
this.loadSampleImage();
}
loadSampleImage() {
try {
// 1. 通过 C NAPI 直接获取
let bufferC = testNapi.getSampleImage();
if (bufferC) {
let imageSource = image.createImageSource(bufferC);
imageSource.createPixelMap().then((pixelMap) => {
this.sampleImageC = pixelMap;
}).catch((err: Error) => {
console.error("Failed to create PixelMap (C): " + err.message);
});
}
// 2. 通过 Kotlin 导出的方法获取 (ArkTS -> NAPI -> Kotlin -> C Static Lib -> Assembly)
let bufferKotlin = testNapi.getKotlinSampleImage();
if (bufferKotlin) {
let imageSource = image.createImageSource(bufferKotlin);
imageSource.createPixelMap().then((pixelMap) => {
this.sampleImageKotlin = pixelMap;
}).catch((err: Error) => {
console.error("Failed to create PixelMap (Kotlin): " + err.message);
});
}
} catch (e) {
console.error("Failed to get sample image: " + e.message);
}
}
refreshGreeting() {
try {
this.kotlinMsg = testNapi.getKotlinUiText();
} catch (e) {
this.kotlinMsg = "调用失败: " + (e?.message ?? String(e));
}
}
refreshAnalytics() {
try {
const s: string = testNapi.getKotlinAnalyticsInfo();
this.analyticsMsg = s;
} catch (e) {
this.analyticsMsg = "调用失败: " + (e?.message ?? String(e));
}
}
refreshSum() {
try {
const v: number = testNapi.getStaticLibSum(2, 3);
this.sumMsg = String(v);
} catch (e) {
this.sumMsg = "调用失败: " + (e?.message ?? String(e));
}
}
refreshSampleAdd() {
try {
const v: number = testNapi.getSampleAdd(4, 5);
this.sampleMsg = String(v);
} catch (e) {
this.sampleMsg = "调用失败: " + (e?.message ?? String(e));
}
}
triggerArkTSFreeze() {
this.freezeResult = "处理中...";
// 使用 setTimeout 将耗时任务放入异步队列(仍在主线程执行)
setTimeout(() => {
let sum = 0;
// 10亿次循环
for (let i = 0; i < 1000000000; i++) {
sum += i;
// 每1000万次尝试更新一次 UI
// 注意:由于主线程被这个循环阻塞,UI 实际上不会在循环期间刷新
// 只有等到循环完全结束后,最后一次状态更新才会被渲染
if (i % 10000000 === 0) {
this.freezeResult = "Progress: " + i;
}
}
this.freezeResult = "Done: " + sum;
}, 100);
}
}
import * as testNapi from 'libentry.so';
import { router } from '@kit.ArkUI';
interface ButtonConfig {
label: string;
action: () => void;
msg: string;
}
@Entry
@Component
struct SanitizerPage {
@State npeMsg: string = '点击以下按钮触发demo中的函数,应用闪退报错,在FaultLog中输出';
private asanButtons: ButtonConfig[] = [
{ label: 'C++堆越界', action: () => testNapi.triggerHeapBufferOverflow(), msg: '' },
{ label: 'Kotlin堆越界', action: () => testNapi.triggerKotlinHeapOverflow(), msg: 'FaultLog输出中包含字段:AddressSanitizer: heap-buffer-overflow' },
{ label: 'Kotlin栈越界', action: () => testNapi.triggerKotlinStackBufferOverflow(), msg: '无法检出' },
{ label: 'Kotlin释放后使用', action: () => testNapi.triggerKotlinHeapUseAfterFree(), msg: 'AddressSanitizer: heap-use-after-free' },
{ label: 'Kotlin重复释放', action: () => testNapi.triggerKotlinDoubleFree(), msg: 'AddressSanitizer: double-free' },
{ label: 'Kotlin释放未分配内存', action: () => testNapi.triggerKotlinFreeNonAllocated(), msg: 'AddressSanitizer: attempting free on address which was not malloc()-ed' }
];
private tsanButtons: ButtonConfig[] = [
{ label: 'TSAN检测-数据竞争', action: () => testNapi.triggerTsan(), msg: '' },
];
private hwasanButtons: ButtonConfig[] = [
{ label: 'HWASAN检测-UAF漏洞', action: () => testNapi.triggerHwasanUaf(), msg: '' }
];
build() {
Scroll() {
Column() {
// Navigation Bar
Row() {
Button('< 返回')
.onClick(() => {
router.back();
})
.backgroundColor(Color.Transparent)
.fontColor(Color.Blue)
.margin({ right: 10 })
Text('Sanitizer 验证')
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 16, top: 16, bottom: 16 })
.alignItems(VerticalAlign.Center)
// ASAN Card
this.buildCard('ASAN (Address Sanitizer)', Color.Red, this.asanButtons,this.npeMsg)
// TSAN Card
this.buildCard('TSAN (Thread Sanitizer)', Color.Green, this.tsanButtons)
// HWASAN Card
this.buildCard('HWASAN (Hardware Sanitizer)', Color.Orange, this.hwasanButtons)
}
.width('100%')
.padding({ bottom: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
@Builder
buildCard(title: string, color: ResourceColor, buttons: ButtonConfig[], msg?: string) {
Column() {
Text(title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(color)
.margin({ bottom: 12 })
.width('100%')
if (msg) {
Text(msg)
.fontSize(14)
.fontColor(Color.Gray)
.margin({ bottom: 10 })
.width('100%')
}
ForEach(buttons, (item: ButtonConfig) => {
Button(item.label)
.onClick(() => item.action())
.backgroundColor(color)
.fontColor(Color.White)
.width('100%')
.margin({ bottom: 8 })
Text(item.msg)
})
}
.padding(16)
.margin({ left: 16, right: 16, bottom: 16 })
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: '#1A000000', offsetY: 2 })
}
}
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
}
}
\ No newline at end of file
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}
\ No newline at end of file
{
"float": [
{
"name": "page_text_font_size",
"value": "50fp"
}
]
}
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}
\ No newline at end of file
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
\ No newline at end of file
{
"allowToBackupRestore": true
}
\ No newline at end of file
{
"src": [
"pages/Index",
"pages/SanitizerPage"
]
}
{
"color": [
{
"name": "start_window_background",
"value": "#000000"
}
]
}
\ No newline at end of file
const NativeMock: Record<string, Object> = {
'add': (a: number, b: number) => {
return a + b;
},
};
export default NativeMock;
\ No newline at end of file
{
"libentry.so": {
"source": "src/mock/Libentry.mock.ets"
}
}
\ No newline at end of file
import { hilog } from '@kit.PerformanceAnalysisKit';
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function abilityTest() {
describe('ActsAbilityTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
})
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
})
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
})
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
})
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
})
})
}
\ No newline at end of file
import abilityTest from './Ability.test';
export default function testsuite() {
abilityTest();
}
\ No newline at end of file
{
"module": {
"name": "entry_test",
"type": "feature",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false
}
}
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}
\ No newline at end of file
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function localUnitTest() {
describe('localUnitTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
});
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
});
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
});
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
});
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
});
});
}
\ No newline at end of file
{
"modelVersion": "6.0.0",
"dependencies": {
},
"execution": {
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
// "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
// "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
// "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
// "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
// "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
},
"logging": {
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
},
"debugging": {
// "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
},
"nodeOptions": {
// "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
// "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
}
}
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment