Post

Kotlin/Native 编译器开发技巧

Kotlin/Native 编译器开发技巧

Gradle / IDEA 相关

IDEA中配置并发下载依赖加速sync

Screenshot 2025-09-26 at 9.43.55 AM.png

运行具体任务

反复调试同一个位置的修改时,触发更小范围的构建在低配电脑上通常速度比较快。一般的kn编译器开发不太改kgp及其他组件,这些产物可以都用线上的,只出 kotlin-native/dist目录下的kn编译器产物进行开发。这种模式下不需要修改maven仓库添加本地的build/repo,不需要修改kotlin版本号,旨在gradle.properties中添加 kotlin.native.home=/path/to/kotlin/kotlin-native/dist 就行。Kotlin仓库打包编译器组件的命令如下

1
./gradlew :kotlin-native:dist :kotlin-native:platformLibs:ohos_arm64Install :kotlin-native:platformLibs:ios_arm64Install

其中后两个 platformLibs 命令是打包 platform.xx 调用平台能力的cinterop klib。没改def文件的话可以只在clean之后加这两个task,只是修改kn编译器代码的话只跑第一个task就行

更通用的子项目名通常是文件夹名,tasks列出这个子项目的所有gradle任务。如果是kn的文件夹需要传配置

1
./gradlew :kotlin-native:runtime:tasks -Pbootstrap.local=true -Pkotlin.native.enabled --dependency-verification=off

在输出的tasks中找可能感兴趣的任务,如单编ohos的runtime

1
./gradlew :kotlin-native:runtime:ohos_arm64Runtime -Pbootstrap.local=true -Pkotlin.native.enabled --dependency-verification=off

单打platformlibs

1
./gradlew :kotlin-native:ohos_arm64PlatformLibs -Pbootstrap.local=true -Pkotlin.native.enabled --dependency-verification=off

单独构建一个def

1
./gradlew :kotlin-native:platformLibs:compileKonanOhos_arm64-WifiOhos_arm64 -Pbootstrap.local=true -Pkotlin.native.enabled --dependency-verification=off 

kotlin-native文件夹下代码跳转

Kotlin Native构建的时候需要特殊的gradle.properties参数,kotlin项目拉下来之后直接sync后,kotlin-native部分的代码都无法点击跳转到定义

  1. 拉取项目, bash scripts/build-ohos.sh 运行构建
  2. 本地出包完成后,在gradle.properties中添加,冲突的key注掉原有的
    1
    2
    3
    4
    5
    6
    7
    8
    
     bootstrap.kotlin.default.version=2.2.255-SNAPSHOT
     deployVersion=2.2.255-SNAPSHOT
     versions.kotlin-native=2.2.255-SNAPSHOT
     konanVersion=2.2.255-SNAPSHOT
     bootstrap.kotlin.version=2.2.255-SNAPSHOT
     kotlin.native.enabled=true
     bootstrap.local=true
     bootstrap.local.version=2.2.255-SNAPSHOT
    
  3. 执行sync

调试

有两种调试,调kmp的编译过程/调运行起来的kmp打包产物。KN的编译器是kotlin写的打包成jar在jvm上运行,前三个调java的都是调编译过程的方式。

通过IDEA调试gradle任务

  1. clone kotlin项目https://github.com/linhandev/kuiklybase-kotlin
  2. 使用intellj idea打开kotlin项目
  3. 在kotlin项目右上角添加构建compose-sample项目句gradle任务配置

image.png

image.png

其中gradle project被构建的项目地址,如ovcomposesample项目地址。run命令填写构建那个项目的命令,如 ./gradlew publishDebugBinariesToHarmonyApp

  1. 在kotlin仓库,kotlin编译器代码中打断点,进行调试

调试gradle命令

  1. gradle命令添加-Dorg.gradle.debug=true,如
1
2
3
./gradlew :kotlin-native:dist
# 添加后
./gradlew :kotlin-native:dist -Dorg.gradle.debug=true
  1. 添加jvm remote debug profile,所有东西都默认

image.png

