calling c++ from golang with swig--windows dll (三)
calling c++ from golang with swig--windows dll 三
使用动态链接库(DLL)主要有两种方式:一种通过链接导入库,在代码中直接调用DLL中的函数;另一种借助LoadLibrary/LoadLibraryEx,GetProcessAddress函数在代码中间接调用DLL中的函数。这两种使用方式对应两种动态链接,分别称为: load-time dynamic link (加载时动态链接)和run-time dynamic link (运行时动态链接)。
DLL工程编译后产生*.dll文件和同名的.lib文件, .lib文件是导入库,使用加载时链接的应用工程可以在代码中显式调用DLL中的函数,而且必须要用导入库*.lib链接。当系统启动一个使用加载时动态链接的程序时,它将利用链接器放置在文件中的信息来定位进程使用的DLL(可执行程序导入的)中的名称(DLL导出函数)。然后系统搜索DLL文件。如果系统无法找到所需的DLL,则会终止该进程,并向用户显示一个报告错误的对话框。否则,系统将DLL映射到进程的虚拟地址空间,并增加DLL引用计数。系统调用入口点函数(end-point function)。如果入口点函数没有返回TRUE,系统将终止进程并报告错误。最后,系统使用导入的DLL函数起始地址来修改函数地址表。(参阅msdn:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms684184(v=vs.85).aspx)
使用运行时动态链接,当应用程序调用LoadLibrary或LoadLibraryEx函数时,系统将尝试找到DLL。如果搜索成功,系统将DLL模块映射到进程的虚拟地址空间,并将引用计数递增。然后系统在调用LoadLibrary或LoadLibraryEx的线程上下文中调用入口点函数。成功后LoadLibray或LoadLibraryEx返回一个指向DLL的句柄。进程可以在GetProcAddress、FreeLibrary函数调用中使用这个句柄来标识DLL。进程调用GetProcessAddress获取一个DLL导出函数的地址。当DLL模块不再需要时,进程可以调用FreeLibrary或FreeLibraryExAndExitThread。这些函数将减少模块引用计数并从进程的虚拟地址空间中取消映射的DLL代码。(参阅msdn:https://msdn.microsoft.com/en-us/library/windows/desktop/ms682596(v=vs.85).aspx)
可以用上述两种动态链接在Golang中使用C++ DLL。Go标准库”syscall”提供了syscall.LoadLibrary,syscall.GetProcAddress,syscall.Syscall系列函数,可以轻松实现运行时动态链接,可参阅godoc查看syscall包的用法。
当DLL导出了C++类或者导出了大量函数,使用运行时动态链接很难实现或很繁琐,这种情况下应该使用加载时动态链接。下面主要讲如何用swig实现golang调用C++ DLL。(环境 windows 7 64位操作系统,安装64位 tdm-gcc,代码编译采用x64)
用VS 2010向导创建一个简单的动态链接库Simple.dll
主要代码如下:
Simple.h文件
#ifdef SIMPLE_EXPORTS
#define SIMPLE_API __declspec(dllexport)
#else
#define SIMPLE_API __declspec(dllimport)
#endif
// This class is exported from the Simple.dll
class SIMPLE_API CSimple {
public:
CSimple(void);
// TODO: add your methods here.
void SayHello();
};
extern SIMPLE_API int nSimple;
SIMPLE_API int fnSimple(void);
Simple.cpp文件:
// Simple.cpp : Defines the exported functions for the DLL application.
//
#include "stdafx.h"
#include "Simple.h"
#include <iostream>
using namespace std;
// This is an example of an exported variable
/*SIMPLE_API*/ int nSimple=0;
// This is an example of an exported function.
/*SIMPLE_API*/ int fnSimple(void)
{
return 42;
}
// This is the constructor of a class that has been exported.
// see Simple.h for the class definition
CSimple::CSimple()
{
return;
}
void CSimple::SayHello()
{
cout<<"Hello World"<<endl;
}
这个简单的DLL导出了一个变量,一个函数和一个类,类中包含一个构造函数和一个SayHello方法。用Dependency Walker查看Simple.dll文件:
Simple.dll导出了5个函数(变量),前面介绍过C++ Name mangling,可以用undname.exe查看undecorated name,多出来的那个函数是CSimple类的拷贝赋值操作符=。
接下来创建一个简单的go程序来调用Simple.dll,工程目录是D:\GoSimple,在该目录下创建simple文件夹,将Simple.dll和Simple.h文件拷贝到D:\GoSimple\simple中,再创建swig程序的输入文件simple.i
在命令行中切换到 D:\GoSimple\simple,执行 swig -c++ -go -cgo -intgosize 64 simple.i
成功后生成两个文件 simple_wrap.cxx 和 simple.go
打开simple.go,增加链接项
#cgo CFLAGS: -I .
#cgo LDFLAGS:-L . -lSimple
在D:\GoSimple中创建一个简单的main.go文件调用DLL
go build编译,可以看到出现一堆错误:
全部是链接错误,找不到定义。最开始实现go调用c++ dll时,费了较多的时间来解决这个编译错误,后来才明白go使用了gcc编译器,gcc的name mangling与微软visual c++编译器的name mangling不同,需要将DLL文件中增加一份按照g++ name mangling的函数名称。
根据DLL导出函数、dependency walker、go build的错误提示,前面讲过的C++ name mangling相关的内容,可以整理出如下关系:
函数名 |
Visual C++ decorated name |
g++ mangled name |
nSimple |
?nSimple@@3HA |
nSimple |
fnSimple |
?fnSimple@@YAHXZ |
_Z8fnSimplev |
CSimple::SayHello |
?SayHello@CSimple@@QEAAXXZ |
_ZN7CSimple8SayHelloEv |
CSimple::CSimple |
??0CSimple@@QEAA@XZ |
_ZN7CSimpleC1Ev |
|
|
|
我们需要利用.def文件为DLL文件的导出项部分增加一份g++使用的符号。为Simple DLL工程增加Simple.def,
重新编译后,再次用dependency walker来查看Simple.dll
增加def文件的作用是为函数入口点增加一个别名,例如?fnSimple@@YAHXZ 和_Z8fnSimplev对应的入口点同为0x0000102D,?fnSimple@@YAHXZ是visual c++ mangled name,_Z8fnSimplev是gcc mangled name。golang使用gcc编译c++代码,为DLL增加导出项后,就可以解决函数未定义的链接错误了。
更新D:\GoSimple\simple目录的Simple.dll,同时也在D:\GoSimple中拷贝一份Simple.dll
重新go build,编译成功。
运行
注意:1.go build前,需要在D:\GoSimple中也拷贝一份Simple.dll,否则会出现错误:
2.不能将Simple.lib拷贝到go程序目录中,否则会出现错误: