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

goroution · · 3878 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

各位老师、前辈、同学们,大家好! 我最近在使用 Go 语言调用 Windows dll 时遇到了一个问题,这个问题具体表现为 Go 语言中的字符串传递给 dll 之后,如果 dll 里的函数执行较为缓慢的话,则 Go 语言字符串里的内容可能会被 gc 回收掉,从而导致 dll 里的函数读取到的内容是错误的。 下面是测试代码(go 语言部分): ```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 的源代码: ```cpp #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 很容易就可以检测出来: ```shell $ 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。 ```go 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 文档](http://golang.org/pkg/unsafe/#Pointer) 所描述的文字,判断问题是出在 uintptr 上,于是好奇 [syscall.Proc.Call 是如何做的](https://golang.org/src/syscall/dll_windows.go)?最后在 [Go 源代码](https://golang.org/src/cmd/compile/internal/gc/lex.go)里找到了如下内容: ```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

3878 次点击  
加入收藏 微博
4 回复  |  直到 2017-02-26 08:39:46
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传