了解知识

本文讨论如何在Windows中使用SWIG来让Go语言调用C/C++代码。笔者的实验环境是32位Windows 7系统、Go 1.1版本、MSYS、swigwin 2.0.9版、pcre 8.3.2版、Visual Studio 2008带的C/C++编译器。
1 开发环境
1.1 安装Go语言
到http://code.google.com/p/go/downloads/list 下载go1.1.windows-386.zip文件,解压缩到D:\目录。设置环境变量GOROOT为D:\Go、GOOS为windows、GOARCH为386,并且将D:\Go目录加入到环境变量PATH中。这样就设置好了Go语言开发环境。
1.2 安装MSYS
到http://code.google.com/p/msys-cn/downloads/list 下MSYS-Update.7z文件,解压缩到D:\目录,将D:\MSYS\bin和D:\MSYS\mingw\bin加入到系统环境变量PATH中。这样就设置好了MSYS环境。
1.3 安装swigwin
到http://sourceforge.net/projects/swig/ 下载swigwin-2.0.9.zip文件,解压缩到D:\目录。
1.3.1 为swig准备pcre
swigwin依赖pcre,所以还需要到http://sourceforge.net/projects/pcre/files/pcre/ 下载pcre-8.3.2.zip文件。下载后解压缩pcre-8.3.2.zip文件,得到pcre-8.3.2文件夹,将这个文件夹重新压缩成pcre-8.3.2.tar文件,放到d:\swigwin-2.0.9目录中。
执行d:\msys\msys.bat文件,启动msys环境。依次执行cd /d/swigwin-2.0.9和./Tools/pcre-build.sh命令,为swig准备pcre。命令执行的最后会提示创建一个符号链接失败:


可以忽略这个错误,对后续操作没有影响。
1.3.2 编译swig代码
swigwin-2.0.9.zip提供的源代码有bug,在Windows不能正确地为Go语言生成调用C/C++代码的接口。到https://groups.google.com/forum/?fromgroups#!msg/golang-nuts/9L0U4Q7AtyE/iNFTnLJH9xMJ 下载swig_go_windows2.patch文件,应用这个补丁到swig源代码后swig就可以正确工作了。这个补丁是针对swig的SVN 12454版本的源代码的,不能直接用补丁工具应用到swigwin 2.0.9的源代码。笔者是手动将其应用到源代码的。
1.4 安装Visual Studio 2008
这里不论述如何安装Visual Studio 2008。安装后之后,把<Visual Studio 2008>安装目录\vc\bin目录(含有微软编译器程序cl.exe)添加到系统环境变量PATH中。还需要把<windows sdk目录>\bin目录(含有清单工具程序mt.exe)添加到系统环境变量PATH中,这个目录的全路径名一般为C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin。
2 入门示例
本节论及的文件中,example.c、example.i和main.go是预先编写的,example_wrap.c、example_dllmain.cxx、example_gc.c和example.go是SWIG编译example.i后生成的。
2.1 预先准备的文件
C语言源代码文件example.c:


SWIG接口文件example.i:



Go客户端代码文件main.go:
 

2.2 用swig编译example.i生成封装代码
在windows控制台中执行swig -go -windows -intgosize 32 example.i命令,生成example.go、example_gc.c、example_wrap.c和example_dllmain.cpp文件。注意:
只有使用了1.3.2节描述的补丁,并且重新编译swig,使用新生成的swig.exe程序,才可以在命令行中使用-windows选项,才会生成example_dllmain.cpp文件。
此外,example_wrap.c中的各个_wrap_xxx函数需要从dll中导出,才可以被Go代码调用。使用微软的C/C++编译器时可使用.def文件来定义需要导出的函数,但这要求另外编写一个.def文件,不方便;也可以在源代码中用__declspec(dllexport)来导出函数,这种方法比较方便,但是swig生成的example_wrap.c并没有使用这种方法。为此,除了对swig源代码应用1.3.2节描述的补丁外,还需要修改d:\swigwin-2.0.9\source\modules\go.cxx文件的约1278行如下:


 

