欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/

项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_AutoHotkey

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

这篇文章记录的是一次把 AutoHotkey 适配到 HarmonyOS PC / 鸿蒙 PC 的完整过程。它和我之前写的那些适配(krita、LANDrop、Tesseract、kiwi…)都不太一样——因为 AutoHotkey 是一个无法被「完整」移植到鸿蒙的软件,而这恰恰是它最值得记录的地方:当一个桌面软件的核心功能在目标系统上从架构层面就不被允许时,适配应该怎么做?

最终的结论和做法是:不假装能搬运它的自动化能力,而是把它身上唯一与操作系统无关、又有独立价值的部分——AutoHotkey v2 的脚本语言本身——用 OHOS NDK 交叉编译成原生 .so,经 N-API 暴露给 ArkTS,做成一个能在鸿蒙 PC 上写脚本、跑脚本的「AutoHotkey 脚本引擎」。 这条路线和 Tesseract、kiwi 那两篇的 NDK+NAPI 模式一脉相承。

在这里插入图片描述

一、AutoHotkey 是什么,为什么它和别的项目不一样

AutoHotkey 是 Windows 上非常有名的免费开源自动化 / 宏工具。它有一套自带的脚本语言,最招牌的能力是:

  • 定义全局热键 / 热字串(按个组合键触发动作、打几个字母自动展开);
  • 模拟键盘鼠标,向其它应用发送按键、点击;
  • 操控其它应用的窗口(激活、移动、查找);
  • COM 自动化、DllCall 调任意 DLL、读写注册表。

问题就出在这里。我之前适配的 krita、LANDrop、KeePassXC 之所以能搬到鸿蒙,是因为它们的功能本身是「操作系统中立」的——画图、传文件、存密码,换个 GUI 承载层就能跑。

AutoHotkey 的功能本身,就是上面那些只有桌面系统、且通常需要特权才能做的事。更关键的是:

全局键鼠钩子、向别的应用注入按键、操控别的应用的窗口——这些行为是鸿蒙(以及任何现代沙箱化操作系统)从安全架构上就禁止第三方应用去做的。这不是「移植难度大」,而是「操作系统设计上不允许」。

我先用源码统计确认了这件事,证据非常硬:

源码 34 个 .cpp 文件,34 个全部 #include <windows.h>     (100% 与 Win32 耦合)
入口是 Windows 专属的 _tWinMain
SendInput ×112   SendMessage ×338   SetForegroundWindow ×36
SetWindowsHookEx(全局钩子)  RegisterHotKey(全局热键)  keybd_event(模拟按键)
连「纯数学」的 source/qmath.h 都是 MSVC x86 内联汇编(__asm fld / fsqrt / fsin),
clang / aarch64 根本无法编译

所以一个忠实的 AutoHotkey 移植在鸿蒙上不可能成立。承认这一点,是这次适配的起点,也是它和别的项目最大的不同。

二、路线选择:只移植「语言核心」,不碰自动化

既然自动化、热键、窗口操控都搬不动,那 AutoHotkey 身上还剩什么是有价值、又能搬的?

答案是:它的脚本语言本身。 AutoHotkey v2 内部其实是一个完整的脚本语言解释器——有词法分析、语法分析、表达式求值、对象系统。这部分逻辑(算术、字符串、变量、控制流、内置函数)和「操作系统」没有关系,只要能脱离 windows.h 编译,就能在鸿蒙上跑。

于是路线就清晰了,和 Tesseract / kiwi 那两篇「C++ → NDK 交叉编译 → N-API → ArkTS」完全是同一条:

鸿蒙 ArkTS 页面(脚本编辑器)
  -> AhkRunner 把脚本字符串交给 native
  -> libahk_engine.so (N-API)
  -> AutoHotkey v2 语言核心:词法 -> 语法 -> 求值
  -> 返回 JSON { ok, output, result, errorType, ... }
  -> ArkTS 解析结果、用弹窗 / 输出区展示

和别的路线对比一下,差异很清楚:

Python 库(如 jieba) Tesseract / kiwi(C++) AutoHotkey
库本体 纯 Python C++ C++ 应用(深度耦合 Win32)
能否完整移植 不能(核心功能被沙箱禁止)
移植对象 整个库 整个引擎 只移植语言核心
鸿蒙后端 HNP Python 编进 .so 的 C++ 编进 .so 的 C++
运行时依赖 依赖设备 Python 无,自包含 无,自包含

