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
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0",
"@ohos/hypium@1.0.24": "@ohos/hypium@1.0.24"
},
"packages": {
"@ohos/hamock@1.0.0": {
"name": "@ohos/hamock",
"version": "1.0.0",
"integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har",
"registryType": "ohpm"
},
"@ohos/hypium@1.0.24": {
"name": "@ohos/hypium",
"version": "1.0.24",
"integrity": "sha512-3dCqc+BAR5LqEGG2Vtzi8O3r7ci/3fYU+FWjwvUobbfko7DUnXGOccaror0yYuUhJfXzFK0aZNMGSnXaTwEnbw==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.24.har",
"registryType": "ohpm"
}
}
}
\ No newline at end of file
{
"modelVersion": "6.0.0",
"description": "Please describe the basic information.",
"dependencies": {
},
"devDependencies": {
"@ohos/hypium": "1.0.24",
"@ohos/hamock": "1.0.0"
}
}
TEAM_ID=
PRODUCT_NAME=Kotlinsample
PRODUCT_BUNDLE_IDENTIFIER=com.example.kotlin_sample.Kotlinsample$(TEAM_ID)
CURRENT_PROJECT_VERSION=1
MARKETING_VERSION=1.0
\ No newline at end of file
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXFileReference section */
A880CD7FA60809310F926F0E /* Kotlinsample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Kotlinsample.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
BE4E8F1993E731ED2F500670 /* Exceptions for "iosApp" folder in "iosApp" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 334D9269B12FA5C1A08FC9EC /* iosApp */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
B51A5926C56ACC27ABD6BDDE /* iosApp */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
BE4E8F1993E731ED2F500670 /* Exceptions for "iosApp" folder in "iosApp" target */,
);
path = iosApp;
sourceTree = "<group>";
};
33394D1CC7A870BDFEFD0219 /* Configuration */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Configuration;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
22542E4CC80B1C0EF0ACD6BD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2F924F3D49D5CBBD07833C8D = {
isa = PBXGroup;
children = (
33394D1CC7A870BDFEFD0219 /* Configuration */,
B51A5926C56ACC27ABD6BDDE /* iosApp */,
5BEB0377DF31465AB99EB1BC /* Products */,
);
sourceTree = "<group>";
};
5BEB0377DF31465AB99EB1BC /* Products */ = {
isa = PBXGroup;
children = (
A880CD7FA60809310F926F0E /* Kotlinsample.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
334D9269B12FA5C1A08FC9EC /* iosApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 32553FABD842CEE84FB84DAE /* Build configuration list for PBXNativeTarget "iosApp" */;
buildPhases = (
396814608F9861C1787B0232 /* Compile Kotlin Framework */,
646842A7A830785DA6CDFBDF /* Sources */,
22542E4CC80B1C0EF0ACD6BD /* Frameworks */,
35242F3D1DBB48613C258125 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
B51A5926C56ACC27ABD6BDDE /* iosApp */,
);
name = iosApp;
packageProductDependencies = (
);
productName = iosApp;
productReference = A880CD7FA60809310F926F0E /* Kotlinsample.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
5A5F5D56D1E5E0E6B132CD1F /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620;
TargetAttributes = {
334D9269B12FA5C1A08FC9EC = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = 77246D39D59C2C1D3975E5F9 /* Build configuration list for PBXProject "iosApp" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 2F924F3D49D5CBBD07833C8D;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 5BEB0377DF31465AB99EB1BC /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
334D9269B12FA5C1A08FC9EC /* iosApp */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
35242F3D1DBB48613C258125 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
396814608F9861C1787B0232 /* Compile Kotlin Framework */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Compile Kotlin Framework";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
646842A7A830785DA6CDFBDF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
33EB432A0FDCC3E298EFAFC5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 33394D1CC7A870BDFEFD0219 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
4219DF063A667575164A0BBA /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 33394D1CC7A870BDFEFD0219 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
9E9E42FDC87D79E176F13E4D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
847BDC45CB7D58CBAD88848A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
77246D39D59C2C1D3975E5F9 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33EB432A0FDCC3E298EFAFC5 /* Debug */,
4219DF063A667575164A0BBA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
32553FABD842CEE84FB84DAE /* Build configuration list for PBXNativeTarget "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9E9E42FDC87D79E176F13E4D /* Debug */,
847BDC45CB7D58CBAD88848A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 5A5F5D56D1E5E0E6B132CD1F /* Project object */;
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "app-icon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
import UIKit
import SwiftUI
import ComposeApp
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea()
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
import SwiftUI
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
\ No newline at end of file
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
}
kotlin {
jvmToolchain(17)
androidTarget()
iosArm64()
iosSimulatorArm64()
iosX64()
ohosArm64()
sourceSets {
val commonMain by getting
val androidMain by getting
val iosMain by creating { dependsOn(commonMain) }
val iosArm64Main by getting { dependsOn(iosMain) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
val iosX64Main by getting { dependsOn(iosMain) }
val ohosArm64Main by getting { dependsOn(commonMain) }
}
}
android {
namespace = "com.example.analytics"
compileSdk = 34
defaultConfig { minSdk = 24 }
}
<manifest package="com.example.analytics"/>
package com.example.analytics
fun analyticsInfo(): String = "lib-analytics ready"
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
}
kotlin {
// 统一使用 JDK 17 作为工具链
jvmToolchain(17)
androidTarget()
iosArm64()
iosSimulatorArm64()
iosX64()
ohosArm64()
sourceSets {
val commonMain by getting {
dependencies {
api(project(":lib-base"))
}
}
val androidMain by getting
val iosMain by creating { dependsOn(commonMain) }
val iosArm64Main by getting { dependsOn(iosMain) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
val iosX64Main by getting { dependsOn(iosMain) }
val ohosArm64Main by getting { dependsOn(commonMain) }
}
}
android {
namespace = "com.example.arith"
compileSdk = 34
defaultConfig { minSdk = 24 }
}
\ No newline at end of file
<manifest package="com.example.arith" />
\ No newline at end of file
package com.example.arith
object Arith {
fun add(a: Int, b: Int): Int = a + b
fun sub(a: Int, b: Int): Int = a - b
}
\ No newline at end of file
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
}
kotlin {
jvmToolchain(17)
androidTarget()
iosArm64()
iosSimulatorArm64()
iosX64()
ohosArm64()
sourceSets {
val commonMain by getting
}
}
android {
namespace = "com.example.base"
compileSdk = 34
defaultConfig { minSdk = 24 }
}
package com.example.base
class BaseUtils {
fun getBaseInfo(): String = "Base Info from lib-base"
}
rootProject.name = "kmptcp_kotlin_sample"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
// maven {
// url = uri("/Users/jiangyuhuan/AndroidStudioProjects/kmptpc_kotlin/build/repo")
// }
// mavenLocal() // 备用:脚本中也执行了
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
// 引入 Gradle 插件门户(主要用于查找 Gradle 插件)
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
// maven {
// url = uri("/Users/jiangyuhuan/AndroidStudioProjects/kmptpc_kotlin/build/repo")
// }
// mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
}
}
include(":composeApp")
include(":lib-arith")
include(":lib-analytics")
include(":lib-base")
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