其中注释的那一行是原来的代码。进行这处修改后,重新编译swig源代码生成新的swig.exe文件。此后使用swig.exe生成的example_wrap.c文件中,封装函数前面会加上SWIGEXPORT宏,而这个宏在example_wrap.c的约89行处被定义为__declspec(dllexport),这样就在dll中导出了封装函数,可以被Go代码调用。
对各文件的作用简介如下:
example.go 含有供客户端Go代码调用的封装代码,需要使用Go语言提供的8g工具进行编译
example_gc.c 含有封装代码,需要使用Go语言提供的8c工具进行编译。example.go和example_gc.c的编译输出文件分别为example.8和example_gc.8,需要使用Go语言提供的pack工具将这两个文件打包成example.a包文件供Go客户端代码调用
example_wrap.c 含有封装代码,需要使用C/C++编译器进行编译
example_dllmain.cpp 含有Windows特有的DllMain函数,需要使用C/C++编译器进行编译。需要将编译example_wrap.c和example_dllmain.cpp生成的example_wrap.obj和example_dllmain.obj文件,以及编译example.c文件生成的example.obj文件链接到一起,生成example.dll文件,供Go客户端程序运行时动态调用。
2.3 编译C/C++代码生成example.dll文件
依次执行下述命令可以生成example.dll文件:
cl /TP /c /D_WIN32 /ID:\3rdlibs\winsdk_inc /ID:\3rdlibs\vc_inc example.c example_wrap.c example_dllmain.cxx
link example.obj example_wrap.obj example_dllmain.obj kernel32.lib /MANIFEST:NO /LIBPATH:d:\3rdlibs\vc_lib /LIBPATH:d:\3rdlibs\winsdk_lib /NOLOGO /DLL /OUT:example.dll
上述命令中:
d:\3rdlibs\winsdk_inc目录含有Windows SDK的头文件(Windows.h等) ,这个目录一般为C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include,笔者系统中的d:\3rdlibs\winsdk_inc是到这个目录的链接(使用windows命令mdlink生成)
d:\3rdlibs\winsdk_lib目录含有Windows SDK的库文件(kernel32.lib等) ,这个目录一般为C:\Program Files\Microsoft SDKs\Windows\v6.0A\Lib,笔者系统中的d:\3rdlibs\winsdk_lib是到这个目录的链接
d:\3rdlibs\vc_inc目录含有Visual Studio的C/C++头文件(stdio.h等),这个目录一般为<VC安装目录(Microsoft Visual Studio 9.0)>\VC\include,笔者系统中的d:\3rdlibs\vc_inc是到这个目录的链接
d:\3rdlibs\vc_lib目录含有Visual Studio的C/C++库文件(msvcrt.lib等),这个目录一般为<VC安装目录(Microsoft Visual Studio 9.0)>\VC\lib,笔者系统中的d:\3rdlibs\vc_lib是到这个目录的链接
上面是使用Visual Studio 2008带的C/C++编译器、链接器编译链接C/C++代码的,也可以使用msys带的gcc系列工具来编译链接C/C++代码:
gcc -Wall -c example.c example_wrap.c example_dllmain.cxx
gcc -shared example.o example_wrap.o example_dllmain.o -o example.dll
使用GCC工具编译似乎简单一些:不用明确指示C/C++头文件、库文件的目录。
2.4 编译Go代码生成Go包
执行下述命令,将example_gc.c、example.go文件编译打包,生成example.a文件:
go tool 8c -I D:\Go\pkg\windows_386 example_gc.c
go tool 8g example.go
go tool pack grc example.a example.8 example_gc.8
2.5 编译Go客户端代码生成可执行程序
执行下述代码,编译main.go文件生成main.exe可执行程序:
go tool 8g -I . main.go
go tool 8l -L . -o main.exe main.8
运行main.exe:


函数调用过程大致为:main.go-->example.go(在example.a中)-->example_gc.c(在example.a中)-->example_wrap.c(在example.dll中)-->example.c(原始C代码,在example.dll中)。
上述编译链接过程总结如下:


