各位老师、前辈、同学们,大家好!
我最近在使用 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?
有疑问加站长微信联系(非本文作者)