如何在 Golang 中将 *uint16 指针传递给 windows.CreateFile()

How to pass *uint16 pointer to windows.CreateFile() in Golang

我正在尝试在 Golang 1.14 中使用 windows.CreateFile() 函数(参考请参阅 https://godoc.org/golang.org/x/sys/windows#CreateFile and https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew )创建一个文件。 除了代码有效之外,我显然为 CreateFile().

file Name 属性传递了错误的参数

密码是:

package main

import (
    "unsafe"

    "golang.org/x/sys/windows"
)

func main() {
    var (
        nullHandle windows.Handle
        filename   string = "test_file"
    )

    strptr := &filename
    fileNamePtr := (*uint16)(unsafe.Pointer(strptr))
    dwShareMode := uint32(windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE)
    dwFlagsAndAttributes := uint32(windows.FILE_FLAG_DELETE_ON_CLOSE)

    windows.CreateFile(fileNamePtr, windows.GENERIC_WRITE, dwShareMode, nil, windows.CREATE_NEW, dwFlagsAndAttributes, nullHandle)
}

我正在获取一个使用非 ascii 字符创建的文件(在本例中 庡R

Directory of C:\Users\rodrigo\src\delete_on_close

04/30/2020  03:15 PM    <DIR>          .
04/30/2020  03:15 PM    <DIR>          ..
04/30/2020  03:12 PM               715 main.go
04/30/2020  03:14 PM         2,698,240 __debug_bin
04/30/2020  03:15 PM                 0 庡R
               3 File(s)      2,698,955 bytes
...

此外,这个名称在每个 运行 中都不同,所以我认为我没有正确指向我的 filename 变量。任何想法? (提前致谢)

Referring to this...

In Windows, some procedures which take string arguments have two variants: one for ANSI-encoded, and one for UTF-16 encoded strings. Regardless of which you choose, neither of these string types are directly compatible with Go strings. In order to use them, you’ll need to construct compatible strings.

您可以使用类似这样的方法将 Go 字符串转换为以 null 结尾的 UTF-16 字符串。

func StringToUTF16Ptr(str string) *uint16 {
    wchars := utf16.Encode([]rune(str + "\x00"))    
    return &wchars[0]
}

注意事项(来自 Rob Pike 的 "Go Proverbs")

With the unsafe package there are no guarantees.

问题

var filename string = "test_file"
strptr := &filename
fileNamePtr := (*uint16)(unsafe.Pointer(strptr))

在多个层面上都不正确:

  1. Go 中的字符串是一个 struct 类型的值,包含两个字段:指向字符串数据第一个字节的指针和包含字符串长度的整数(以字节为单位) )——基本上是这样定义的:

    type string struct {
        ptr *byte
        len int
    }
    

    因此获取 Go 的字符串变量的地址就是获取内存中包含指向字符串数据的指针的位置的地址(上面的 ptr 字段)。

    要获取字符串 数据的第一个字节的地址, 可以 &filename[0]。但这在你的情况下仍然是不正确的——请耐心等待。

  2. Go 字符串包含不透明字节。

    Go 中有几个地方 do 假设 Go 字符串的特定编码——即 UTF-8,这就是你在任何教程中都会读到的内容 material 在 Go 中,—但实际上它们可能包含不透明字节,使用任何编码或根本不编码。
    这意味着必须根据具体情况决定将字符串重新编码为某种目标编码的方式——同时考虑源字符串的编码。

    幸运的是,您的具体情况是最简单的。
    由于 Go 源代码文件被定义为以 UTF-8 编码,因此定义为字符串文字的 Go 字符串(并且您的 filename 变量被分配了一个由字符串文字定义的值)以 UTF-8 编码。

    UTF-8 是一种可变长度 编码,每个编码的 Unicode 代码点使用 1 到 4 个字节——具体取决于其整数值。

    您打算调用的 Win32 API 函数需要一个以 UTF-16.
    编码的字符串 UTF-16 是一种固定长度编码,它编码的每个 Unicode 代码点使用 2 个字节。

    我认为现在应该很明显,将指向 UTF-8 编码字符串的指针转换为指向 UTF-16 编码字符串的指针是行不通的 "reinterpreting" 该字符串的内容的任何内容:它们将保持以 UTF-8 编码。

解决方案

因此,您首先需要进行适当的转换:计算源字符串中包含的 Unicode 代码点 ("runes") 的数量,为新字符串分配两倍的字节数,然后遍历 运行在源字符串中逐个添加,将每个正确编码到目标字符串中(Windows 使用 UTF-16 的小端格式)。

虽然您可以按上述方式推出自己的实现,但 Go 已经以

的形式在其内置 syscall 包中提供了它
func UTF16FromString(s string) ([]uint16, error)

函数。

所以你的代码应该变成这样

u16fname, err := syscall.UTF16FromString(filename)
if err != nil {
  // fail
}

windows.CreateFile(&u16fname[0], ...)

请注意,您可能会通过阅读 go doc syscall.

的输出来了解 syscall 包中的可用内容

如果您不在目标中 OS, 运行 GOOS=windows go doc syscall.

请注意 https://golang.org/pkg/syscall 呈现 GOOS=linux 的文档,因此当您想使用 Windows-specific stdlib 代码时阅读它是无用的。


如果你很好奇,在你的例子中,当你将指针值的地址传递给 CreateFileW 时,该函数开始解释从 64- 的第一个字节开始的原始内存位指针值作为四个连续的 UTF-16 编码字符,然后它继续到包含值 0x0000000000000009 的字符串值的长度字段 — 字符串的长度 "test_file" 以字节为单位,— 所以 CreateFileW 读取第一个 0x0009,将其解释为 TAB 字符,然后在 0x0000 处停止,因为它是 UTF-16 编码的 NUL(在 "wide" Win32 API).
它也可能设法提前停止——这取决于指针的实际值:如果它的高位字有 0x0000,则该值用作 NUL 终止符。