图中第一列是预先编写的文件,第二列是swig工具生成的文件,第三列是中间文件,第四列是最终生成的文件。红色箭头表示使用swig工具;绿色箭头表示使用C/C++编译器、链接器工具;蓝色箭头表示使用Go语言提供的工具。虚线表示调用方向(当前Go语言是静态链接的,example.a中的代码已经被包含到main.exe中了,所以不存在main.exe调用外部的example.a文件的问题,这里的箭头只是表示调用逻辑)。
3 封装已有的dll
有时候需要将已有的第三方C/C++库进行封装,供Go语言代码调用。第三方库一般不提供源代码,只提供.h、.lib和.dll文件。这种情况下的封装方法与第二节描述的基本相同,只是有以下差别:
编译的时候没有源代码文件(example.c)
链接的时候不是链接源代码生成的目标文件(example.obj),而是链接第三方库(example.lib)
运行Go代码生成的可执行程序时,要求两个dll,即第三方提供的dll、以及封装第三方dll的dll
具体细节请看笔者提供的示例代码。
4 回调函数
有时候C/C++代码要求提供回调函数:C/C++代码在必要的时候会调用客户代码提供的回调函数。因为(C/C++和Go)运行时系统的差别,Go代码中的函数无法被C/C++代码直接调用。为应对这种情况,swig提供了代理(director)类,使得Go代码中的类型可以成为C++代码中类的子类,并且支持虚拟方法。这样,C++代码调用某个类的某虚拟方法时,实际调用的代码可以是Go代码。借助这个特性可以实现C++代码回调Go代码。细节可参考D:\swigwin-2.0.9\Examples\go\callback目录中的文件。
下面论述在第二节的代码基础上增加回调函数示例代码的过程。
swigwin 2.0.9生成的代码有点问题,需要修改D:\Source\Modules\go.cxx后重新编译生成新的swig.exe。修改go.cxx的方法如下:
约第2866行的代码为:Printv(f_c_directors, "extern "C" void (*", wname, ")(void*, int);\n", NULL);
去掉其中的"C",改为:Printv(f_c_directors, "extern void (*", wname, ")(void*, int);\n", NULL)
为方便代码编写,新建example.h文件,其内容是example.c中变量、函数的声明;将example.i中的变量、函数声明替换成#include "example.h"
在example.h末尾增加下述代码

这里的GoCallbackHelper是执行回调函数的辅助类,其RunCallback虚拟方法用于辅助调用回调函数。这个函数不会被调用(运行中会调用子类重新实现的这个函数),但是必须写上空的实现代码。Go代码将实现这个类的子类,重新实现RunCallback函数,用于真正调用Go代码中的回调函数。三个参数分别是(Go代码提供的)回调函数和回调函数的参数。
go_callback指针用于辅助调用回调函数,运行中会通过它来调用上述GoCallbackHelper::RunCallback函数。
修改example.c,在其末尾增加下述代码


修改example.i,增加红色方框表示的代码

①表示启用辅助类特征;②表示为C++代码中的GoCallbackHelper生成辅助类代码;③表示在进行参数传入(Go代码调用C++代码时传入参数)时,Go代码中签名为func(int,int) int的函数,可以用作C++代码中的CallbackFunc函数。也就是说,Go代码调用上述第2点末尾的test_callback函数时,可以将签名为func(int,int)int的函数作为参数。
修改main.go,增加下述代码

这里,Go代码中的goCallbackHelper类型成了上述C++代码中GoCallbackHelper类的子类(注意:两个名字碰巧一致,但这不是必须的),并且重新实现了虚拟方法RunCallback。这个方法的第一个参数就是Go代码中的回调函数,所以可以直接调用这个回调函数(调用者和被调用函数都是Go代码写成的)。
init()是(main)模块初始化方法,模块初始化的时候会调用这个方法。这个方法设置了上述第2点和第3点论及的全局变量GoCallbackHelper* go_callback(swig生成的set方法名为SetGo_callback)。后面的NewDirectorXXX是swig生成的包装函数,其中XXX是(C++中的)辅助类名。example.NewDirectorGoCallbackHelper(...)表示将Go代码中的指定对象封装成一个C++代码中的GoCallbackHelper类型的对象。相关代码在swig生成的example_wrap.cxx文件中。
最后的mod函数是将被C++代码调用的回调函数。
main.go中调用上述test_callback函数的代码如下




 