调试java命令

如果命令不是gradle启动的,是直接调用了konanc这种脚本没法添加 -Dorg.gradle.debug=true,需要手动修改java命令插入调试相关选项

  1. 获取jvm中运行的命令,找到 ~/.konan 目录下对应版本的prebuilt文件夹,使用腾讯版本文件夹名带kba,使用ohos版本文件夹名带ohos,使用本地指定的的就是 kotlin.native.home 指向的文件夹。修改其中的 bin/run_konan 脚本,将最后一行的 java 命令打到文件中

image.png

  1. 在java命令中插入 -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005。其中 suspend=y 的意思是debug attach之后再开始执行,kotlin单个构建任务很快,不加这个容易还没 attach 就执行完了

比如

1
2
3
/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home/bin/java -ea -Xmx3G -XX:TieredStopAtLevel=1 -Dfile.encoding=UTF-8 -Dkonan.home=/Volumes/disk/git/kmp/kotlin22-ohos/kotlin-native/dist -cp /Volumes/disk/git/kmp/kotlin22-ohos/kotlin-native/dist/konan/lib/kotlin-native-compiler-embeddable.jar org.jetbrains.kotlin.cli.utilities.MainKt konanc main.kt -o hello.kexe -verbose

/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -ea -Xmx3G -XX:TieredStopAtLevel=1 -Dfile.encoding=UTF-8 -Dkonan.home=/Volumes/disk/git/kmp/kotlin22-ohos/kotlin-native/dist -cp /Volumes/disk/git/kmp/kotlin22-ohos/kotlin-native/dist/konan/lib/kotlin-native-compiler-embeddable.jar org.jetbrains.kotlin.cli.utilities.MainKt konanc main.kt -o hello.kexe -verbose
  1. 在kotlin项目中添加 remote jvm debug 配置,所有选项默认 image.png
  2. 运行修改过的java命令,在kotlin项目中通过remove jvm debug配置attach,正常debug

调试runtime

调跑起来的KMP产物需要发到手机上的hap里KMP的so包含debug信息,debug信息还要和本地打断点文件的绝对路径一致(比如ci上打包的runtime包含的debug信息是另一个电脑上的绝对路径,在本地同一个内容的文件上打断点因为绝对路径不一样也不会停)。在KN打包产物中包含runtime的debug信息hacking中有指导

  1. patch kotlin工程
    1
    2
    3
    
     if (project.findProperty("kotlin.native.isNativeRuntimeDebugInfoEnabled") == "true") {
       args += "-Xbinary=stripDebugInfoFromNativeLibs=false"
     }
    

    alt text

  2. 在kotlin项目根目录gradle.properties或local.properties中设置 kotlin.native.isNativeRuntimeDebugInfoEnabled=true,之后重新出kotlin版本
    1
    2
    3
    
     ./gradlew clean
     ./gradlew --stop
     ./gradlew :kotlin-native:dist :kotlin-native:platformLibs:ohos_arm64Install :kotlin-native:platformLibs:ios_arm64Install
    
  3. 在应用工程中通过freeCompilerArgs添加编译选项 -Xbinary=stripDebugInfoFromNativeLibs=false,带 –rerun-tasks 重新出包 打印打包出的so的debug信息,检查其中是否包含属于runtime的文件路径 dwarfdump libkn.so | grep '/kotlin-native/runtime/src/',应该能grep到输出而且输出的自己电脑上的路径 alt text
  4. 鸿蒙工程打包时默认会strip掉所有so中的debug信息,需要配置打debug包时不进行strip。在入口模块或鸿蒙工程根目录的 build-profile.json5 buildOptionSet 中添加配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     {
       "name": "debug",
       "nativeLib": {
         "debugSymbol": {
           "strip": false,
           "exclude": []
         }
       }
     }
    

    alt text 官方文档,搜strip。确定生效的方式是解压发送到手机安装的hap,找到里边kn的so,file xx.so,应该看到 not stripped

  5. 启动鸿蒙应用,attach,将要断点的文件拖进devecostudio,打断点,进行调试 alt text

