# 二进制兼容性

本文档包含一些主题, 这些主题在处理外部库或向控件或窗口发送消息时有时很重要.

- Unicode vs ANSI
  - Buffer
  - DllCall
  - NumPut / NumGet
- 指针大小

## Unicode 与 ANSI

注意: 本节建立在文档其他部分中涉及的主题之上: 字符串, 字符编码.

在一个字符串(文本值) 中, 每个字符的数字编码和大小(字节) 取决于字符串的编码. 这些细节通常对做以下事情的脚本很重要:

- 通过 DllCall 传递字符串到外部函数.
- 通过 PostMessage 或 SendMessage 传递字符串.
- 通过 NumPut/NumGet 直接操作字符串.
- 分配 Buffer 来容纳特定数量的字符.

AutoHotkey v2 原生使用 Unicode(UTF-16), 但一些外部库或窗口信息可能需要 ANSI 字符串.

ANSI: 每个字符占用一个字节(8 位). 大于 127 的字符编码取决于系统的语言设置(或在对文本进行编码时选择的编码页, 例如, 当它被写入文件时).

Unicode: 每个字符占用两个字节(16 位). 字符编码是由 UTF-16 格式定义的.

语义注: 技术上, 一些 Unicode 字符表示为 两个 16 位代码单元, 一起被称为 "代理项对". 同样地, 一些 ANSI 代码页(通常称为双字节字符集, 例如 cp936 中的汉字) 含有一些双字节字符. 然而, 由于特殊的原因它们几乎都被视为两个单独的单元(为了简化而称为 "字符").

### Buffer

在分配 Buffer 时, 要注意为所需的任何编码计算正确的 字节 数. 例如:

    ansi_buf  := Buffer(capacity_in_chars)
    utf16_buf := Buffer(capacity_in_chars * 2)

如果使用 StrPut 将 ANSI 或 UTF-8 字符串写入缓冲, 不要使用 StrLen 来确定缓冲的大小, 因为 ANSI 或 UTF-8 的长度可能与原生(UTF-16) 长度不同. 相反, 使用 StrPut 来计算所需的缓冲大小. 例如:

    required_bytes := StrPut(source_string, "cp0")
    ansi_buf := Buffer(required_bytes)
    StrPut(source_string, ansi_buf)

### DllCall

使用 "Str" 类型时, 表示字符串使用当前版本原生的编码格式. 由于一些函数可能需要或返回特殊格式的字符串, 所以有时还需要使用下列的字符串格式:

| 字符大小 | C / Win32 类型 | 编码
--- | --- | --- | ---
WStr | 16-位 | wchar_t*, WCHAR*, LPWSTR, LPCWSTR | UTF-16
AStr | 8-位 | char*, CHAR*, LPSTR, LPCSTR | ANSI(系统默认 ANSI 代码页)
Str | -- | TCHAR*, LPTSTR, LPCTSTR | 等同于 AutoHotkey v2 中的 WStr.

如果 "Str" 或 "WStr" 被用于一个参数, 字符串的地址被传递给函数. 对于 "AStr", 会创建一个字符串的临时 ANSI 拷贝, 并传递其地址. 一般来说, "AStr" 不应该用于输出参数, 因为缓冲区的大小只够容纳输入的字符串.

注意: "AStr" 和 "WStr" 对于参数和函数的返回值同样是有效的.

一般来说, 如果脚本通过 DllCall 调用一个接受字符串参数的函数, 必须采取以下一种或多种方法:

1. 如果函数的 Unicode(W) 和 ANSI(A) 版本都可用, 省略 W 或 A 的后缀, 对输入参数或返回值使用 "Str" 类型. 例如, DeleteFile 函数从 kernel32.dll 导出为 `DeleteFileA` 和 `DeleteFileW`. 由于 `DeleteFile` 本身并不真正存在, DllCall 自动尝试 `DeleteFileW`:

       DllCall("DeleteFile", "Ptr", StrPtr(filename))
       DllCall("DeleteFile", "Str", filename)

   在这两种情况下, 原始的未修改的字符串的地址被传递给函数.

   在某些情况下, 这种方法可能会适得其反, 因为 DllCall 只有在找不到原名称的函数时才会添加 W 后缀. 例如, shell32.dll 导出的 ExtractIconExW, ExtractIconExA 和 ExtractIconEx 都没有后缀, 最后两个是等价的. 在这种情况下, 省略 W 的后缀会导致 ANSI 版本被调用.

2. 如果函数仅接受特定类型的字符串作为输入, 那么脚本可能需要使用相应的字符串类型:

       DllCall("DeleteFileA", "AStr", filename)
       DllCall("DeleteFileW", "WStr", filename)

3. 如果函数有一个用于输出的字符串参数, 脚本必须如上 所述分配一个缓冲并将其传递给函数. 如果该参数接受输入, 脚本还必须将输入的字符串转换为适当的格式; 为此可以使用 StrPut.

### NumPut / NumGet

当使用 NumPut 或 NumGet 操作字符串时, 对于给定类型的字符串其偏移和类型都必须正确. 可以参考下面的代码:

    ;  8 位/ANSI   字符串:  size_of_char=1  type_of_char="UChar"
    ; 16 位/UTF-16 字符串:  size_of_char=2  type_of_char="UShort"
    nth_char := NumGet(buffer_or_address, (n-1)*size_of_char, type_of_char)
    NumPut(type_of_char, nth_char, buffer_or_address, (n-1)*size_of_char)

对于第一个字符, n 的值应为 1.

## 指针大小

指针在 32 位版本中是 4 个字节大小, 而在 64 位版本中是 8 个字节. 使用结构或 DllCall 的脚本可能需要为在两种平台上正常运行进行考虑. 受影响的特殊地方包括:

- 含有一个或多个指针的结构字段的偏移计算.
- 含有一个或多个指针的结构大小计算.
- 在 DllCall, NumPut 或 NumGet 中使用的类型名称.

对于大小和偏移计算, 使用 A_PtrSize. 对于 DllCall, NumPut 和 NumGet, 使用适当的 Ptr 类型.

记住一个字段的偏移常常是在它之前所有字段的总大小. 同时注意句柄(包括类似 HWND 和 HBITMAP 的类型) 实际上是指针类型.

    /*
      typedef struct _PROCESS_INFORMATION {
        HANDLE hProcess;    // Ptr
        HANDLE hThread;
        DWORD  dwProcessId; // UInt(4 字节)
        DWORD  dwThreadId;
      } PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
    */
    pi := Buffer(A_PtrSize*2 + 8) ; Ptr + Ptr + UInt + UInt
    DllCall("CreateProcess", <为简短而省略>, "Ptr", &pi, <省略>)
    hProcess    := NumGet(pi, 0)         ; 默认为 "Ptr".
    hThread     := NumGet(pi, A_PtrSize) ;
    dwProcessId := NumGet(pi, A_PtrSize*2,     "UInt")
    dwProcessId := NumGet(pi, A_PtrSize*2 + 4, "UInt")

dftg