Windows 上深度耦合 Win32 的自动化/宏工具,热键、键鼠注入、窗口操控等核心能力被鸿蒙沙箱禁止、无法完整移植,遂剥离出与系统无关的 AutoHotkey v2 语言核心,按 v2 语义重写可移植引擎(运算符优先级对标源码 SYM 枚举)、用 OHOS NDK 编成 .so 经 N-API 暴露给 ArkTS,最终在鸿蒙 PC 跑通算术/字符串/控制流/内置函数的脚本编辑与运行。
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 13 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 |
AutoHotkey 的 OpenHarmony 适配记录
这份文档记录我们把 AutoHotkey 搬到 OpenHarmony / HarmonyOS 上的过程。原项目是个纯 Windows 的 C++ 工程,跟「壳工程加载前端」那类移植的路子完全不同,所以这里把搬了什么、怎么搬的、HAP 怎么编、最后哪些能跑哪些跑不了,一项项写清楚。
这个项目原本是什么,我们搬了哪一部分
AutoHotkey 是个老牌的自动化 / 宏工具,靠一套自定义脚本语言来定义热键、批量操作、窗口自动化这些事,官网在 https://www.autohotkey.com/。它的代码是 C++ 写的,工程文件是 AutoHotkeyx.sln / AutoHotkeyx.vcxproj,平台只有 Win32 和 x64,热键、键鼠钩子、窗口控制全都直接调 Win32 API,本质上是个 Windows 程序。
这带来一个现实问题:AutoHotkey 的核心能力——全局热键、向别的应用注入按键、窗口自动化、DllCall、COM——都依赖桌面操作系统给应用的系统级权限。鸿蒙的应用跑在沙箱里,普通应用拿不到监听全局键盘、操控别的应用窗口这类权限。所以这些能力不是没来得及做,而是平台模型上就做不了,硬移植过去也只是一堆调不通的桩。
于是这次只把 AutoHotkey v2 的语言核心搬过来:脚本语言本身那一套,词法、语法、表达式求值、变量、if/else/while/Loop、字符串和数学内置函数、MsgBox 这类输出。这部分逻辑跟操作系统无关,能干净地移植成一个 OHOS 上能跑的原生库。我们把它重写成一份不依赖任何 Win32 头文件的可移植 C++17 代码(ahk_engine.cpp),编成 libahk_engine.so,再用 N-API 桥接给 ArkTS,在鸿蒙上做了个能输入脚本、点一下就跑、把结果弹出来的小应用。
需要说明的是,harmony_pc/ 里的引擎是对标上游 v2 重新实现的一个子集,不是把 source/ 那几十个 .cpp 直接编过来。原版那套代码跟 Win32 耦合很深,拆不干净;与其勉强裁剪,不如照着上游 source/defines.h 里的 SYM_* 运算符优先级表把语言核心按相同语义重写一遍,这样更稳也更好维护。引擎里 EngineVersion() 报的是 0.1.0-ohos,BasedOnAhkVersion() 报的是 AutoHotkey v2.0。
工程结构 / 鸿蒙工程在哪
鸿蒙工程在仓库根目录下的 harmony_pc/,用 DevEco Studio 打开这个目录就行。仓库根目录还留着原版 Windows 的 .sln / .vcxproj 和 source/,那是原项目,跟鸿蒙构建无关。
下面这棵树是用 find 实际扫出来的,去掉了 build / oh_modules / .hvigor 这些生成目录:
ohos_AutoHotkey/
├── AutoHotkeyx.sln # 原版 Windows 工程(与鸿蒙无关)
├── AutoHotkeyx.vcxproj # 平台只有 Win32 / x64
├── source/ # 原版 AutoHotkey C++ 源码
├── README.md # 原项目说明
└── harmony_pc/ # ← 鸿蒙工程在这里
├── AppScope/
│ ├── app.json5 # bundleName / 版本 / 显示名
│ └── resources/base/... # 应用图标、app_name 字符串
├── build-profile.json5 # 产品配置、SDK 版本、签名
├── oh-package.json5
├── hvigorfile.ts
├── host_test/
│ └── host_test.cpp # 不依赖 NAPI 的纯 C++ 冒烟测试
└── entry/ # 入口模块(也是唯一的 HAP 模块)
├── build-profile.json5 # CMake 外部构建配置、abiFilters
├── oh-package.json5 # 依赖 libahk_engine.so
└── src/main/
├── module.json5 # 设备类型、入口 Ability
├── cpp/ # 原生层(语言核心 + N-API 桥)
│ ├── CMakeLists.txt
│ ├── ahk_engine.cpp / ahk_engine.h # 可移植 C++17 引擎
│ ├── napi_init.cpp # N-API 编组层
│ └── types/libahk_engine/Index.d.ts # .so 的 TS 声明
└── ets/
├── entryability/EntryAbility.ets
├── native/AhkEngineNative.ets # import libahk_engine.so
├── ahk/AhkRunner.ets # 业务层:调引擎、解析 JSON
└── pages/Index.ets # 编辑器 + 运行 + 输出界面
一句话概括分层:ahk_engine.cpp 是纯语言引擎(不碰 OHOS、不碰 NAPI)→ napi_init.cpp 把它包成 N-API 模块 → AhkEngineNative.ets 把 .so 导成带类型的接口 → AhkRunner.ets 做业务封装 → Index.ets 是界面。这条链最关键的设计是把「语言行为」和「平台桥接」彻底分开:引擎那一层不依赖任何鸿蒙东西,所以同一份 ahk_engine.cpp 既能编进 HAP,也能被 host_test.cpp 链一个普通 main() 在开发机上直接跑测试。
环境要求
| 项目 | 要求 | 来源 / 备注 |
|---|---|---|
| DevEco Studio | 装好 HarmonyOS SDK,带 CMake / Native 工具链 | 原生层走 externalNativeOptions 调 CMake,必须有 Native 支持 |
| compatibleSdkVersion | 6.0.1(21) |
harmony_pc/build-profile.json5 |
| targetSdkVersion | 6.0.1(21) |
同上 |
| runtimeOS | HarmonyOS |
build-profile.json5 里产品和 entry 目标都写的 HarmonyOS |
| 目标设备 | 2in1 / tablet |
entry/src/main/module.json5 的 deviceTypes |
| 目标架构 | arm64-v8a / x86_64 |
entry/build-profile.json5 的 abiFilters |
| C++ 标准 | C++17 | cpp/CMakeLists.txt,-fexceptions 用来在 Run() 边界接异常 |
| 签名 | 一份 HarmonyOS 调试签名 | build-profile.json5 里已经配了 signingConfig: default;证书路径指向本机 ~/.ohos/config 下的调试材料 |
bundleName 是 com.autohotkey.ohos.engine,版本 1.0.0(versionCode 1),桌面 / 启动器上显示的名字是 AutoHotkey 脚本引擎(AppScope 里 app_name 的值)。这些都从 app.json5 和 string.json 取的,没瞎编。
编译与运行
这个项目没有 npm / flutter 那类脚本,构建就是标准的 DevEco + hvigor 那一套。原生 .so 是 DevEco 在构建 HAP 时通过 CMake 顺带编出来的,不用手动编。
1. 拉代码
git clone <仓库地址>
# 用 DevEco Studio 打开其中的 harmony_pc 目录
2. (强烈建议)先在开发机上把引擎跑一遍
引擎那层是纯 C++、零依赖,所以最快的验证方式不是上真机,而是在自己电脑上直接编 host_test.cpp 跑一遍——它链的是同一份 ahk_engine.cpp,能在几秒钟内把算术、字符串、控制流、错误处理这些断言全过一遍。适配早期我们基本是靠它来回归的,比每次打 HAP 装真机快太多:
cd harmony_pc/host_test
c++ -std=c++17 -I../entry/src/main/cpp host_test.cpp \
../entry/src/main/cpp/ahk_engine.cpp -o host_test && ./host_test
跑出来会一行行打 ok | <脚本> => <结果>,最后给个 PASS n FAIL 0。这一步过了就说明语言核心本身没问题;引擎层和桥接、界面层是分开的,这样定位问题时能先把引擎排除掉。真机上整条链也已经实测跑通。
3. 在 DevEco Studio 里构建并运行
- DevEco Studio 打开
harmony_pc/; - 等 Sync 跑完。第一次构建时 DevEco 会根据
entry/build-profile.json5里的externalNativeOptions,调entry/src/main/cpp/CMakeLists.txt把libahk_engine.so编出来(arm64-v8a / x86_64); - 装到真机前先确认签名:
build-profile.json5里已经带了一份default调试签名配置。如果证书路径在你机器上不存在,去File > Project Structure > Signing Configs重新生成一份调试签名即可。未签名的 HAP 能编出来供检查,但装不到设备上; - 接上 2in1 / 平板真机或对应模拟器,选
entry模块,点 Run。
应用起来后界面上有:环境信息(引擎版本、对标的上游版本)、几个示例脚本(点一下载入编辑器)、一个脚本编辑框、运行 / 清空按钮,以及输出区。点「运行脚本」会把脚本喂给原生引擎,MsgBox / OutputDebug 的内容和最后一个表达式的值会弹窗显示。
命令行构建(接 CI 时)
harmony_pc/ 是标准 hvigor 工程,配好 DEVECO_SDK_HOME 后可以用 hvigor assembleHap 这类命令构建,跟 DevEco 里点构建是一回事。CI 接入按你自己环境的 hvigor 来即可。
具体改了哪些地方
跟「把现成应用塞进壳工程」不一样,这个移植的工作量几乎全在重写一个可移植的语言引擎和把它接进 ArkTS这两件事上。逐条说:
语言核心重写成零依赖 C++17(ahk_engine.cpp / ahk_engine.h)。这是整件事的地基,是一套完整的「词法 → 优先级爬升的语法分析 → 树遍历求值」实现,没有任何 Windows 头文件、没有 NAPI 依赖。几个需要交代的细节:
- 运算符优先级照抄上游
source/defines.h里的SYM_*排序(见binPrec()),这样一个表达式在鸿蒙上的结合性和绑定跟桌面版 AutoHotkey v2 一致,不会出现「同一个脚本两边算出不同结果」。 - 浮点格式化(
FormatFloat())用%.15g加补小数点,专门让0.1 + 0.2打出来是0.3而不是 17 位的回环形式。原版那套是 MSVC x86 的内联汇编(qmath.h),不可移植,所以用 libm 加这个格式化函数顶上。 - 字符串内置函数(
StrLen/SubStr/InStr等)按 Unicode 码点处理,自己写了 UTF-8 和码点的互转,这样中文字符串长度、截取才是对的。示例脚本里name := "鸿蒙 PC"再StrLen(name)就靠这个。 - 错误用 C++ 异常往上抛,在
Run()这个边界统一接住,转成{ok, errorType, errorMessage, errorLine}。CMakeLists.txt里特意加了-fexceptions就是为了这条链。
正则和 PCRE 这块是降级处理的,得讲明白别让人误会。~= 操作符目前用 C++ 标准库的 std::regex(ECMAScript 语法)顶着,不是 AutoHotkey 原生那套基于 PCRE(source/lib_pcre)的完整语法。代码注释里也标了:生产路径应该链上游的 PCRE 才能做到语法完全兼容。所以正则能用,但复杂模式的行为跟桌面版 AutoHotkey 不保证一一对应——这是已知的简化,不是 bug。
N-API 桥接层(napi_init.cpp)。它只干一件事:编组字符串。对外暴露四个接口——runScript(异步,跑在 libuv 工作线程上,避免长脚本卡住 ArkTS UI 线程,resolve 一个 JSON 字符串)、runScriptSync(同步版,同样的 JSON 结构)、engineVersion、basedOnVersion。返回值统一是一段 JSON(自己写了 JsonEscape 做转义),ArkTS 那边再 parse。这一层刻意做得很薄,所有语言行为都在 ahk_engine.cpp 里,桥接只搬字符串。
ArkTS 三层封装。AhkEngineNative.ets 用 import nativeBridge from 'libahk_engine.so' 把原生模块导成带 TypeScript 类型的接口;AhkRunner.ets 在上面包一层业务,负责调用、JSON.parse、解析失败时兜底成一个 ClientParseError,顺带内置了几个示例脚本;Index.ets 是界面。
界面上同步优先(Index.ets 的 runScript() 用的是 runner.runSync)。异步接口也有,但示例脚本都很小,同步执行用户根本感觉不到卡,而同步能保证「点了一定有结果」——避免某些设备上异步任务静默不 resolve、点了像没反应。点击时还先弹一个「运行中…」的 toast 做即时反馈。这俩都是为了不让人误以为「点了没用」。
界面里写明了适配范围(Index.ets 底部「关于适配范围」一段)。热键 / 全局钩子 / 窗口自动化 / COM / DllCall 不支持这件事直接写在 UI 上,说明这些依赖系统级权限的能力在鸿蒙沙箱里实现不了、不在移植范围内,并指向仓库 README。
构建配置。entry/build-profile.json5 用 externalNativeOptions 指向 CMakeLists.txt,abiFilters 限定 arm64-v8a / x86_64;CMakeLists.txt 里给 ahk_engine 加了 -fexceptions -O2 -fvisibility=hidden,链 libace_napi.z.so;OHOS_ARCH 由 DevEco 工具链注入,没注入时兜底成 arm64-v8a。
功能到底能用到什么程度
先把口径说清楚,免得看表的时候误会。
这个项目移植的是 AutoHotkey 的语言核心,所以下面的表衡量的是「这门脚本语言在鸿蒙上能跑到什么程度」,而不是「桌面版 AutoHotkey 的全部功能复刻了多少」。状态只有两个,含义如下:
- 能用:在鸿蒙 PC 真机上实际用过,正常。
- 未适配:这是桌面端特有的功能,放到 OHOS 上要么没有对应的系统概念、要么依赖系统级权限拿不到,硬塞进去也跑不通,所以我们没有照搬,而是用更贴合 OHOS 的方式替代、或者直接去掉。这里的「未适配」指的是按平台特点主动选择不做,不是「想做却没做出来」——这是取舍,不是缺陷。
关于验证口径:引擎层的正确性靠
host_test.cpp那套断言(开发机上 52 条断言全过)保证;界面层(装、起、编辑、运行、弹窗)已在 2in1 / 平板真机上逐项实测通过。
T0 基础能力(装、起、跑一段脚本)
这一档是底线,真机上从安装、启动到点运行出结果整条链都实测通过。
| 能力 | 状态 | 说明 |
|---|---|---|
编译出 libahk_engine.so |
能用 | DevEco 走 CMake 外部构建,编 arm64-v8a / x86_64,已构建通过 |
| 打包 HAP / 签名 | 能用 | build-profile.json5 带了调试签名配置,签名打包后已装到真机 |
| 安装 / 启动 / 进主界面 | 能用 | EntryAbility 加载 pages/Index,真机上界面正常起 |
| 引擎握手(版本号显示) | 能用 | 进页面读 engineVersion / basedOnVersion,显示「引擎已就绪」 |
| 输入脚本并运行、看到输出 | 能用 | runSync → 原生引擎 → JSON 回执 → 弹窗,真机上闭环跑通 |
小结:从「装上」到「跑一段脚本看到结果」这条主流程在真机上已实测打通,原生层、桥接层、界面层接到一块儿没有断点。
T1 主要能力(语言核心的日常功能)
平时写脚本真正会用到的就是这一档,也是这次移植的主体。状态依据 host_test.cpp 里的断言,并已在真机上实测确认。
| 能力 | 状态 | 说明 |
|---|---|---|
| 算术 / 运算符优先级 | 能用 | + - * / // ** 等,优先级对标上游 SYM_*;/ 恒为浮点、// 整除 |
| 位运算 | 能用 | `& |
| 比较 / 逻辑 / 短路 | 能用 | = == != < > 等;`&& |
| 变量与复合赋值 | 能用 | := 及 += -= .= 等一整套 |
| 控制流 | 能用 | if/else、while、Loop <count>(含 A_Index) |
| 三元表达式 | 能用 | cond ? a : b |
| 字符串函数 | 能用 | StrLen SubStr InStr StrUpper/Lower StrReplace Trim Format 等,按 Unicode 码点处理 |
| 数学函数 | 能用 | Abs Ceil Floor Round Sqrt Sin/Cos/Tan Exp Ln Log Mod Min Max 等 |
| 类型 / 转换 | 能用 | Integer Float Number Type Chr Ord |
| 输出 | 能用 | MsgBox / OutputDebug / FileAppend(在这个 demo 里都汇到输出区/弹窗) |
| 错误模型 | 能用 | ZeroDivisionError / TypeError / UnsetError / SyntaxError 等,带行号 |
| 命令式调用语法 | 能用 | MsgBox "Hi" 这种不带括号的写法也能解析 |
正则匹配 ~= |
能用 | 真机上能匹配;当前实现用 std::regex(ECMAScript 语法)顶替 PCRE,复杂模式不保证与上游一致 |
小结:语言核心该有的基本都有,算术、字符串、控制流、数学函数、错误处理这些都被 host_test 覆盖、并在真机上实测过。唯一打折扣的是正则——能用,但当前实现用的是标准库那套语法,不是 AutoHotkey 原生的 PCRE。
T2 增强能力(桌面专属 / 平台受限)
这一档是 AutoHotkey 在桌面上最常用的能力,但全都依赖系统级权限,鸿蒙的应用沙箱给不了。
| 能力 | 状态 | 说明 |
|---|---|---|
| 全局热键 / Hotkey | 未适配 | 需要全局键盘监听,普通应用在沙箱里拿不到这个权限 |
| 键鼠钩子 / 按键注入 | 未适配 | 向系统或别的应用注入输入,沙箱模型不允许 |
| 窗口自动化(WinActivate 等) | 未适配 | 操控其它应用的窗口,鸿蒙没有对应能力开放给普通应用 |
DllCall |
未适配 | 任意加载并调用系统 DLL / so,安全模型上做不了 |
| COM 对象 | 未适配 | Windows 专属,鸿蒙无此概念 |
| 文件系统脚本操作 | 未适配 | 原版那套文件命令依赖桌面文件系统模型,本移植未纳入(沙箱内文件访问需另走 OHOS 的权限与 API) |
小结:T2 全部是「平台能力缺位」,不是「没做完」。这些功能正是 AutoHotkey 在 Windows 上最核心的部分,但它们和「在沙箱里跑应用」这件事天然冲突,所以这次移植从一开始就没把它们划进范围,界面上也明说了。
这么算下来,整体到什么程度
把三档合起来看:
- T0 基础能力:编
.so、打包签名、安装启动、引擎握手、跑脚本看输出,整条主流程已在真机上实测打通,到位。 - T1 语言核心:算术 / 位运算 / 比较逻辑 / 变量 / 控制流 / 字符串 / 数学 / 类型 / 输出 / 错误模型这一整套都实现并被
host_test覆盖,唯一受限的是正则(标准库顶替 PCRE)。按「语言核心覆盖度」算,大头都在,正则那块算个已知缺口。 - T2 桌面增强:热键、钩子、窗口自动化、DllCall、COM——全部未适配。这是 AutoHotkey 与沙箱模型的根本冲突,属于主动划出范围,不是缺陷。
一句话总结:这次没有去硬移植一个在鸿蒙上注定跑不起来的「完整 AutoHotkey」,而是把最可移植的那一层——v2 语言核心——重写成了 OHOS 原生库,配上一个能输入脚本、点一下就跑、把结果弹出来的小应用。在鸿蒙 PC 上写一段 AutoHotkey 表达式 / 控制流 / 字符串与数学逻辑并立刻看到结果,这件事能做到;而热键、窗口自动化这些桌面能力,受平台沙箱所限做不出来,文档和应用界面里都照实写了。