文中代码已过时,更新地址 在 LuaJIT 中调用 AHK

LuaJIT 和 AHK 配合使用有两种方法,一种是在 AHK 里调用 LuaJIT 的 dll,另一种是在 LuaJIT 里调用 AHK 的 dll。本文简单讲下后者。

因为 AHK 官方不提供 dll 文件,可以使用 AutoHotkey_H v1,64 位的用 x64w/AutoHotkey.dll,32 位的用 Win32w/AutoHotkey.dll,代码应该是一样的。

AutoHotkey_H 导出的函数只有十几个,自带的文档里说明了用法,我们可以挑选一些来使用。代码都是 UTF-8 编码的。

先准备一个字符转码库:

-- utf8fix.lua

-- https://github.com/actboy168/ydhost/blob/master/tools/mapdump/lua/ffi/unicode.lua

--[[ Usage 
local utf8fix = require 'utf8fix'
local L = utf8fix.L
local A = utf8fix.A
--]]

local ffi = require 'ffi'

ffi.cdef[[
int MultiByteToWideChar(unsigned int CodePage,
    unsigned long dwFlags,
    const char* lpMultiByteStr,
    int cbMultiByte,
    wchar_t* lpWideCharStr,
    int cchWideChar);

int WideCharToMultiByte(unsigned int CodePage,
    unsigned long dwFlags,
    const wchar_t* lpWideCharStr,
    int cchWideChar,
    char* lpMultiByteStr,
    int cchMultiByte,
    const char* lpDefaultChar,
    int* pfUsedDefaultChar);
]]

local CP_UTF8 = 65001
local CP_ACP = 0