第一次使用mod函数作为回调函数;第二次使用闭包函数作为回调函数。
调用流程大致为:example.Test_callback(main.go中)-->test_callback(dll中)-->go_callback->RunCallback()(虚拟方法,实际调用的是main.go中的goCallback::RunCallback方法)-->mod(main.go中的回调函数)。可参考2.5节关于调用流程的描述。
适当修改build.bat
调用swig.exe的时候,需要增加-c++选项
调用cl或者g++编译源代码的时候,要注意example_wrap.c应该改成example_wrap.cxx
5 在其他线程中调用回调函数
有时候C++代码要在不同的线程中调用回调函数(意思是说:在一个线程中提供/设置回调函数,但是在另一个线程中调用回调函数。以第四节的代码为例,如果test_callback函数不立即调用回调函数,而是创建一个线程,然后在新创建的线程中调用回调函数,就是这里所谓的“在不同的线程中调用回调函数”)。同样的,由于运行时系统的不同,这种调用会导致程序崩溃。
参考http://stackoverflow.com/questions/4312894/c-callbacks-and-non-go-threads提供的思路,笔者成功实现了在不同的线程中调用Go代码中的回调函数。下面论述如何在第四节代码基础上增加在其他线程中调用回调函数的示例代码。
5.1 回调服务
上面说过,由于运行时系统的不同,不能在其他线程中调用回调函数。也就是说,不能在Go代码不知道的、由C++代码创建的线程中调用Go代码提供的回调函数。但是实际开发中这种需求(比如说,第三方库内部创建自己的工作线程,在工作线程中调用客户提供的回调函数)是比较多的,如何解决这个问题呢?解决方法就是:在Go代码了解的线程中执行回调函数。实际上,Go在语言级别支持去程(goroutine),应该说是在Go代码提供的去程中执行回调函数。这就涉及到一个线程切换:C++代码创建的工作线程要调用回调函数的时候,切换到Go代码提供的线程(去程)中,在这个线程(去程)中执行Go代码提供的回调函数。参考http://stackoverflow.com/questions/4312894/c-callbacks-and-non-go-threads提供的思路,笔者使用boost.asio的io_service作为线程切换工具,实现了在C++代码新创建的工作线程中调用Go代码提供的回调函数的功能。
首先,在C++头文件中声明三个函数:

三个函数的含义很容易从名字上看出来。它们的实现代码如下:
// ==================== 回调服务线程 ====================
static std::map<int,boost::asio::io_service*> s_callback_service_map;
static boost::mutex s_cb_serv_map_mtx;
static int s_next_cb_serv_idx = 1;

// 新建回调线程
int NewCallbackThread(){
boost::mutex::scoped_lock lock(s_cb_serv_map_mtx);
int id = s_next_cb_serv_idx++;
try{
s_callback_service_map.insert(std::make_pair(id,new boost::asio::io_service));
return id;
}
catch(...){
}
return 0;
}

// 启动回调线程
bool RunCallbackThread(int id){
boost::mutex::scoped_lock lock(s_cb_serv_map_mtx);
std::map<int,boost::asio::io_service*>::iterator itor = s_callback_service_map.find(id);
if (itor != s_callback_service_map.end()){
boost::asio::io_service& callback_service = *(itor->second);
boost::asio::io_service::work idle(callback_service);
boost::system::error_code bsec;
lock.unlock();
callback_service.run(bsec);
return true;
}
else{
return false;
}
}

// 停止回调线程
bool StopCallbackThread(int id){
boost::mutex::scoped_lock lock(s_cb_serv_map_mtx);
std::map<int,boost::asio::io_service*>::iterator itor = s_callback_service_map.find(id);
if (itor != s_callback_service_map.end()){
boost::asio::io_service& callback_service = *(itor->second);
callback_service.stop();
s_callback_service_map.erase(itor);
return true;
}
else{
return false;
}
}
如果使用过boost.asio,那么上述代码是很容易理解的:
NewCallbackThread:函数并没有创建新的线程,新建了一个boost::asio::io_service对象,我们将借助这个对象来进行执行回调函数所需的线程切换。
RunCallbackThread:对boost::asio::io_service对象执行run函数,开始事件循环。
StopCallbackThread:对boost::asio::io_service对象执行stop函数,停止事件循环。
上述三个函数并没有创建新的线程,因为我们不能在C++代码创建的线程中执行Go代码提供的回调函数。我们要借助boost::asio::io_service对象的事件循环来执行回调函数,所以上述RunCallbackThread应该在Go代码了解的线程(去程)中执行。实际上,Go代码是这样使用这三个函数的:

