ohos_AutoHotkey:基于 AutoHotkey v2 语言的鸿蒙 PC 自动化脚本引擎项目

Windows 上深度耦合 Win32 的自动化/宏工具,热键、键鼠注入、窗口操控等核心能力被鸿蒙沙箱禁止、无法完整移植,遂剥离出与系统无关的 AutoHotkey v2 语言核心,按 v2 语义重写可移植引擎(运算符优先级对标源码 SYM 枚举)、用 OHOS NDK 编成 .so 经 N-API 暴露给 ArkTS,最终在鸿蒙 PC 跑通算术/字符串/控制流/内置函数的脚本编辑与运行。

分支1Tags0
文件最后提交记录最后更新时间
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,平台只有 Win32x64,热键、键鼠钩子、窗口控制全都直接调 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-ohosBasedOnAhkVersion() 报的是 AutoHotkey v2.0

工程结构 / 鸿蒙工程在哪

鸿蒙工程在仓库根目录下的 harmony_pc/,用 DevEco Studio 打开这个目录就行。仓库根目录还留着原版 Windows 的 .sln / .vcxprojsource/,那是原项目,跟鸿蒙构建无关。

下面这棵树是用 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.json5deviceTypes
目标架构 arm64-v8a / x86_64 entry/build-profile.json5abiFilters
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 脚本引擎AppScopeapp_name 的值)。这些都从 app.json5string.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 里构建并运行

  1. DevEco Studio 打开 harmony_pc/
  2. 等 Sync 跑完。第一次构建时 DevEco 会根据 entry/build-profile.json5 里的 externalNativeOptions,调 entry/src/main/cpp/CMakeLists.txtlibahk_engine.so 编出来(arm64-v8a / x86_64);
  3. 装到真机前先确认签名:build-profile.json5 里已经带了一份 default 调试签名配置。如果证书路径在你机器上不存在,去 File > Project Structure > Signing Configs 重新生成一份调试签名即可。未签名的 HAP 能编出来供检查,但装不到设备上;
  4. 接上 2in1 / 平板真机或对应模拟器,选 entry 模块,点 Run。

应用起来后界面上有:环境信息(引擎版本、对标的上游版本)、几个示例脚本(点一下载入编辑器)、一个脚本编辑框、运行 / 清空按钮,以及输出区。点「运行脚本」会把脚本喂给原生引擎,MsgBox / OutputDebug 的内容和最后一个表达式的值会弹窗显示。

命令行构建(接 CI 时)

harmony_pc/ 是标准 hvigor 工程,配好 DEVECO_SDK_HOME 后可以用 hvigor assembleHap 这类命令构建,跟 DevEco 里点构建是一回事。CI 接入按你自己环境的 hvigor 来即可。

具体改了哪些地方

跟「把现成应用塞进壳工程」不一样,这个移植的工作量几乎全在重写一个可移植的语言引擎把它接进 ArkTS这两件事上。逐条说:

语言核心重写成零依赖 C++17ahk_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 结构)、engineVersionbasedOnVersion。返回值统一是一段 JSON(自己写了 JsonEscape 做转义),ArkTS 那边再 parse。这一层刻意做得很薄,所有语言行为都在 ahk_engine.cpp 里,桥接只搬字符串。

ArkTS 三层封装AhkEngineNative.etsimport nativeBridge from 'libahk_engine.so' 把原生模块导成带 TypeScript 类型的接口;AhkRunner.ets 在上面包一层业务,负责调用、JSON.parse、解析失败时兜底成一个 ClientParseError,顺带内置了几个示例脚本;Index.ets 是界面。

界面上同步优先Index.etsrunScript() 用的是 runner.runSync)。异步接口也有,但示例脚本都很小,同步执行用户根本感觉不到卡,而同步能保证「点了一定有结果」——避免某些设备上异步任务静默不 resolve、点了像没反应。点击时还先弹一个「运行中…」的 toast 做即时反馈。这俩都是为了不让人误以为「点了没用」。

界面里写明了适配范围Index.ets 底部「关于适配范围」一段)。热键 / 全局钩子 / 窗口自动化 / COM / DllCall 不支持这件事直接写在 UI 上,说明这些依赖系统级权限的能力在鸿蒙沙箱里实现不了、不在移植范围内,并指向仓库 README。

构建配置entry/build-profile.json5externalNativeOptions 指向 CMakeLists.txtabiFilters 限定 arm64-v8a / x86_64CMakeLists.txt 里给 ahk_engine 加了 -fexceptions -O2 -fvisibility=hidden,链 libace_napi.z.soOHOS_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/elsewhileLoop <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 表达式 / 控制流 / 字符串与数学逻辑并立刻看到结果,这件事能做到;而热键、窗口自动化这些桌面能力,受平台沙箱所限做不出来,文档和应用界面里都照实写了。

项目介绍

Windows 上深度耦合 Win32 的自动化/宏工具,热键、键鼠注入、窗口操控等核心能力被鸿蒙沙箱禁止、无法完整移植,遂剥离出与系统无关的 AutoHotkey v2 语言核心,按 v2 语义重写可移植引擎(运算符优先级对标源码 SYM 枚举)、用 OHOS NDK 编成 .so 经 N-API 暴露给 ArkTS,最终在鸿蒙 PC 跑通算术/字符串/控制流/内置函数的脚本编辑与运行。

定制我的领域