这条路线诚实又有价值:用户能在鸿蒙 PC 上真的写 AutoHotkey 脚本、做计算、处理字符串,而不是给一个跑不起来的空壳。

三、圈定可移植边界:哪些能搬,哪些搬不动

适配前先把源码过了一遍,明确「能搬 / 搬不动」的边界,这一步省掉了后面大量返工。

搬不动(与 Win32 死耦合,全部排除): 热键、热字串、全局键鼠钩子、SendInput、窗口自动化、COM、DllCall、注册表、GUI 构建器。这些就是 AutoHotkey 不可移植的那一半。

能搬(语言核心,本次移植对象):

  • 三种基本类型 Integer / Float / String,以及 AutoHotkey 的数值⇄字符串自动转换、真值规则(""0"0" 为假);
  • 完整运算符集,优先级逐条对标源码 source/defines.hSYM_* 枚举——这是 AutoHotkey 区别于 C 的关键(位运算优先级高于移位、. 拼接夹在比较与位运算之间、** 右结合且紧于一元负号,-2**2 == -4);
  • 控制流 if / elsewhileLoop N(带 A_Index)、return{} 代码块;
  • 内置函数的 OS 无关子集:Abs Round Floor Ceil Sqrt Sin Cos Tan Exp Ln Log Mod Min MaxInteger Float Number Chr Ord TypeStrLen SubStr InStr StrUpper StrLower StrReplace Trim FormatMsgBox / OutputDebug

这里也要诚实交代:上游的词法 / 语法 / 求值器与 script.cpp 等 3 万行代码、和 windows.h 全耦合,无法直接抽取qmath.h 是 x86 内联汇编不可复用。所以语言核心是按 AutoHotkey v2 的行为重新实现的一份可移植 C++,运算符语义严格对标源码枚举。这一点在工程的 README 里写得很清楚,不夸大成「搬运了上游解释器」。

四、先在开发机上把语言引擎测对

C++ 适配有个很实用的习惯:先把最核心、最容易出错的那层在开发机上验证通过,再去搭 UI。对这次来说,最核心的就是语言引擎本身——词法、语法、求值。

所以我把引擎 ahk_engine.cpp 写成和 N-API 完全解耦:它不引用任何 OpenHarmony / NAPI 头文件,错误一律通过返回值 RunResult 上报、绝不抛到边界外。这样它在开发机上用普通 clang++ 就能编译运行,不需要鸿蒙 SDK 就能先把语言逻辑测对。

配套写了一个 host_test.cpp,覆盖运算符优先级、字符串 / 数学函数、变量与 if / while / Loop 控制流、三元、正则、真值规则,以及除零 / 类型错误 / 未定义变量 / 语法错误这些错误用例。开发机上一条命令编译并运行:

cd ohos_AutoHotkey/harmony_pc/host_test
c++ -std=c++17 -Wall -Wextra -I../entry/src/main/cpp \
    host_test.cpp ../entry/src/main/cpp/ahk_engine.cpp -o host_test && ./host_test

全部 52 个用例通过、-Wall -Wextra 零警告——这就是「语言核心确实脱离了 Win32、可移植」的实证。如果这一层就错了,后面 ArkTS 和 Native 写得再漂亮也没用。

在这里插入图片描述

五、鸿蒙 PC 工程结构

适配完成后,仓库里新增了 ohos_AutoHotkey/harmony_pc/ 工程,结构和 kiwi / Tesseract 那两篇类似,cpp/ 是重点:

ohos_AutoHotkey/
├── README.md                          适配说明(范围 / 证据 / 真实复用 vs 重实现 / 路线图)
└── harmony_pc/                        DevEco 工程根
    ├── build-profile.json5 / oh-package.json5 / hvigorfile.ts
    ├── AppScope/                      应用级配置 + 图标
    └── entry/src/main/
        ├── module.json5
        ├── cpp/
        │   ├── CMakeLists.txt          编 ahk_engine.cpp + napi_init.cpp -> libahk_engine.so
        │   ├── ahk_engine.h / .cpp     ★ AutoHotkey v2 语言核心(纯 C++,零 Win32)
        │   ├── napi_init.cpp           N-API 桥(脚本字符串进 / JSON 出)
        │   └── types/libahk_engine/    .so 的 TS 声明
        ├── ets/
        │   ├── native/AhkEngineNative.ets   .so 的 ArkTS 封装
        │   ├── ahk/AhkRunner.ets             调用 native、解析 JSON
        │   └── pages/Index.ets               脚本编辑器 + 示例 + 运行 + 输出
        └── resources/

