[go/golang]go语言中调用 dll 时传递字符串的大坑及解决方法
go1.7.3中的dll字符串参数与垃圾回收机制
这次使用 golang 调用标准 windows stdcall 的 dll 函数时发现,golang 中的垃圾回收机制是非常晦涩的,一定要注意.
在非常密集的大压力调用下,传递给 dll 的字符串参数使用不当的话会在传递出去前被垃圾回收导致程序访问了不存在的内存而崩溃!
1.和 C++ 的类析构造函数不同,函数体内定义的变量也有可能在还没出函数体时就被回收了!
解决的办法是:在函数体最后再使用一下这个变量,让其保持引用.但这样的代码不小心的话很有可能会被编译器优化掉.
2.和 C/C++ 不同, golang 中的指针也能让其所在的内存块一直保持引用. 但调用 unsafe.Pointer() 得到的指针则不会保持这种引用.
3.再强调一次! unsafe.Pointer 操作后的指针尽量在最后需要时再转换出来,特别不要作为函数的参数来使用.那样会引起编码的整体流畅度,思想会在到底垃圾回收,引用
等有没有效上纽结,干脆就不要用它传递参数,就当所有变量都是可以强引用的好了.
4.以上都是猜测和测试后的结论,最安全保守的做法是: 1 - 分配内存块,并在函数体最后 fmt.println 一下某个字节; 2 - 引出非 unsafe.Pointer() 的指针给子函数调用; 3 - unsafe.Pointer() 的指针不作为参数,在最后要使用时再临时转换出来.
这种思想应该可以很好的对付所有的垃圾回收机制,理解上也更容易.
2020 补充
5.这些在 dll 中调用的注意点在 go 源码中的 unsafe.go 中有非常详尽的说明。
而我们后面提到的那个示例使用了最安全的保守做法性能又非常好,我觉得尽量用这种方式,毕竟 golang 自己调用 windows api dll 的那种方式太隐晦,太容易误导大家写出错误的代码了。
它的处理方法也比较简单,就是用 cgo 来分配内存,然后又用 cgo 自己释放。这样就完全是使用的 c 语言的内存处理方式。就解决了这个很容易混淆的东西。
//参考 https://github.com/mattn/go-oci8/blob/master/oci8.go
//unsafe.Pointer 指针保存
//字符串内容保存//最好的当然还是这种在调用 dll 前明确先分配好内存
// connectString := cString(dsn.Connect)
// defer C.free(unsafe.Pointer(connectString))
// username := cString(dsn.Username)
// defer C.free(unsafe.Pointer(username))
// password := cString(dsn.Password)
// defer C.free(unsafe.Pointer(password))
--------------------------------------------------
测试时的代码如下
package main;
import (
"fmt"
// "fmt"
//"io/ioutil"
// "database/sql"
//"fmt"
// "reflect"
// "strconv"
//"strings"
"syscall"
"unsafe"
"net/http"
// "runtime"
//"runtime/internal/atomic" //这个是内部包,不能这样引用
//从 Go\src\syscall\os_windows.go 来看,一个 stdcall 是开了一个线程来调用一个 dll 函数的
//不对,好象是 tstart_stdcall, newosproc 才开
"runtime/debug"
"runtime"
)
//据说是高手的代码
//https://github.com/liudch/goci/blob/master/oci.go
//其实只要导出一个函数就可以了
var (
http_func * syscall.LazyProc = nil; //syscall.NewLazyDLL("C:\\Program Files\\Oracle\\instantclient_11_2\\oci.dll")
http_func_sql * syscall.LazyProc = nil;
)
func LoadHttpDlls(){
//mod_http := syscall.NewLazyDLL("http.dll");
mod_http := syscall.NewLazyDLL("http_dll.dll")
http_func = mod_http.NewProc("http_func"); // Clear all attribute-value information in a namespace of an application context
////http_func_sql = mod_http.NewProc("http_func_sql");
http_func_sql = mod_http.NewProc("http_func_sql_err1");
}//
func ExecuteHttp(w http.ResponseWriter, r *http.Request) (string, error) {
defer PrintError("ExecuteHttp");
//这个在高速时仍然有问题(异常退出),后期再用 golang 自己的锁定试试
//Ajax跨域问题的两种解决方法之一,据说 html5 后的才支持
w.Header().Set("Access-Control-Allow-Origin", "*");
//if (r.URL.Path == "/s/new.php")||(r.URL.Path == "/s/old.php")
if (r.URL.Path == "/sql/get")||(r.URL.Path == "/sql/get.php"){
sql := r.FormValue("sql");
sql = Trim(" " + sql + " "); //test 测试变量生存期
sql = " " + sql + " "; //test 测试变量生存期
//------------------
//调试 delphi 的 dll 时意外发现 syscall.StringToUTF16Ptr 已经是弃用的了 go 1.7.3 的源码注释中说了用 UTF16PtrFromString 来代替
sqlp, err := syscall.UTF16PtrFromString(sql);
if err !=nil { panic("dll 调用参数严重错误!!!");}
a, err2 := syscall.UTF16FromString(sql); //来自 UTF16PtrFromString 的源码
if err2 !=nil { panic("dll 调用参数严重错误!!! err2");}
sqlp = &a[0];
//------------------
//dll_errtest1(sql);
dll_errtest2(sql);
return "", nil;
//------------------
p1 := uintptr(unsafe.Pointer(sqlp));
runtime.GC(); //不调用这两个应该也是很容易重现的//还是加上容易重现,并且是一大堆一起失效,而没的的话则是久不久失效一个指针
debug.FreeOSMemory();
r0, _, e1 := http_func_sql.Call(
//utf82utf16(r.URL.Path),//,
p1, //uintptr(unsafe.Pointer(sqlp)), // utf82utf16(sql),//, !!! 这个会出错,内存混乱
//uintptr(unsafe.Pointer(http_func)), // OCIEnv **envhpp,
//uintptr(0), // ub4 mode,
//0, // CONST dvoid *ctxp,
);
//if r0 != 0 {
// return "", error(e1)
//}//if
if e1 != nil {
// return "", error(e1)
}//if
////s := prttostr(r0, 4*1024*1024); //这个确实是有问题的
fmt.Println(r0);
//fmt.Println(a); //奇怪这里不调用的话, a 的内存在多线程中会发生变化,估计是调用时间拖动得长的话被垃圾回收了,所以一定要在 dll 调用后再使用一下其中的变量,
//特别是字符串变量一定要注意,不能是只使用它的指针,要整个字节缓冲区一起//过会我写个可以一定重现的 dll 函数调用
//参考 zsyscall_windows.go 的用法,也是一直引用一个内存区的
//return s, nil;
return "", nil;
}//if
//--------------------------------------------------
r0, _, e1 := http_func.Call(
utf82utf16(r.URL.Path),//,
//uintptr(unsafe.Pointer(http_func)), // OCIEnv **envhpp,
//uintptr(0), // ub4 mode,
//0, // CONST dvoid *ctxp,
);
if r0 != 0 {
return "", error(e1)
}//if
return "", nil
}//
//重要!!! 调用 dll 传递字符串参数因 gc 导致数据失效的示例(实际上会因此内存访问错误崩溃)
func dll_errtest1(sql string) (string, error) {
defer PrintError("dll_errtest1");
//------------------
//调试 delphi 的 dll 时意外发现 syscall.StringToUTF16Ptr 已经是弃用的了 go 1.7.3 的源码注释中说了用 UTF16PtrFromString 来代替
sqlp, err := syscall.UTF16PtrFromString(sql);
if err !=nil { panic("dll 调用参数严重错误!!!");}
a, err2 := syscall.UTF16FromString(sql); //来自 UTF16PtrFromString 的源码
if err2 !=nil { panic("dll 调用参数严重错误!!! err2");}
sqlp = &a[0];
//------------------
p1 := uintptr(unsafe.Pointer(sqlp));
runtime.GC(); //不调用这两个应该也是很容易重现的//还是加上容易重现,并且是一大堆一起失效,而没的的话则是久不久失效一个指针
debug.FreeOSMemory();
//放到一个单独的函数时,这里调用 gc 后面会立即崩溃! 不 gc 的时候还能运行一下
r0, _, e1 := http_func_sql.Call(
//utf82utf16(r.URL.Path),//,
p1, //uintptr(unsafe.Pointer(sqlp)), // utf82utf16(sql),//, !!! 这个会出错,内存混乱
//uintptr(unsafe.Pointer(http_func)), // OCIEnv **envhpp,
//uintptr(0), // ub4 mode,
//0, // CONST dvoid *ctxp,
);
//if r0 != 0 {
// return "", error(e1)
//}//if
if e1 != nil {
// return "", error(e1)
}//if
////s := prttostr(r0, 4*1024*1024); //这个确实是有问题的
fmt.Println(r0);
//fmt.Println(p1); //强制再引用一会,不让垃圾回收//这个变量没用
//fmt.Println(a); //强制再引用一会,不让垃圾回收//这个变量有用
////fmt.Println(sqlp); //强制再引用一会,不让垃圾回收//这个变量有用,这个 *uint16 变量居然也可以让其所在的 []uint16 数组保持引用!!! 也就是说即使是引用了指针,其数组也是会保持的,那么会解引用的原因应该只是那个 unsafe.Pointer 后的变量
//也就是说 unsafe.Pointer 后的变量就随时可能被释放掉!
//fmt.Println(a); //奇怪这里不调用的话, a 的内存在多线程中会发生变化,估计是调用时间拖动得长的话被垃圾回收了,所以一定要在 dll 调用后再使用一下其中的变量,
//特别是字符串变量一定要注意,不能是只使用它的指针,要整个字节缓冲区一起//过会我写个可以一定重现的 dll 函数调用
//参考 zsyscall_windows.go 的用法,也是一直引用一个内存区的
//return s, nil;
return "", nil;
}//
//同 dll_errtest1 ,只是为了单独测试 UTF16PtrFromString 后的指针是否可以保持引用
func dll_errtest2(sql string) (string, error) {
defer PrintError("dll_errtest2");
//------------------
//调试 delphi 的 dll 时意外发现 syscall.StringToUTF16Ptr 已经是弃用的了 go 1.7.3 的源码注释中说了用 UTF16PtrFromString 来代替
//从 go 1.7.3 的源码来看,这是因为 StringToUTF16Ptr 会在调用 StringToUTF16 时,无法转换时
//调用 panic 引发程序立即退出
//ps.这种从 go 源码中直接退出的机制也太可怕了,一旦发生甚至不知道是怎样退出的,还以为是自己代码的问题...
sqlp, err := syscall.UTF16PtrFromString(sql);
if err !=nil { panic("dll 调用参数严重错误!!!");}
//a, err2 := syscall.UTF16FromString(sql); //来自 UTF16PtrFromString 的源码
//if err2 !=nil { panic("dll 调用参数严重错误!!! err2");}
//sqlp = &a[0];
//------------------
p1 := uintptr(unsafe.Pointer(sqlp));
runtime.GC(); //不调用这两个应该也是很容易重现的//还是加上容易重现,并且是一大堆一起失效,而没的的话则是久不久失效一个指针
debug.FreeOSMemory();
//放到一个单独的函数时,这里调用 gc 后面会立即崩溃! 不 gc 的时候还能运行一下
r0, _, e1 := http_func_sql.Call(
//utf82utf16(r.URL.Path),//,
p1, //uintptr(unsafe.Pointer(sqlp)), // utf82utf16(sql),//, !!! 这个会出错,内存混乱
);
if e1 != nil {
// return "", error(e1)
}//if
fmt.Println(r0);
fmt.Println(sqlp); //参考 dll_errtest1 处的说明//经测试,至少在 go 1.7.3 UTF16PtrFromString 后的指针变量是可以保持其所在原内存块的引用的,会让其内存块不被垃圾回收
//这也说明 golang 这样的垃圾回收语言,它的回收时机有时候还是很晦涩的
//golang 自己调用 dll 使用字符串时的处理可以参考 zsyscall_windows.go 见下一个示例
return "", nil;
}//
-----------------------------------------
对应 delphi 函数源码为
var
id:Integer = 0;
function http_func_sql_err1(sql:PWideChar):PAnsiChar;stdcall;
var
i,j:Integer;
_sql:string;
tmp:string;
begin
//indll
tmp := sql;
Writeln(tmp);
CoInitialize(nil);
try
//ShowMessage(sql); //线程里面这个不安全的
if gLock = nil then gLock := TThreadLock.Create(Application);
try
gLock.Lock();
id := id + 1;
while True do
begin
//Writeln(id, ' - ', 'test:' + tmp);
//--------------------------------------------------
_sql := sql;
//_sql := tmp;//sql; //调试发现线程并没有重入,但传入的 sql 已经被改变了,所以要尽快接收参数
Writeln(id, ' - ', '_sql:' + _sql);
Writeln(id, ' - ', 'tmp:' + tmp); //可以看到,出错的时候,这两者确实不一样了! golang 调用的参数居然发生了变化! 这是为什么,实际上还是在一个调用中啊
if _sql <> tmp then Break;
//--------------------------------------------------
Sleep(1000);
end;
finally
gLock.UnLock(); //一定要保证解锁
end; // try
Writeln('end.');
except on E: Exception do
//MessageBox(0, PChar( e.message), '', 0);
Writeln('Http 调用组件时,错误异常! ' + e.message);
end;
CoUninitialize();
end;
golang 中自己调用 windows dll 时使用的方法则是非常隐晦和利用了非 unsafe 指针在传参时可以一直保持引用,直到函数返回的特性(也有可能是 golang 方便这样调用故意如此实现的).按这种方法得出的代码如下
//测试不保留引用的情况下,看看只传递非 unsafe.Pointer() 的指针,是否能保持内存的存在//感觉 go1.7.3 就是这样传递字符串参数的
//如果成立说明对非 unsafe.Pointer 指针的引用可以保持到传递出去的函数调用返回时
func dll_errtest3(sql string) (string, error) {
defer PrintError("dll_errtest3");
sqlp, err := syscall.UTF16PtrFromString(sql);
if err !=nil { panic("dll 调用参数严重错误!!!");}
//------------------
//p1 := uintptr(unsafe.Pointer(sqlp));
runtime.GC(); //不调用这两个应该也是很容易重现的//还是加上容易重现,并且是一大堆一起失效,而没的的话则是久不久失效一个指针
debug.FreeOSMemory();
//放到一个单独的函数时,这里调用 gc 后面会立即崩溃! 不 gc 的时候还能运行一下
dll_errtest3_sub(sqlp);
//fmt.Println(sqlp); //重要! 可以看到,将非 unsafe.Pointer 指针传递出去后就可以不用再人为的保持内存块引用了
return "", nil;
}//
func dll_errtest3_sub(sqlp *uint16) {
p1 := uintptr(unsafe.Pointer(sqlp));
r0, _, e1 := http_func_sql.Call(
//utf82utf16(r.URL.Path),//,
p1, //uintptr(unsafe.Pointer(sqlp)), // utf82utf16(sql),//, !!! 这个会出错,内存混乱
);
if e1 != nil {
// return "", error(e1)
}//if
fmt.Println(r0);
}//
这是个挺严重的问题,我看了下网上的一些代码都是有这个问题的. 这些原因显然都是没搞清楚 go 本身调用 dll 的代码为什么不用保留引用造成的.
例如
//据说是高手的代码
//https://github.com/liudch/goci/blob/master/oci.go
//2019 更新,这份代码的字符串传递也是有问题的,例如
/*
func OCILogon(env *OCIHandle, username, password, database string) (svcctx *OCIHandle, err error) {
var s *OCIHandle = new(OCIHandle) // Service context
s.t = OCI_HTYPE_SVCCTX
var e *OCIHandle = new(OCIHandle) // Error handle
e.t = OCI_HTYPE_ERROR
r0, _, e1 := procOCILogon.Call( // sword OCILogon (
env.h, // OCIEnv *envhp,
uintptr(unsafe.Pointer(&e.h)), // OCIError *errhp,
uintptr(unsafe.Pointer(&s.h)), // OCISvcCtx **svchp,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(username))), // CONST OraText *username,
uintptr(len(username)), // ub4 uname_len,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(password))), // CONST OraText *password,
uintptr(len(password)), // ub4 passwd_len,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(database))), // CONST OraText *dbname,
uintptr(len(database))) // ub4 dbname_len );
if r0 != 0 {
err = error(e1)
return
}
return s, nil
}
这其中的用户名就是有可能在大压力环境下失效
*/
这一份 oci 的代码,对字符串就做有一些手工释放的工作.
https://github.com/mattn/go-oci8/blob/master/oci8.go