-- UTF-8 to UTF-16
local function u2w(input)
    local wlen = ffi.C.MultiByteToWideChar(CP_UTF8, 0, input, #input, nil, 0)
    local wstr = ffi.new('wchar_t[?]', wlen + 1)
    ffi.C.MultiByteToWideChar(CP_UTF8, 0, input, #input, wstr, wlen)

    return wstr, wlen
end

-- UTF-16 to UTF-8
local function w2u(wstr, wlen)
    local len = ffi.C.WideCharToMultiByte(CP_UTF8, 0, wstr, wlen, nil, 0, nil, nil)
    local str = ffi.new('char[?]', len + 1)
    ffi.C.WideCharToMultiByte(CP_UTF8, 0, wstr, wlen, str, len, nil, nil)

    return ffi.string(str)
end

-- ANSI to UTF-16
local function a2w(input)
    local wlen = ffi.C.MultiByteToWideChar(CP_ACP, 0, input, #input, nil, 0)
    local wstr = ffi.new('wchar_t[?]', wlen + 1)
    ffi.C.MultiByteToWideChar(CP_ACP, 0, input, #input, wstr, wlen)

    return wstr, wlen
end

-- UTF-16 to ANSI
local function w2a(wstr, wlen)
    local len = ffi.C.WideCharToMultiByte(CP_ACP, 0, wstr, wlen, nil, 0, nil, nil)
    local str = ffi.new('char[?]', len + 1)
    ffi.C.WideCharToMultiByte(CP_ACP, 0, wstr, wlen, str, len, nil, nil)

    return ffi.string(str)
end

return {
    u2w = u2w,
    w2u = w2u,
    a2w = a2w,
    w2a = w2a,
    u2a = function(input)
        return w2a(u2w(input))
    end,
    a2u = function(input)
        return w2u(a2w(input))
    end,
    L = function(input)
        if input == nil then
            return nil
        end

        return (u2w(input))
    end,
    A = function(input)
        if input == nil then
            return nil
        end

        return w2a(u2w(input))
    end,
    Pass = function(input)
        return input
    end,
}

主体代码:

-- ahk.lua

local M = {}

local utf8fix = require 'utf8fix'
local L = utf8fix.L
local w2u = utf8fix.w2u

local ffi = require 'ffi'

ffi.cdef[[
typedef wchar_t *Str;
typedef unsigned int UInt;
typedef UInt *Handle;

// scriptPath: A path to existing ahk file.
// options: Additional parameter passed to AutoHotkey.dll.
// parameters: Parameters passed to dll.
// return: A thread handle.
Handle ahkdll(Str scriptPath, Str options, Str parameters);

// script: A string with ahk script.
// options: Additional parameter passed to AutoHotkey.dll.
// parameters: Parameters passed to dll.
// return: A thread handle.
Handle ahktextdll(Str script, Str options, Str parameters);

// return: 1 if a thread is running or 0 otherwise.
int ahkReady();

// script: A string with ahk script.
// return: 1 if script was executed and 0 if there was an error.
int ahkExec(Str script);

// varName: Name of a variable.
// value: Name of a variable.
// return: 0 on success and -1 on failure.
int ahkassign(Str varName, Str value);

// varName: Name of variable to get value from.
// getPointer: Use 1 to get pointer of variable, else 0 to get the value.
// return: Always a string, empty string if variable does not exist or is empty.
Str ahkgetvar(Str varName, UInt getPointer);

// timeout: Time in milliseconds to wait until thread exits.
// return: Returns always 0.
int ahkTerminate(int timeout);

// timeout: Time in milliseconds to wait until thread exits.
// return: ?
int ahkReload(int timeout);

// operation: Pause or un-pause a script, on/off/1/0.
// return: 1 if thread is paused or 0 if it is not.
int ahkPause(Str operation);

// Launch a Goto/GoSub routine in script.
// name: Name of label to execute.
// nowait: Do not to wait until execution finished.
// return: 1 if label exists 0 otherwise.
UInt ahkLabel(Str name, UInt nowait);

// Launch a function in script.
// name: Name of function to call.
// p*: Name of function to call.
// return: Always a string.
Str ahkFunction(Str name, Str p1, Str p2, Str p3, Str p4, Str p5,
    Str p6, Str p7, Str p8, Str p9, Str p10);

// Like ahkFunction, but run in background and return value will be ignored.
UInt ahkPostFunction(Str name, Str p1, Str p2, Str p3, Str p4, Str p5,
    Str p6, Str p7, Str p8, Str p9, Str p10);

// addFile
// addScript
// ahkExecuteLine 
// ahkFindLabel
// ahkFindFunc 
]]

function M.init()
    if M.ahk ~= nil then
        return
    end

    M.ahk = ffi.load('AutoHotkey')

    -- ahktextdll 一定要先调用,第一个参数是 AHK 代码内容
    -- #Persistent 让 AHK 一直运行,不然退出了,后边就没法用了
    -- SetBatchLines, -1 全速运行
    M.ahk.ahktextdll(L"#NoEnv\n#NoTrayIcon\n#Persistent\nSetBatchLines, -1", nil, nil)
end

function M.exec(script)
    return M.ahk.ahkExec(L(script))
end

function M.getvar(name)
    local ret = M.ahk.ahkgetvar(L(name), 0)
    return w2u(ret, ffi.sizeof(ret))
end

function M.func(name, ...)
    local p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 = ...
    local ret = M.ahk.ahkFunction(L(name), L(p1), L(p2), L(p3), L(p4), L(p5),
        L(p6), L(p7), L(p8), L(p9), L(p10))
    return w2u(ret, ffi.sizeof(ret))
end

return M

调用代码:

local ahk = require 'ahk'

ahk.init()

-- 运行一行 AHK 代码
ahk.exec('MsgBox, test 测试')

-- 运行多行 AHK 代码
ahk.exec([[
    a := 333
    b := "good"
]])

-- 读取 AHK 中变量
-- 变量都会以字符串的形式返回
print(ahk.getvar('a'), ahk.getvar('b'))

-- 定义一个 AHK 函数
ahk.exec([[
Fun(text) {
    MsgBox, % "test " . text
    return 123
}
]])

-- 调用 AHK 函数
print(ahk.func('Fun', '测试函数'))

-- 直接调用 dll 的函数
local L = require 'utf8fix'.L
ahk.ahk.ahkExec(L'MsgBox, 测试')

其他函数根据说明调用即可,我没有验证全部函数的功能。

我没有仔细封装,因为感觉这么用实用性不强,有如下问题:

  1. 参数只能以字符串的形式传递,还需要频繁转码,麻烦而且性能差。
  2. 不能在 AHK 里反过来调用 LuaJIT 的代码。

我这里的耗时情况:

  • LuaJIT 启动后直接退出不到 10 毫秒。
  • ahk.init() 35 毫秒左右。
  • 之后 ahk.exec() 就不需要什么时间了。
  • 一共将近 45 毫秒。

而从 AHK 里调用 LuaJIT 的 dll 要好很多:

  1. 各种类型的参数都可以直接传递。
  2. 双方可以互相调用。

我这里的耗时情况:

  • AHK 启动后里直接退出 30 毫秒出头。
  • 调用 LuaJIT 做些事情,不到 5 毫秒。
  • 一共 35 毫秒左右。
  • 我简单测试一个赋值语句,LuaJIT 调用 AHK 100000 次需要将近 4 秒,AHK 调用 LuaJIT 只需要 0.5 秒出头。

又想了想,其实也还好。启动时间多 10 毫秒,影响不大,而且可以等需要用 AHK 时再加载,这样劣势反倒变成了优势。转码的话封装一下就不会很麻烦了,不非常频繁地调用也没有什么性能问题。如果需要互相调用,那就只能换 AHK 调用 LuaJIT 了。