边界很清晰:语言核心集中在 cpp/ahk_engine.*,N-API 只做字符串和 JSON 的进出,ArkTS 分三层(页面 → 业务 Client → native 封装),和 kiwi 的分层完全一致。

在这里插入图片描述

六、Native / N-API:脚本进,JSON 结果出

ArkTS 不能直接调 C++ 引擎,中间靠 N-API 桥接。napi_init.cpp 只暴露四个方法,且只做字符串 / JSON 的搬运:

runScript(source: string): Promise<string>    // 脚本字符串进,JSON 结果出(异步,工作线程)
runScriptSync(source: string): string          // 同步版,结果 JSON 一致
engineVersion(): string                        // 移植版本
basedOnVersion(): string                       // 对标的上游 AutoHotkey 版本

真正的执行在引擎里完成,桥只把 RunResult 拼成一段 JSON 返回,页面解析后展示:

{
  "ok": true,
  "output": "1..100 的和 = 5050\n",
  "result": "1..100 的和 = 5050",
  "errorType": "",
  "errorMessage": "",
  "errorLine": 0
}

CMake 这边也只做一件事:把引擎和桥两个 .cpp 一起编成 libahk_engine.so,链接鸿蒙的 N-API 运行时。因为语言核心是自包含的纯 C++,不需要 Tesseract 那种预编译依赖链脚本,一次就能编出来,是最易构建的一档:

add_library(ahk_engine SHARED napi_init.cpp ahk_engine.cpp)
target_compile_options(ahk_engine PRIVATE -fexceptions -O2 -fvisibility=hidden)
target_link_libraries(ahk_engine PUBLIC libace_napi.z.so)

七、ArkTS 页面:脚本编辑器 + 示例 + 运行 + 弹窗

页面 Index.ets 做成了一个能直接上手的脚本运行器:顶部是环境检查(显示移植版本、对标的上游版本),中间是六个一键载入的示例脚本和一个脚本编辑器,下面是运行按钮和输出区。

这里有一个很关键的体验细节,也是适配里专门修过的一个坑。桌面版 AutoHotkey 的 MsgBox "xxx"弹一个对话框;而最初我把 MsgBox 的内容写进了页面底部的「输出区」。结果用户按 AutoHotkey 的习惯点「运行」、等弹窗,没看到弹窗就以为「点了没反应」——其实结果早就显示在下方输出区了。

所以最终版改成:点运行后立刻冒一个 运行中… 的 Toast 给即时反馈,MsgBox 的结果用真正的弹窗对话框显示(贴近 AutoHotkey 体验、不会被忽略),同时改用同步执行排除异步在真机上不回调的可能:

private runScript(): void {
  promptAction.showToast({ message: '运行中…', duration: 800 });   // 即时反馈
  const r = this.runner.runSync(this.script);
  if (r.ok) {
    const popup = r.output.length > 0 ? r.output : ('结果: ' + r.result);
    this.showDialog('运行结果', popup);   // MsgBox -> 弹窗,不会被忽略
  } else {
    this.showDialog('运行出错', r.errorType + '\n' + r.errorMessage);
  }
}

页面底部还专门放了一块「关于适配范围」的说明,老老实实告诉用户:本应用移植的是 AutoHotkey 的语言核心,热键 / 键鼠注入 / 窗口自动化 / COM / DllCall 依赖系统级权限、在鸿蒙沙箱中无法实现,不在范围内。不藏着掖着。

八、构建:把 .so 和 ArkTS 一起编出来

工程在 DevEco Studio 里可以直接构建。entry/build-profile.json5externalNativeOptions 会驱动 DevEco 调 cpp/CMakeLists.txt,用 OHOS NDK 的 clang 把引擎交叉编译成 libahk_engine.soarm64-v8a 真机 / x86_64 模拟器),自动打进 HAP 的 libs/