代码go example.RunCallbackThread()创建了一个新的去程,在这个去程中执行boost::asio::io_service对象的事件循环,也就是将在这个去程中执行Go代码提供的回调函数。
5.2 定时器示例
从上节末尾提供的代码可以看出,笔者要提供的示例是一个简单的定时器。使用MySetTimer设置定时器,使用MyCancelTimer停止定时器。这两个函数的声明如下:

Windows中使用swig让Go代码调用C/C++代码
MySetTimer函数的第一个参数是回调线程id,由上一节论述的NewCallbackThread函数返回。
示例C++代码将创建一个工作线程,在这个工作线程中使用boost::asio提供的deadline_timer来实现自己的定时器。定时器超时的时候调用Go客户提供的Go代码中的回调函数,通知当前时间。

MySetTimer函数的主要代码片段如下:
Windows中使用swig让Go代码调用C/C++代码
Windows中使用swig让Go代码调用C/C++代码
上述代码中的callback_service就是上一节新建的boost::asio::io_service对象。回调辅助函数info.helper_fn使用callback_service.wrap包装了Go代码提供的回调函数fn。这样,在定时器工作线程timer_thread(注意: 这个线程是在C++代码中创建的)中调用回调辅助函数info.helper_fn就相当于调用callback_service.post(GoCallbackHelper::RunTimerCallback,go_callback,fn,...)。关于boost::asio::io_service::wrap函数的详细说明,请参阅boost.asio文档。
上述GoCallbackHelper::RunTimerCallback是新增加的函数,其定义如下:
Windows中使用swig让Go代码调用C/C++代码
其作用与第四节描述的RunCallback函数的作用类似。

上述timer_thread线程的线程函数TimerThread代码如下:
Windows中使用swig让Go代码调用C/C++代码
注意:上述代码中的timer_service不同于上一节描述的由NewCallbackThread新建的boost::asio::io_service对象。

