calling c++ from golang with swig--windows dll
之前項目組開發的項目核心代碼全部使用C++語言,新項目可能會引入golang,花了一天多時間研究了windows環境下golang調用C++動態鏈接庫的方法。
谷歌加百度之后,很快發現官方推薦的方法,在官方FAQ頁面可以找到答案:
Do Go programs link with C/C++ programs?
There are two Go compiler implementations, gc and gccgo. Gc uses a different calling convention and linker and can therefore only be linked with C programs using the same convention. There is such a C compiler but no C++ compiler. Gccgo is a GCC front-end that can, with care, be linked with GCC-compiled C or C++ programs.
The cgo program provides the mechanism for a “foreign function interface” to allow safe calling of C libraries from Go code. SWIG extends this capability to C++ libraries.
上文的大意是:golang有兩個編譯器實現,gc和gccgo。gc編譯器使用一個不同的調用約定和鏈接器,因此只能鏈接使用相同約定的C程序;gccgo是一個GCC的前端,可以鏈接GCC編譯的C和C++程序。cgo程序提供了一個“外部函數接口”機制,允許在Golang中安全地調用C庫。SWIG擴展了這個能力,使得Golang可以調用C++庫。
由官方的問答可以知道,如果Golang要調用C++庫,需要借助gccgo和swig。
SWIG 是一個非常優秀的開源工具,支持您將 C/C++ 代碼與任何主流腳本語言相集成。SWIG的一個入門介紹可以參考《開發人員 SWIG 快速入門》
http://www.ibm.com/developerworks/cn/aix/library/au-swig/
SWIG官方網址 http://www.swig.org/
Github上的開源代碼倉庫: https://github.com/swig/swig
從github上看介紹,swig支持很多種語言:
SWIG is a compiler that integrates C and C++ with languages
including Perl, Python, Tcl, Ruby, PHP, Java, C#, D, Go, Lua,Octave, R, Scheme (Guile, MzScheme/Racket, CHICKEN), Scilab, Ocaml, Modula-3, Common Lisp (CLISP, Allegro CL, CFFI, UFFI) and Pike. SWIG can also export its parse tree into XML and Lisp s-expressions.
Swig最新的發布版本是2017年1月28日發布的rel-3.0.12。
在Github上下載的發布包沒有預編譯的swig.exe;自己編譯比較麻煩,所以直接從http://www.swig.org/download.html 頁面下載最新發布包,
根據提示,從第二個鏈接下載,包含了預編譯的可執行程序。
解壓縮后,文件夾下有swig.exe,后面會看到需要借助這個工具生成代碼。Golang借助swig調用C++還是非常簡單的,看下Examples/go目錄下的示例就可以入門了。
用瀏覽器打開Examples\go\index.html看下文字說明,
- When using the gccgo compiler, the steps look like this:
% swig -go -cgo interface.i
% mkdir -p gopath/src/interface
% cp interface_wrap.c interface_wrap.h interface.go gopath/src/interface
% GOPATH=`pwd`/gopath
% export GOPATH
% cd gopath/src/interface
% go build
% gccgo -c $(SRCDIR)/runme.go
% gccgo -o runme runme.o interface.a
這個文檔介紹的使用說明與軟件版本不太相符,文檔更新落后軟件太多。實際上只要一條命令就可以了。下面來介紹下go借助swig調用C++的方法,以Examples\go\class為例:
(我的電腦是windows 7 X64,golang也是64位)
將下載的swigwin-3.0.12.zip包解壓到D盤,然后將D:\swigwin-3.0.12\swig.exe加入環境變量。
打開cmd,工作目錄切換到D:\swigwin-3.0.12\Examples\go\class
執行命令 swig -c++ -cgo -go -intgosize 64 example.i
成功執行后在D:\swigwin-3.0.12\Examples\go\class生成兩個文件,example.go和example_wrap.cxx,這兩個文件便是是在go中調用的包,runme.go屬於main包,golang只運行一個文件夾對應一個包,所以需要在D:\swigwin-3.0.12\Examples\go\class下創建example子文件夾,將example.go和example_wrap.cxx、class.cxx、example.h四個文件移動到D:\swigwin-3.0.12\Examples\go\class\example路徑下。
命令行中執行go build, 編譯成功后,在D:\swigwin-3.0.12\Examples\go\class文件夾中生成class.exe,執行class.exe輸出如下內容:
回過頭來看下詳細的開發步驟。
首先編寫供golang調用的c++源文件,
example.h和class.cxx
/* File : example.h */
class Shape {
public:
Shape() {
nshapes++;
}
virtual ~Shape() {
nshapes--;
}
double x, y;
void move(double dx, double dy);
virtual double area() = 0;
virtual double perimeter() = 0;
static int nshapes;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) { }
virtual double area();
virtual double perimeter();
};
class Square : public Shape {
private:
double width;
public:
Square(double w) : width(w) { }
virtual double area();
virtual double perimeter();
};
該頭文件包含了一個基類和兩個派生類的聲明。
class.cxx包含了對應的源碼實現:
/* File : class.cxx */
#include "example.h"
#define M_PI 3.14159265358979323846
/* Move the shape to a new location */
void Shape::move(double dx, double dy) {
x += dx;
y += dy;
}
int Shape::nshapes = 0;
double Circle::area() {
return M_PI*radius*radius;
}
double Circle::perimeter() {
return 2*M_PI*radius;
}
double Square::area() {
return width*width;
}
double Square::perimeter() {
return 4*width;
}
接下來需要定義swig工具生成代碼需要的輸入文件example.i,只需指定包名和包含的c++頭文件,相當簡單。
/* File : example.i */
%module example
%{
#include "example.h"
%}
/* Let's just grab the original header file here */
%include "example.h"
接下來就是在命令行中輸入swig -c++ -cgo -go -intgosize 64 example.i 生成golang調用的包裝代碼:example.go和example_wrap.cxx;example_wrap.cxx文件將c++語法隱藏在C函數內部,對外暴露c接口,example.go調用example_wrap.cxx中的c函數,聲明並實現golang版的接口。
runme.go中引入example包 . "./example"
c := NewCircle(10) 簡短變量聲明並定義了SwigcptrCircle類型的變量,SwigcptrCircle實現了Circle 接口。SwigcptrCircle的基礎類型是uintptr,NewCircle返回的值實際上就是C++對象的this指針,通過SwigcptrCircle類型的值調用Circle 接口定義的方法,實質上是通過example_wrap.cxx暴露的C函數及“this指針”在函數內部調用將指針轉換成c++對象指針然后調用相應的方法。
type Circle interface {
Swigcptr() uintptr
SwigIsCircle()
Area() (_swig_ret float64)
Perimeter() (_swig_ret float64)
SetX(arg1 float64)
GetX() (_swig_ret float64)
SetY(arg1 float64)
GetY() (_swig_ret float64)
Move(arg1 float64, arg2 float64)
SwigIsShape()
SwigGetShape() Shape
}
從Golang的角度看,借助swig生成的包裝類型,調用c++類方法就像調用golang內部的接口一樣,非常簡單易用。
在實際的項目開發中,包含了大量的C++頭文件和源碼文件,通常還會引入大量的第三方庫文件。我們的項目中,開發人員編寫的代碼超過五十萬行,還有大量的第三方庫,例如boost、thrift、redis等,所有C++代碼量超過幾百萬行,編譯后的輸出包含大量的動態鏈接庫及少量的可執行程序;在服務端,進行跨語言混合開發無外乎涉及兩方面,實現庫時提供不同語言的實現,例如使用thrift框架可以生成C++、Java、Golang等語言的客戶端、服務端版本,不同的組件可實現跨語言跨平台調用;另外一種主要涉及跨語言庫調用。Examples\go目錄下的示例沒有講述如何實現golang調用C++ dll,並且在官方文檔中也沒有找到如何實現。(通過D:\swigwin-3.0.12\Doc\Manual\index.html 可以查看最新版的3.0文檔)。Google加百度沒有找到一篇文檔能夠詳細地指出windows環境下golang調用c++ dll的方法,在不斷地編碼嘗試下,最終自己搞定。具體細節在后兩篇文檔中指出。