文章

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]))
	}

后续可以考虑包装成一个库,只需处理结构部分,不用反复读取

License:  CC BY 4.0