一步启动鸿蒙应用

参考 composeApp/build.gradle.kts 中 startHarmonyApp 任务的实现

任务配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
# DevEco Studio 安装路径
devEcoStudioDir=/Applications/DevEco-Studio.app
# 鸿蒙工程的路径:如果写相对路径是相对kotlin项目的根目录,如果写绝对路径就指哪就是哪
harmonyAppDir=harmonyApp
# 鸿蒙应用中入口模块所在的文件夹
harmonyAppEntryModuleDir=entry/
# 鸿蒙应用入口模块中,gradle打出来的kn项目 .h 放在哪个文件夹
hFileDir=src/main/cpp/include/
# 鸿蒙应用入口模块中,gradle打出来的kn项目 .so 放在哪个文件夹
soFileDir=libs/arm64-v8a/
# 启动应用中ability的名字,可以通过在DevEco Studio中点击运行,查看'hdc shell aa start -a'后面的参数,就是这个ability名字
abilityName = EntryAbility

执行过程(这里只简介,详细命令参考gradle任务实现或DevEco Studio中构建执行日志):

  1. 依赖 linkRelease/DebugSharedOhosArm64 任务,构建kotlin项目
  2. 将kotlin项目打包的 .so 和 .h 复制到鸿蒙应用中

后面的步骤都在鸿蒙应用中执行

  1. ohpm i,这步不改依赖就不需要重复执行,不过重复执行耗时也只是几秒,加上这个步骤比较保险
  2. 运行 hvigor sync 同步项目
  3. 打包鸿蒙应用
  4. hdc install 安装hap到手机
  5. 启动应用

现网问题反解

