【疑难杂症】【GC】Go 程序调用 Windows DLL 的正确姿势是什么?

goroution · 2017-02-22 06:01:02 · 4637 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2017-02-22 06:01:02 的主题,其中的信息可能已经有所发展或是发生改变。

各位老师、前辈、同学们,大家好!

我最近在使用 Go 语言调用 Windows dll 时遇到了一个问题,这个问题具体表现为 Go 语言中的字符串传递给 dll 之后,如果 dll 里的函数执行较为缓慢的话,则 Go 语言字符串里的内容可能会被 gc 回收掉,从而导致 dll 里的函数读取到的内容是错误的。

下面是测试代码(go 语言部分):

package main

import (
    "fmt"
    "os"
    "syscall"
    "time"
    "unsafe"
)

var (
    fnFindBUG *syscall.Proc
)

func main() {
    dll := syscall.MustLoadDLL("foo.dll")
    fnFindBUG = dll.MustFindProc("foo")

    for {
        go Worker()
        time.Sleep(time.Microsecond * 1)
    }
}

func Worker() {
    str := "A"
    bin := make([]byte, len(str)+1)
    copy(bin, str)

    ret, _, _ := fnFindBUG.Call(1000, uintptr(unsafe.Pointer(&bin[0])), uintptr(str[0]))
    if ret != 0 {
        fmt.Printf("FindBUG ret: %d\n", int(ret))
        os.Exit(1)
    }
}

下面是 dll 的源代码:

#include <stdio.h>
#include "windows.h"

int foo(int ms, char *ptr, char ok)
{
    if (ptr == NULL) {
        fprintf( stderr, "ptr is null\n" );
        return -1;
    }

    if (ms > 0) {
        Sleep(ms);
    }

    if (*ptr == ok) {
        return 0;
    }
    else {
        fprintf( stderr, "Found BUG: 0xx != 0xx\n", *ptr, ok );
    }

    return -1;
}

我不知道这里如何上传文件,不然的话我这里有一个编译好的 dll 提供给没有环境的同学下载。

好,我们继续。

如同代码所示,dll 函数 foo 是一个测试函数, 它接受三个参数:一个可选的延迟,一个待测试的内存地址,以及一个比较基准值。

我在 Go 语言里通过 Go 标准库 syscall 来调用这个 dll 函数, 通过不停地创建 go 程来触发 gc(哪位朋友有更好的触发 gc 的方法也可以告诉我,我试过 runtime.GC() 并不一定会触发这个 BUG)。

当 gc 来临时,bin 变量的内容可能会被释放掉,这样 foo 就会检测到一个 BUG。

在我的机器上这个 BUG 很容易就可以检测出来:

$ go run bug.go
FindBUG ret: -1
Found BUG: 0x10 != 0x41
exit status 1

从打印输出上可以看到,我传入的值是 'A'(ASCII 码值是 0x41),但是 dll 接收到的是 0x10 ———— 每次测试的时候这个值都会变化,没有什么明显的规律。

我之前在线下也做了一些努力,定位到这个问题可能是由于 unsafe.Pointer 或者 uintptr 的不正确使用造成的,于是我对这个测试程序做了一些改进,得到了一些新的进展。

在下面这段代码里,我用 Go 语言来实现了 FindBUG,模拟了 dll 的行为,也复现了 BUG。

package main

import (
    "fmt"
    "os"
    "time"
    "unsafe"
)

func main() {
    for {
        go Worker()
        time.Sleep(time.Microsecond * 1)
    }
}

func Worker() {
    str := "x"
    bin := make([]byte, len(str)+1)
    copy(bin, str)

    ret := FindBUG(1000, uintptr(unsafe.Pointer(&bin[0])), str[0])
    if ret != 0 {
        fmt.Printf("FindBUG ret: %d\n", int(ret))
        os.Exit(1)
    }
}

// go:uintptrescapes
//
// FindBUG 用来测试代码是否存在 BUG
// 返回 0 表示没有发现 BUG,返回 -1 表示存在 BUG。
// 你可以把上面的注释中 "go" 前面的空格删除,这样就可以打开函数编译指示,就没 BUG 了。
// 加上空格可以关闭这个编译指示(使其变为不带特殊含义的普通注释),这样就有 BUG。
//
// 更多内容请参见:
// * 关于 uintptr: http://golang.org/pkg/unsafe/#Pointer
// * 关于 go:uintptrescapes: https://golang.org/src/cmd/compile/internal/gc/lex.go
func FindBUG(ms int, fooPtr uintptr, ok byte) int {
    if ms > 0 {
        time.Sleep(time.Microsecond * (time.Duration)(ms))
    }

    a := *(*byte)(unsafe.Pointer(fooPtr))

    if a != ok {
        fmt.Printf("Found BUG: %v != %v\n", a, ok)
        return -1
    }

    return 0
}

于是,我凭借 unsafe 文档 所描述的文字,判断问题是出在 uintptr 上,于是好奇 syscall.Proc.Call 是如何做的?最后在 Go 源代码里找到了如下内容:

// Source file src/cmd/compile/internal/gc/lex.go
func pragmaValue(verb string) Pragma {
    switch verb {
    // 省略无关内容 ...
    case "go:uintptrescapes":
        // For the next function declared in the file
        // any uintptr arguments may be pointer values
        // converted to uintptr. This directive
        // ensures that the referenced allocated
        // object, if any, is retained and not moved
        // until the call completes, even though from
        // the types alone it would appear that the
        // object is no longer needed during the
        // call. The conversion to uintptr must appear
        // in the argument list.
        // Used in syscall/dll_windows.go.
        return UintptrEscapes

然后我给代码里加了这个编译指示,问题果然就解决了。

那么,我的问题来了:

  • 既然 syscall.Proc.Call 里也有 go:uintptrescapes,为什么仍然会有问题?
  • Go 语言下调用 Windows dll 的正确姿势是什么?如何安全地传递字符串给 dll?

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

4637 次点击  
加入收藏 微博
4 回复  |  直到 2017-02-26 08:39:46
goroution
goroution · #1 · 8年之前

以上描述及代码都基于 Go 1.7,开发环境分别为 MacBook 和 Windows Server

mortemnh
mortemnh · #2 · 8年之前

func StrPtr(s string) uintptr { ptr, err := syscall.BytePtrFromString(s) if err != nil { fmt.Println(err) } return uintptr(unsafe.Pointer(ptr)) } 你的这个问题是因为Go中字符串的类型结构不是C中的纯[]byte,调用系统专用转换函数syscall.BytePtrFromString 就可以了。这个函数会把string类型先转换成连续的[]byte并返回首指针

mortemnh
mortemnh · #3 · 8年之前
mortemnhmortemnh #2 回复

func StrPtr(s string) uintptr { ptr, err := syscall.BytePtrFromString(s) if err != nil { fmt.Println(err) } return uintptr(unsafe.Pointer(ptr)) } 你的这个问题是因为Go中字符串的类型结构不是C中的纯[]byte,调用系统专用转换函数syscall.BytePtrFromString 就可以了。这个函数会把string类型先转换成连续的[]byte并返回首指针

搞错了,你这个应该是 Goroutine的问题.....

huangxianghan
huangxianghan · #4 · 8年之前

win64问,无法重现你这个bug,无论我是删掉go:uintptrescapes 这行注释,还是加上这行。我是直接编译成.exe文件再执行的,没用 go run 这种方式。

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传