上述代码中定时器超时回调函数OnTimer代码如下:
Windows中使用swig让Go代码调用C/C++代码
这段代码的要点是调用info.helper_fn。上面说过,调用这个函数相当于调用callback_service.post(GoCallbackHelper::RunTimerCallback,go_callback,fn,...):将调用GoCallbackHelper::RunTimerCallback函数的请求投递到callback_service对象中。这里的callback_service对象由上一节描述的NewCallbackThread函数创建,其事件循环在RunCallbackThread函数中运行。RunCallbackThread函数中的事件循环取得函数调用请求后,执行指定的函数GoCallbackHelper::RunTimerCallback。RunCallbackThread是在Go代码创建的去程中执行的,GoCallbackHelper::RunTimerCallback也是在同一个去程中执行的。这就实现了在Go代码创建的去程中调用GoCallbackHelper::RunTimerCallback,进而调用Go代码提供的回调函数的功能了。
6 指针
6.1 简单指针
swig直接支持简单数据类型的指针,如char*,int*,uint64_t*等。要注意的是,要使用uint64_t类型,必须在.i文件中添加%include <stdint.i>(注意是.i,不是.h,stdint.i可以在d:\swigwin-2.0.9\Lib目录中找到)。此外,Visual Studio 2008是不带stdint.h文件的,可以百度搜索出下载这个文件的地方,或者使用带有stdint.h的Visual Studio 2010。
6.2 传递数据
有时候需要将C/C++代码生成的数据传递到Go代码中,这时候需要将数据转换成一串字节,SWIG提供的cdata.i文件(在d:\swigwin-2.0.9\Lib\Go目录中)提供了对这种转换的支持。下面通过一个生成菲波拉契数列的函数来看看如何使用这种转换。
C++代码的菲波拉契函数的代码如下:
Windows中使用swig让Go代码调用C/C++代码
SWIGCDATA在上述cdata.i中定义如下:
Windows中使用swig让Go代码调用C/C++代码
实际上这与Go语言中字节切片类型[]byte的内部存储结构类似(参考Go语言文档D:\Go\doc\articles\slices_usage_and_internals.html)。所以在C++代码和Go代码间交换数据时,C++代码中的SWIGCDATA*类型可以对应到Go代码中的*byte[]类型。
Go代码调用菲波拉契函数的代码如下:
Windows中使用swig让Go代码调用C/C++代码
这里将*byte[]类型的&x用作SWIGCDATA*类型传入到菲波拉契函数中,让菲波拉契函数将生成的数列存储在字节切片x中。
上面论述的是很简单的数据传递。如果要将C++中结构体MyStruct表示的数据传递到Go代码中,可以这么做:
在.i文件中使用cdata.i中定义的cdata宏:�ata(MyStruct)
在C++代码中使用cdata宏展开生成的cdata_MyStruct函数:
void getMyStruct(SWIGCDATA* p){
MyStruct *data = (MyStruct*)malloc(sizeof(MyStruct) * 10);
<... 填充 ...>
*p = cdata_MyStruct(data,10);
}
这样做一般没有什么意义:虽然能够将C++中的结构体数据转换成一串字节传回到Go代码中,但是Go代码并不理解这一串字节的含义(Go代码不能理解C++的MyStruct结构体)。但是可以使用这种方法将数据从C++代码中(的一个函数)传回到Go代码中,然后Go代码又将其传递到C++代码中(的另一个函数),这种处理是有意义的。
注意内存的使用
写代码时一定要清楚SWIGCDATA的data字段当前指向的内存是C++代码分配的,还是Go代码分配的。Go有垃圾收集功能,不用考虑内存释放问题;但是C++没有垃圾收集功能,需要考虑如何释放由C++代码分配的内存。改变SWIGCDATA的data字段时,如果data字段当前指向C++分配的内存,则要考虑释放这段内存。例如,如果Go代码这样调用上述getMyStruct函数,就有内存泄漏了:
x := make([]byte,10)
example.GetMyStruct(&x)
x = nil
这时候C++代码可以提供一个释放内存的函数供Go代码调用:在x=nil前调用example.FreeMyStruct(&x)。
6.3 传递Go中的[]int到C++中
上述SWIGCDATA只能用于[]byte。如果要实现一个等差级数函数,让C++代码填充Go中一个整型切片[]int的数据,该怎么办呢?
参考cdata.i,可以在自己的.i文件中写上:
Windows中使用swig让Go代码调用C/C++代码
然后定义等差级数函数如下:
Windows中使用swig让Go代码调用C/C++代码
Go代码这样调用:
Windows中使用swig让Go代码调用C/C++代码
7 示例代码
本文论述的示例代码可以在http://www.kuaipan.cn/file/id_43841381185098984.htm 下载。解压缩后的文件介绍如下:
example1、2、3、4、5文件夹分别含有本文第2、3、4、5、6节描述的代码。执行每个文件夹中的build.bat生成可执行程序,执行clean.bat进行清理。默认使用微软的C/C++编译器进行C/C++代码的编译链接,可以修改build.bat文件,取消对set compiler=gcc这一行的注释,然后执行build.bat,就是使用GCC工具进行C/C++代码的编译链接了(除对应本文第五节的example4中的代码没有使用GCC工具编译链接完成外,其他几个目录中的代码都可以正确地使用GCC工具进行编译链接。第五节的代码使用了boost,笔者虽然用GCC成功地在Windows中编译了boost,但是还不知道如何使用,链接.o文件不成功)。要正确地用微软的C/C++编译器、链接器,请按照2.3节的描述为VC和Windows SDK的头文件、库文件目录建立链接(Windows7中使用mklink命令),或者修改build.bat文件中的头文件、库文件目录指定。
patch文件夹含有修改后的swigwin2.0.9的几个文件:go.cxx对应Source\Modules\go.cxx文件;goruntime.swg对应Lib\go\goruntime.swg文件;Go.html对应Doc\Manual\Go.html文件
stdint.h和inttypes.h供Visual Studio 2008使用,可以放到VS 2008安装目录\vc\include中 

标签: go
扩展知识