和 kiwi 那篇一样,这里也要注意 compatibleSdkVersion 要和本机 SDK 匹配(本机是 API 21 就写 6.0.1(21),否则会报 Unable to find the compatibleSdkVersion )。构建日志里能看到 Native 和 ArkTS 两条线都编了,最后签名打包:

Finished :entry:default@BuildNativeWithCmake
Finished :entry:default@BuildNativeWithNinja
Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
Finished :entry:default@SignHap
BUILD SUCCESSFUL

构建产物里能看到 libahk_engine.so 同时编出了 arm64-v8ax86_64 两个架构,已打进 HAP。

在这里插入图片描述

九、装到真机并实测

构建出签名 HAP 后,用 hdc 装到鸿蒙 PC 上并启动:

HDC=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
$HDC install -r entry/build/default/outputs/default/entry-default-signed.hap
$HDC shell aa start -a EntryAbility -b com.autohotkey.ohos.engine

装好后在设备上实测,编辑器里就是默认脚本——用 Loop 把 1 到 100 累加:

total := 0
Loop 100
{
  total += A_Index
}
MsgBox "1..100 的和 = " . total

点「运行脚本」,立刻冒出 运行中… 提示,紧接着弹出对话框:1..100 的和 = 5050。这说明从脚本字符串 → N-API → C++ 引擎求值 → JSON 回传 → ArkTS 弹窗的整条链路在真机上跑通了。

在这里插入图片描述

再点几个示例验证不是写死的结果:

  • 字符串处理MsgBox Format("你好, {1}! 长度={2}", name, StrLen(name)) —— 弹出 你好, 鸿蒙 PC! 长度=5
  • 数学函数Round(Sqrt(2), 6)Max / Min / Mod —— 数值正确;
  • if / else 判断三元 + 正则匹配 —— 逻辑正确。

每个示例都是引擎真在求值,换输入结果就跟着变。

在这里插入图片描述

十、踩坑与总结

这次适配踩到、并值得记下来的几个点:

  1. 预览器不加载原生 .so DevEco 的 Previewer 不会加载 Native 模块,import 'libahk_engine.so' 在预览器里必然失败——原生模块的功能必须用模拟器或真机验证。
  2. 「点了没反应」往往是反馈不可见,不是真没跑。 桌面习惯期待 MsgBox 弹窗,而结果在输出区。改成 Toast + 弹窗对话框后体验立刻对了。
  3. compatibleSdkVersion 要和本机 SDK 对齐,否则命令行 / DevEco 构建直接报错。
  4. 诚实划定范围比硬撑更重要。 AutoHotkey 注定无法完整移植,与其做个跑不起来的空壳,不如把能搬的语言核心做扎实、并明确告诉用户哪些做不了。

完成后,这个应用是一个自包含、安装即用的鸿蒙 PC「AutoHotkey 脚本引擎」:

  • 不依赖设备上任何运行时,语言核心编进 .so,安装即用;
  • 运算符 / 优先级 / 类型语义严格对标 AutoHotkey v2 源码;
  • 引擎与 N-API 解耦,开发机上普通 C++ 编译器就能先测对(52/52 通过);
  • DevEco 构建通过,真机实测算术、字符串、数学、控制流、正则全部正常。

从这次适配能总结出一条和「整库移植」不一样的思路——当一个软件的核心功能在目标系统上从架构层面就不被允许时,不要假装能整体搬运,而是剥离出它「与操作系统无关」的那部分内核去移植

先确认哪些功能是目标系统禁止的(沙箱权限边界)
  -> 圈定「与 OS 无关、又有独立价值」的内核(这里是语言解释器)
  -> 把内核与 N-API 解耦,在开发机上测对
  -> NDK 交叉编译进 .so,N-API 只做字符串 / JSON 进出
  -> ArkTS 做成能上手的页面,并诚实标注不可用的能力
  -> DevEco / 真机验证闭环

后续如果要继续往「FULL 档」推进,可以接入 AutoHotkey 自带的 lib_pcre(纯 C、可移植)替换正则占位,拿到与上游完全一致的 RegEx 语法;再为上游解释器写一层 TCHAR→char 的可移植 shim,分阶段把对象系统、用户自定义函数搬过来。但就「让鸿蒙 PC 用户能真的写、跑 AutoHotkey 脚本」这个目标而言,当前这一档已经闭环。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