基本流程为出发布包时打release且debuggable的so,这个so中带符号表,保留下来用于出现问题时进行反解。在 harmonyApp/entry/build-profile.json5 中配置打 release 模式的hap包时对so进行strip,这样最终进入hap的so会被去掉符号表,可以通过解压 entry/build/default/outputs/default/entry-default-signed.hap 获取其中的so确定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"buildOptionSet": [
  {
    "name": "release",
    "arkOptions": {
      "obfuscation": {
        "ruleOptions": {
          "enable": false,
          "files": [
            "./obfuscation-rules.txt"
          ]
        }
      }
    },
    "nativeLib": {
      "debugSymbol": {
        "strip": true,
        "exclude": []
      }
    }
  },

当现网出现崩溃时,使用现网崩溃的地址+本地保留的带debug符号的release so,用addr2line反解,如

1
llvm-addr2line -fCe libkn.so cf2ec3

如需将debug符号上传到外部平台,可以去掉debuggable so中可执行代码部分,只保留debug符号表

1
2
llvm-objcopy --only-keep-debug libkn.so libkn.debug # 将debug符号复制到 libkn.debug 中,给问题监控平台上传这个so
llvm-objdump -dS libkn.debug > asm # 验证步骤,dump so中所有汇编代码,结果应该为空,确认so中不包含可执行代码

崩溃栈通常有多层,可以写个简单的脚本批量通过偏移量解析文件名和行号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""
➜  stack ls -lah 
total 30M
drwxr-xr-x 2 user user 4.0K Sep  5 20:19 .
drwxr-xr-x 5 user user 4.0K Sep  5 09:11 ..
-rwxr-xr-x 1 user user  23M Sep  5 09:14 libkn.debug
-rwxr-xr-x 1 user user 6.9M Sep  5 09:12 llvm-addr2line
-rw-r--r-- 1 user user  764 Sep  5 15:01 symbolicate.py
"""

import subprocess

so_name = "libkn.debug"
addresses = input("Plz input addresses:\n")

addresses = addresses.strip().split(" ")
addresses = [addr for addr in addresses if addr and len(addr) != 0]

for address in addresses:
    cmd = ["./llvm-addr2line", "-fCe", so_name, address]
    try:
        result = subprocess.run(cmd, check=True, capture_output=True, text=True)
        res = result.stdout.strip().split("\n")
        # res[1] = res[1].replace("/path/to/ci/runner/project/", "/path/to/local/project/") # 将出包机器的项目文件夹路径换成自己本地的,方便跳转
        print(f"{address}\tfile://{res[1]}\t{res[0]}")
    except subprocess.CalledProcessError as e:
        print(f"Error running command for address {address}: {e.stderr}")

反解完成后命令行会输出文件路径。vscode中ctrl+鼠标左键,intellj idea或as中左键点击地址可以直接打开对应对应代码行

传递kotlin版本

Kotlin / KuiklyBase-Kotlin仓库(包含kotlin标准库,KN编译器等)运行出包脚本后,打包的产物都在 build/repo 文件夹下。线下传递版本

  1. 打包要传递的版本前最好先删除build文件夹再出包,避免文件夹中存在多个版本,压缩没用的东西
  2. 在出包机器上打包kotlin项目,zip build/repo 文件夹
  3. 传递zip,在运行机器上解压
  4. 在sample项目中添加 maven(”/path/to/build/repo”) 指向解压的本地仓库
  5. 在 gradle/libs.version.toml 中切换kotlin版本为传递的zip包中版本
  6. kotlin native编译器在 ~/.konan 文件夹中的依赖不受gradle管理,保险起见可以删除本地的 ~/.konan 文件夹,避免部分依赖因为已经存在不被更新
  7. sample项目中首次使用本地版本建议gradle命令添加 —refresh-dependencies —rerun-tasks ,确保用上更新了的依赖

单打鸿蒙

EnabledTargets.kt

1
fun enabledTargets(platformManager: PlatformManager) = listOf(KonanTarget.OHOS_ARM64) // 单出鸿蒙

gradle task详细状态

命令行执行gradle,任务名称在命令行最下面动态刷新,执行完看不到每个task这次任务是否被执行。配置gradle.properties org.gradle.console=verbose 可以让任务执行状态不刷掉旧的,执行完成后也可以查看

image.png

LLVM 工具

~/.konan/dependencies中上游的llvm有两个版本,dev版本在kotlin项目出包时使用,包含的工具更多。为了减小体积,构建kn应用时会下载essential版本,只有kn构建过程中必须的依赖,不包含llvm-dis之类的一些工具

llvm-dis/llvm-as

  • bc是二进制格式llvm ir无法打开查看,可以通过 llvm-dis 命令将二进制的llvm ir转换为文本格式的 llvm ir 查看,二者内容完全一致。llvm-as相反是ll转bc
  • llvm-dis/as对版本一致性要求严格,用哪个版本的llvm打包的bc就需要使用哪个版本的llvm-dis转换格式
  1. 找到dev版本kotlin社区llvm,如kn2.2的在 ~/.konan/dependencies/llvm-19-aarch64-macos-dev-75
  2. llvm-dis 命令在 bin 目录下,可以将bin目录export到PATH中:export PATH=$PATH:~/.konan/dependencies/llvm-19-aarch64-macos-dev-75/bin
  3. llvm-dis runtime.bc 会生成 runtime.ll,或者 llvm-dis runtime.bc -o - > temp 指定输出文件名

KN 工具

klib

https://kotlinlang.org/docs/native-libraries.html#klib-utility

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
klib info /Volumes/disk/git/sample/kn-sample/src-lib/build/classes/kotlin/macosArm64/main/klib/src-lib.klib
Full path: /Volumes/disk/git/sample/kn-sample/src-lib/build/classes/kotlin/macosArm64/main/klib/src-lib.klib
Module name (metadata): <com.example:src-lib>
Non-empty package FQNs (1):
  com.example.src
Has IR: true
Has LLVM bitcode: false
Manifest properties:
  abi_version=1.8.0
  builtins_platform=NATIVE
  compiler_version=2.0.21-dev-107
  depends=stdlib
  ir_signature_versions=1,2
  metadata_version=1.4.1
  native_targets=macos_arm64
  short_name=src-lib
  unique_name=com.example:src-lib
1
2
3
4
5
6
7
klib dump-ir /Volumes/disk/git/sample/kn-sample/src-lib/build/classes/kotlin/macosArm64/main/klib/src-lib.klib
MODULE_FRAGMENT name:<com.example:src-lib>
  FILE fqName:com.example.src fileName:/Volumes/disk/git/sample/kn-sample/src-lib/src/commonMain/kotlin/com/example/src/SrcLibrary.kt
    FUN name:getSrcMessage visibility:public modality:FINAL <> () returnType:<unbound IrClassSymbolImpl>
      BLOCK_BODY
        RETURN type=<unbound IrClassSymbolImpl> from='public final fun getSrcMessage (): <unbound IrClassSymbolImpl> declared in com.example.src'
          CONST String type=<unbound IrClassSymbolImpl> value="Message from Src Library"

runtime相关

单打kn runtime

ps:性能正常的电脑上 ./gradlew :kotlin-native:dist 一般也是一两分钟的样子,没必要用这么专门的脚本。注意编sample的kgp gradle任务不把runtime当cache key,修改后runtime的bc后不会重编sample,打包命令要加上 –rerun-tasks

准备

  1. 在sample根目录的gradle.properties中设置kotlin.native.home到本地的 kotlin/kotlin-native/dist 文件夹 image.png
  2. 设置下面脚本中的三个路径

过程:

  1. kotlin项目中跑 :kotlin-native:runtime:ohos_arm64Runtime task将runtime从cpp代码打成bc,输出在 kotlin/kotlin-native/runtime/build/bitcode/main/ohos_arm64
  2. 将 kotlin/kotlin-native/runtime/build/bitcode/main/ohos_arm64 中的bc复制到 kotlin/kotlin-native/dist/konan/targets/ohos_arm64/native
  3. 重新打包sample
  4. 启动应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin_repo=/path/to/kotlin # kotlin仓库根目录地址
sample_repo=/path/to/ovCompose-sample-ohos # sample仓库根目录地址
deveco_studio_install_path=/Applications/DevEco-Studio.app # deveco studio安装文件夹,下一层应该是 Content

set -ex

cd $kotlin_repo
./gradlew :kotlin-native:runtime:ohos_arm64Runtime -Pbootstrap.local=true -Pkotlin.native.enabled --dependency-verification=off

rm -rf $kotlin_repo/kotlin-native/dist/konan/targets/ohos_arm64/native/*
cp $kotlin_repo/kotlin-native/runtime/build/bitcode/main/ohos_arm64/*.bc $kotlin_repo/kotlin-native/dist/konan/targets/ohos_arm64/native/

cd $sample_repo
./gradlew publishDebugBinariesToHarmonyApp --rerun-tasks

cd $sample_repo/harmonyApp
$deveco_studio_install_path/Contents/tools/node/bin/node $deveco_studio_install_path/Contents/tools/hvigor/bin/hvigorw.js --mode module -p module=entry@default -p product=default -p buildMode=debug -p requiredDeviceType=phone assembleHap --analyze=normal --parallel --incremental --daemon

hdc uninstall com.tencent.compose
hdc install $sample_repo/harmonyApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.tencent.compose

效果:runtime中修改了一行代码,开始编译到启动应用耗时1分02s,验证修改生效

image.png

runtime 中c代码跳转

各种ide做c的代码跳转都接受 compile_commands.json 配置,vscode装了微软的c/cpp插件之后应该是会从当前cpp文件递归往上找到最近的 compile_commands.json。这个文件中包含c文件的编译选项,比如宏变量的值,有这个信息之后ide就知道给什么宏下的代码置灰。kn有专门的生成这个json的gradle任务,默认是生成target本机的json。生成target鸿蒙的json,在kotlin仓库根目录执行

1
2
3
./gradlew :kotlin-native:compilationDatabaseOhos_Arm64
find . -name compile_commands.json
cp ./kotlin-native/build/ohos_arm64/compile_commands.json kotlin-native

执行完高亮的就是鸿蒙的宏了

alt text

All Rights Reserved.