golang,cpp 混合编程之dll
在c++、golang混合背景的工作中,经常可以碰见golang调用各种现成的c++库的情况。坑点很多,需要记录下。
明确导出符号
当我们需要引入一个dll里面的函数时,必须要知道他的导出符号。总所周知,c++以坑爹的外部符号著称。因此当我们编写dll源码时,必须要使用c风格导出。
例如一个如下函数
DWORD WebClientInit(const char* pcIP,int iPort,const char* pcName,const char* pcPassword,int iWebIndex);
必须改写为
extern "C"
{
_declspec(dllexport) DWORD WINAPI WebClientInit(const char* pcIP,int iPort,const char* pcName,const char* pcPassword,int iWebIndex);
}
此时就理论上来看,dll会导出一个名为WebClientInit的api。如果你的dll是给c系的语言使用,并配在vs的工程引用里面,那无论32位还是64位,都只需要根据这个名字导入就行。
但是假如是给golang使用,就需要特别注意32位编译的dll导出的符号其实并不是WebClientInit,而是_WebClientInit@一个数字,可以通过vs自带dumpbin来找对应的符号。
打开你的vs->工具->vs 20xx command prompt,cd到编译目录执行dumpbin /exports xxx.dll | findstr "WebClientInit",就能找到对应的api。
使用符号进行操作
基本上使用syscall包进行操作,需要注意syscall包里调用了cgo,所以需要注意使用cgo的一些事项,但是与gohook等库有差异的是不需要安装编译器,同时也可以使用交叉编译,当然系统级编程也不存在交叉编译的可能性就是了。
主要有三种方式使用dll。请注意下述的dll地址需要替换为编译出的dll的地址,"func"需要替换成导出的符号。
dll, e := syscall.LoadLibrary(`./dll.dll`)
f, e := syscall.GetProcAddress(dll, "func")
_, _, e = syscall.Syscall(f, 0, 0, 0, 0)
e = syscall.FreeLibrary(dll)
dll, e := syscall.NewLazyDLL(`./dll.dll`)
f, e := dll.NewProc("func")
_,_,e = f.Call(0,0,0,0)
dll := syscall.MustLoadDLL(`./dll.dll`)
f := dll.MustFindProc("func")
_, _, _ = f.Call(0,0,0,0)
此处有四个坑点
- %1 is not a valid Win32 application. 64位程序试图调用32位dll。请注意32位的dll需要32位的go程序。反之亦然。需要加上编译特性GOARCH=386;CGO_ENABLED=1
- 此时点击调试就会出现unsupported architecture of windows/i386 - only windows/amd64 is supported,这是因为32位go程序不支持调试。。。当然即使调试也会发现以调试模式运行的时候,会出现神奇的调用不成功的问题。
- syscall库滥用error极为常见。正常执行完函数后err会返回"The operation completed successfully."这个error需要过滤下。
- 如果dll内部启动了多线程,那么函数执行结束后线程会继续运行,直到dll变量被释放。
参数传入,返回值传出
所有dll导出函数的返回、传入值,在go中都以uintptr标识。在实践中可以将其理解为一个大到可以存放任何ABI参数的类型。
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
对于常见的int、unsigned int等参数,可以直接硬转换为uintptr。但是对于字符串我们需要格外小心。我们需要将go的字符串强行转换为c风格的字符串。这里有两个注意事项
- go风格的[]byte可以通过unsafe.Pointer(&s[0])的方式强行转为c风格的char*,但是需要注意注意前者如果是string转换而成的是不会补上\0的,需要手动添加
- 如果通过unsafe.Pointer(&s[0])强转需要注意取出的地址是不会算作一个引用的,需要注意内存管理不能让原来的字符串、[]byte被gc或者结束生命周期。例如以下的代码不仅不能正常生效甚至会crash
func randomStr() uintptr {
num := rand.int() % 2 << 20
str := strconv.Itoa(int(num))
return unsafe.Pointer(&[]bytes(str)[0])
}
我的方式是将参数封装起来
func Int2Ptr(n int) uintptr {
return uintptr(n)
}
func Bytes2Ptr(s []byte) uintptr {
if len(s) == 0 {
s = []byte{0x00}
}
if s[len(s)-1] != 0x00 {
s = append(s, 0x00)
}
return uintptr(unsafe.Pointer(&s[0]))
}
func Ptr2Bytes(p uintptr) []byte {
// 将c++的char*转换为go的[]byte,从前向后遍历,遇到0x00停止
var b []byte
for {
if *(*byte)(unsafe.Pointer(p)) == 0x00 {
break
}
b = append(b, *(*byte)(unsafe.Pointer(p)))
p++
}
return b
}
func UseFunc(helloDll *syscall.LazyDLL, funcName string, args ...uintptr) (uintptr, uintptr, error) {
newProc := helloDll.NewProc(funcName)
res1, res2, err := newProc.Call(args...)
return res1, res2, err
}
type ArgType int
const (
AT_INT ArgType = 0
AT_STRING ArgType = 1
)
type UniArgMeta struct {
ArgType ArgType
ArgName string
}
type UniArg struct {
UniArgMeta
Arg interface{}
}
type Args struct {
Args []uintptr
}
func (a *Args) Get() []uintptr {
return a.Args
}
func (a *Args) AddInt(n int) *Args {
a.Args = append(a.Args, Int2Ptr(n))
return a
}
func (a *Args) AddString(s string) *Args {
a.Args = append(a.Args, Bytes2Ptr([]byte(s)))
return a
}
func (a *Args) AddBytes(b []byte) *Args {
a.Args = append(a.Args, Bytes2Ptr(b))
return a
}
func (a *Args) AddUniArg(arg UniArg) *Args {
switch arg.ArgType {
case AT_INT:
a.AddInt(arg.Arg.(int))
case AT_STRING:
a.AddString(arg.Arg.(string))
}
return a
}
调用时则以一下方式调用
args := Args{}
order := rand.Int31n(2 << 20)
orderStr := strconv.Itoa(int(order))
args.AddInt(areaID).AddString(account).AddString(orderStr).AddString(itemTypeID).AddInt(num).AddInt(bind).AddInt(0).AddInt(1).AddString(couponTypeID)
result, _, err := UseFunc(Dll, funcName, args.Get()...)
// 这个库通过err来返回返回执行信息,因此需要做下特判
if err != nil && err.Error() == "The operation completed successfully." {
err = nil
}
当然最后将dll、func都进行封装例如
type FRet struct {
PackKey uint32
WebCMD uint32
Return uint32
}
func (receiver *ApiMgr) F(areaID int, account string, itemTypeID string, num int, bind int, couponTypeID string) fRet {
args := dll.Args{}
order := rand.Int31n(2 << 20)
orderStr := strconv.Itoa(int(order))
args.AddInt(areaID).AddString(account).AddString(orderStr).AddString(itemTypeID).AddInt(num).AddInt(bind).AddInt(0).AddInt(1).AddString(couponTypeID)
buff, size, err := receiver.API("F", args)
if err != nil {
println("F Error:", err.Error())
}
binaryReader := bytes.NewReader(buff[:size])
temp := make([]byte, 4)
var recvPackKey, recvWebCMD, recvRetun uint32
binaryReader.Read(temp)
recvPackKey = *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
recvWebCMD = *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
recvRetun = *(*uint32)(unsafe.Pointer(&temp[0]))
return FRet{PackKey: recvPackKey, WebCMD: recvWebCMD, Return: recvRetun}
}
将不变的函数调用,与变化的参数值、返回值分离,从而强化开发安全。
调试的一些注意事项
- 编dll时必须为debug,如果为release很可能出现随机参数值显示不对的现象
- 将pdb拷贝在dll同目录改成同一个名字,golang启动程序后用vs直接依附在启动的程序上
- 需要注意如果golang为debug,也会将dll的内存全部初始化,可能会漏过一些初始化相关的问题
- 有时出现release的程序调用debug dll的逻辑和debug不同的现象,由于debug时dll不能调试因此猜测可能是因为驱动程序是debug的会影响dll逻辑
传入结构体
需要注意dll一般只能导出基础类型作为接口(不然可能会出现兼容问题)。当我们需要传入class等的时候,需要以指针形式作为参数,在golang中使用bytes写内存,最后传入&buff[0]即可,需要特别注意,c++结构体中只能有基础类型且最多只有一个不定长结构体,不然会出现流发送的问题。同时需要注意golang的字符串即使转换成bytes也是不会自动补0的。以下我会介绍我的库并举几个例子
type CStruct struct {
buff []byte
}
func (s *CStruct) AddInt32(n int32) *CStruct {
n1 := byte(n & 0xFF)
n2 := byte((n >> 8) & 0xFF)
n3 := byte((n >> 16) & 0xFF)
n4 := byte((n >> 24) & 0xFF)
s.buff = append(s.buff, n1, n2, n3, n4)
return s
}
func (s *CStruct) AddUint32(n uint32) *CStruct {
n1 := byte(n & 0xFF)
n2 := byte((n >> 8) & 0xFF)
n3 := byte((n >> 16) & 0xFF)
n4 := byte((n >> 24) & 0xFF)
s.buff = append(s.buff, n1, n2, n3, n4)
return s
}
func (s *CStruct) AddString(str string, add0 bool, size int) *CStruct {
strB := []byte(str)
if add0 {
if strB[len(strB)-1] != 0x00 {
strB = append(strB, 0x00)
}
}
if size > 0 {
for i := len(strB); i < size; i++ {
strB = append(strB, 0x00)
}
}
s.buff = append(s.buff, strB...)
return s
}
func (s *CStruct) AddStringPtr(str string) *CStruct {
ptr := Bytes2Ptr([]byte(str))
s.AddInt32(int32(ptr))
return s
}
func (a *Args) AddCStruct(s *CStruct) *Args {
a.Args = append(a.Args, Bytes2Ptr(s.buff))
return a
}
例如以下结构体
struct SePtlWebCustomizeBoxItemRes
{
SePtlWebCustomizeBoxItemRes()
{
iItemID = 0;
iNum = 0;
iProb = 0;
}
int iItemID;
int iNum;
int iProb;
};
struct SePtlWebCustomizeBoxRes
{
SePtlWebCustomizeBoxRes()
{
iBoxType = 0;
iBoxID = 0;
iMaxSelectNum = 0;
memset(acStartTime,0,sizeof(acStartTime));
memset(acEndTime,0,sizeof(acEndTime));
iNum = 0;
memset(akItem,0,sizeof(akItem));
}
int iBoxType;
int iBoxID; // 包的ID
int iMaxSelectNum; // 最大可选数
char acStartTime[SWD_TIME_LEN];
char acEndTime[SWD_TIME_LEN];
int iNum; // 道具数量
SePtlWebCustomizeBoxItemRes akItem[1];
};
将上两个字符串定义为了char数组,避免使用非常复杂的std::string,仅限使用基础类型。
接口为
WEB_INTERFACE DWORD WINAPI AddWebCustomizeBoxRes(SePtlWebCustomizeBoxRes* pkBox,const char* acOrder);
当我们需要写入时
args := dll.Args{}
cStruct := dll.CStruct{}
cStruct.AddInt32(int32(int(box.BoxType)))
cStruct.AddInt32(int32(int(box.BoxID)))
cStruct.AddInt32(int32(int(box.MaxSelectNum)))
cStruct.AddString(box.StartTime, true, 24)
cStruct.AddString(box.EndTime, true, 24)
cStruct.AddInt32(int32(box.Num))
for i := 0; i < box.Num; i++ {
cStruct.AddInt32(int32(int(box.Item[i].ItemID)))
cStruct.AddInt32(int32(int(box.Item[i].Num)))
cStruct.AddInt32(int32(int(box.Item[i].Prob)))
}
args.AddCStruct(&cStruct).AddString(order)
buff, size, err := receiver.API("AddWebCustomizeBoxRes", args)
采用这种方法就行
如果我们需要从回调中读取指针对应的字符串,那么可以使用以下方法:
binaryReader := bytes.NewReader(buff[:size])
temp := make([]byte, 4)
var recvPackKey, recvWebCMD, recvRetun uint32
binaryReader.Read(temp)
recvPackKey = *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
recvWebCMD = *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
recvRetun = *(*uint32)(unsafe.Pointer(&temp[0]))
boxType := *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
boxID = *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
maxSelectNum := *(*uint32)(unsafe.Pointer(&temp[0]))
startTime := make([]byte, 24)
binaryReader.Read(startTime)
endTime := make([]byte, 24)
binaryReader.Read(endTime)
binaryReader.Read(temp)
num := *(*uint32)(unsafe.Pointer(&temp[0]))
boxItem := make([]WebCustomizeBoxItem, num)
for i := 0; i < int(num); i++ {
binaryReader.Read(temp)
boxItem[i].ItemID = *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
boxItem[i].Num = *(*uint32)(unsafe.Pointer(&temp[0]))
binaryReader.Read(temp)
boxItem[i].Prob = *(*uint32)(unsafe.Pointer(&temp[0]))
}
后续可以考虑包装成一个库,只需处理结构部分,不用反复读取