go經過swig封裝、調用c++共享庫的技術總結

go經過swig封裝、調用c++共享庫的技術總結

@(知識記錄)html

1 簡介

最近在研究golang,但願能對目前既有的python服務作一些優化,這些服務目前已經佔用了6-7臺機器。選擇golang的緣由,是看上其在併發方面更簡單的支持,比c++更高的開發效率,以及比python更高的運行效率。python

因爲現實的緣由,咱們不太可能將全部模塊都用golang重寫一遍,有一些公司通用的模塊是用C++編譯成爲.so的方式提供的。所以,若是想要用golang重構服務,調用C++共享庫是不可能繞過的問題,也是首要解決的問題。c++

本文是對golang調用、封裝c++共享庫的技術總結,共分爲四部分。第一部分介紹golang調用c語言接口的基本方法並介紹cgo;第二部分介紹swig的用法;第三部分是一個示例工程,完整模擬現實環境的調用和封裝;第四部分對實際問題中的一個.so模塊進行封裝。git

2 go調用c及cgo簡介

最初遇到本文問題(go封裝c++共享庫)時,我在網上搜索到最多的文章,就是go如何調用c代碼中的函數。當時的感受是有點失望,由於都沒能一步一步手把手完整解決個人問題。可是如今看來,本節的主題(go調用c代碼)是後面全部工具的基礎。github

2.1 示例代碼

首先,放上一段golang示例代碼,這段代碼來自cgo官方文檔golang

package main

// #include <stdio.h>
// #include <stdlib.h>
//
// static void myprint(char* s) {
//   printf("%s\n", s);
// }
import "C"
import "unsafe"

func main() {
	cs := C.CString("Hello from stdio")
	C.myprint(cs)
	C.free(unsafe.Pointer(cs))
}

首先,這是一段golang代碼,從package定義、到import包、到函數定義,都是咱們熟悉的golang元素。main函數內部是一個變量初始化和兩個函數調用,且變量和函數的定義都來自名爲C的package。在目錄下運行go build -o test命令,能夠獲得一個可執行文件test,再運行./test命令,能夠看到以下輸出。vim

>./test
Hello from stdio

2.2 代碼解析

這段代碼的關鍵部分在於import "C"及其以前的註釋部分。併發

"C"在這裏是一個pseudo-package,並非一個實際存在的go package。對C語言部分的全部引用,都經過這個pseudo-package來進行。**在import "C"以前的註釋,能夠是任何合法的c語言代碼,go代碼能夠引用這些C語言定義的函數、變量等,彷彿它們就是定義在名爲C的package中。**能夠是定義,也能夠經過extern聲明其餘C文件中的定義。具體到上面的代碼,在註釋部分定義了一個C函數myprint,而後在go的main函數中調用了它。app

2.3 cgo指示

另外,在import "C"以前的註釋,還能夠包括cgo指示( #cgo directives),這個特性在上面的簡單示例代碼中沒有涉及。以下代碼所示:ide

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
// #include <png.h>
import "C"

cgo指示(go directives)以#cgo開頭,並緊接着一個空格。這部份內容不是C代碼,可是用來控制C編譯器及link的參數,能夠包括CFLAGS, CPPFLAGS, CXXFLAGS, FFLAGS和LDFLAGS 。

  • 一個package中全部CPPFLAGS和CFLAGS cgo指示,都被連在一塊兒,在編譯C文件時使用;
  • 一個package中全部CPPFLAGS和CXXFLAGS cgo指示,都被連在一塊兒,在編譯C++文件時使用;
  • 一個package中全部CPPFLAGS和FFLAGS cgo指示,都被連在一塊兒,在編譯Fortran文件時使用;
  • 一個package中全部LDFLAGS cgo指示,都被連在一塊兒,在連接時使用。

2.4 這一切怎麼發生的

摘抄自cgo文檔

When the Go tool sees that one or more Go files use the special import "C", it will look for other non-Go files in the directory and compile them as part of the Go package. Any .c, .s, or .S files will be compiled with the C compiler. Any .cc, .cpp, or .cxx files will be compiled with the C++ compiler. Any .f, .F, .for or .f90 files will be compiled with the fortran compiler. Any .h, .hh, .hpp, or .hxx files will not be compiled separately, but, if these header files are changed, the package (including its non-Go source files) will be recompiled. Note that changes to files in other directories do not cause the package to be recompiled, so all non-Go source code for the package should be stored in the package directory, not in subdirectories. The default C and C++ compilers may be changed by the CC and CXX environment variables, respectively; those environment variables may include command line options.

當go tool發現一個或多個文件裏包含import "C"時,它會尋找目錄內其餘非go源碼的文件,而且將它們編譯爲package的一部分。對於.c,.s及.S結尾的文件,會用c編譯器編譯;對於.cc,.cpp和.cxx文件,會用c++編譯器編譯;對於.f,.F,.for和.f90文件,會用fortran編譯器編譯。對於.h,.hh,.hpp和.hxx文件,雖然不會編譯,但若是這些頭文件發生了變化,package整個會被從新編譯。其餘目錄的文件若是發生變化,不會引發package從新編譯。所以,全部全部非go文件都應該放在package的目錄下,不要放在任何子目錄裏。默認的c和c++班一塊兒,會被CC和CXX環境變量影響,這些環境變量能夠在命令行參數中包括。

2.5 其餘注意事項

2.5.1 指針

Go是有gc的語言,C是沒有gc的語言,且二者都是有指針的語言,因此在處理指針時應該格外注意。Go的內存管理模塊沒法知道C內部發生了什麼,C也不知道Go的內存管理。

在使用指針時,應該對某個指針時C指針Go指針有明確的認知。C指針是指向經過C的庫分配的內存,Go指針是指向Go代碼分配的內存。這個目的是區分這塊內存是由誰(C仍是Go)來分配的,而不是由指針的類型決定的。

Go code may pass a Go pointer to C provided the Go memory to which it points does not contain any Go pointers. The C code must preserve this property: it must not store any Go pointers in Go memory, even temporarily.

Go代碼給C傳遞一個指針,應該保證這塊內存或結構中,不包含其餘Go指針。C代碼應該保證,不保存任何Go指針,即使臨時保存也不行。

回到咱們最初的示例代碼。

func main() {
	cs := C.CString("Hello from stdio")
	C.myprint(cs)
	C.free(unsafe.Pointer(cs))
}

因爲cs是一個C字符串,這塊內存是C內存,所以須要咱們在用完以後手動釋放。即使這是在Go代碼中操做的。

2.5.2 封裝

對C函數及類型的訪問和操做,如上述幾段代碼中操做C.開頭的變量或函數,應該限制在一個package裏,這個package的做用就是將C的函數封裝爲Go的函數。由於使用C類型須要操心的東西比較多,封裝起來更容易管理也不容易出問題,例如上例中釋放字符串這種操做。通常人應該不會但願在寫Go代碼時,心理還一直惦記着這些事情。

3 SWIG

有了上一節提到的Cgo支持,全部C庫及接口均可以被Go調用,標準C接口也是大多數庫的開發包都提供的接口。然而,因爲一些歷史問題,有些不那麼規範的庫只提供了C++風格的接口,例如接口用到vector、map、string等,Cgo是不支持C++的這些特性的。遇到這種狀況,標準的作法是給這些C++接口再封裝一個標準C接口。這個封裝工做須要咱們再寫一份C或C++代碼,作一些類型或者接口的轉換,經過extern C導出C風格的接口。而後再按照上一節的作法,用Go去調用這個標準C接口。

若是接口比較簡單,數量也很少,上述封裝能夠手工完成。若是接口數量較多,且涉及大量C++特性,上述封裝工做可能就不難麼容易了。

**SWIG就是自動幫你作了這件事。**準確地說,SWIG生成了兩個文件,一個文件是*_wrapper.cpp文件,一個是*.go文件。*_wrapper.cpp文件將C++接口封裝爲C接口。*.go文件經過上一節說的import "C"來引用C接口,並把對這些C接口的調用,封裝爲不涉及任何C特性的Go函數或方法。所以,它實際作了兩件事,一是咱們上面說的將C++接口封裝爲C接口,另外一件是上一節說的封裝問題,在Go代碼裏把對C接口的使用細節封裝起來。

接下來咱們就看一下SWIG的使用方法及它對Go的支持。

3.1 SWIG簡介

SWIG是一個軟件開發工具,用於將C或C++程序與其餘高級程序語言鏈接起來。它支持多種目標語言,包括腳本語言如Javascript、Perl、PHP、Python、Tcl、Ruby,也包括非腳本語言如C#,Common Lisp (CLISP, Allegro CL, CFFI, UFFI)、D、Go 、Java等。SWIG解析C或C++接口,生成「鏈接代碼」,使其餘高級語言能夠調用C或C++的代碼。

使用SWIG須要先定義一個接口文件,這個接口文件說明了須要導出的接口及數據類型,這個接口文件以.i做爲後綴。我以一個簡單的實例,來講明我用到的一些特性,其餘技術細節能夠參考SWIG DocSWIGPlus Doc

%module compare_length
%{
#include "compare_length.h"
%}

%include "typemaps.i"
%include "std_vector.i"

%template(VecInt) std::vector<int>;

int compare(const std::vector<int>& vl, const std::vector<int>& vr);

從語法層面看,SWIG文件是一個加強版的C++文件。它支持全部C++語法,它還包括SWIG指令。全部以%開頭的行,都是SWIG指令,位於「%{」和「%}」之間的部分不會被處理,會被原封不動地複製到*_wrapper.cpp文件中,這可使wrapper引用一些頭文件。你甚至可讓SWIG來直接處理一個.h文件或.cpp文件,可是並不推薦這麼作。

通常使用來講,SWIG接口文件應該包括

  1. 模塊聲明,位於第一行,以%module指令開頭;
  2. 定義須要在_wrapper.cpp文件裏包含的頭文件,即%{和%}之間的部分。在上面的例子,就是包含int compare函數的那個頭文件;
  3. 聲明要導出的接口及類型。這就是徹底的ANSI C/C++聲明語法。
  4. 根據具體需求,其餘須要的輔助指令。包括%include指令、%template指令等。具體能夠根據須要參考SWIG安裝文件的Examples,路徑位於SWIG_SOURCE_ROOT/Examples,也能夠參考SWIGPlus doc

接口文件寫完後。須要運行SWIG命令,對於不一樣的目標語言,有不一樣的參數可選。咱們這裏以python和go爲例說明一下。

swig -c++ -python compare_length.i
swig -c++ -go -intgosize 64 -cgo compare_length.i

總結一下。第一,須要根據須要寫一個.i接口文件,定義導出的接口;第二,根據目標語言運行swig命令,生成鏈接C/C++與目標語言的代碼。

剩下的部分,就因目標語言而異了。對於Python,須要將swig生成的[module_name]_wrapper.cpp文件與原有的C/C++庫或文件編譯爲一個.so,而後經過生成的[module_name].py在Python中使用。對於Go,須要將[module_name]_wrapper.cpp、[module_name].go,以及原有的C/C++庫或文件放到GOPATH下的一個具體路徑裏而後經過go來build。

3.2 SWIG與Go

這裏先說一下版本問題,咱們用的是Go 1.8和SWIG 3.0。Go 1.4和Go 1.5的go tool有較大差異,不少方法中用到的6c、6g、8c、8g在1.5之後的版本都去掉了,但1.5之後支持cgo。SWIG 3.0支持-cgo參數。本文用的方法都是基於cgo的。

由於C++與Go語法上存在一些差異,不能徹底對應上,所以在將C++的元素導出時會有一些修改。

  1. 全部的Go代碼都必須在一個package內,SWIG生成的Go代碼,也都位於一個package內,這個package的名稱由SWIG接口文件中的%package指令設置;
  2. 因爲在Go中只有大寫字母開頭的名稱,纔是在package外可用的,所以全部被導出的C++名稱(變量、函數、類等),若是是小寫字母開頭,則會被轉換爲大寫字母開頭;
  3. 導出C++的變量,會被封裝爲Get和Set兩個函數。例如一個C++變量名爲var,SWIG會爲其生成SetVar和GetVar兩個方法;
  4. 導出C++變量若是是常亮,則只提供Get方法;
  5. 導出C++的宏,在Go中會變成一個常量;
  6. 導出C++的類,會被Go導出爲兩個類型。因爲Go沒有類的概念,所以會爲其生成兩個類型,一個類型用來持有C++對象的指針,一個與C++類同名的接口類型用來調用方法。全部的C++類的公共變量,都會在這個接口內生成Set和Get兩個方法。另外,Go會生成一個NewClassName的函數來初始化對象。

其餘細節我這裏也沒有涉及,有須要能夠參考SWIG文檔中SWIG and Go這一部分。

如今咱們回過頭來看一下SWIG所作的事情,就是利用一些trick把C++接口中與Go沒法對應的部分,在儘可能不影響語義的前提下對應起來。這也正是若是咱們若是手工封裝C++模塊時所要作的。如今SWIG替咱們作了這些累活。

4 用SWIG封裝C++動態庫示例

4.1 項目簡介

本節以一個示例來講明,如何使用SWIG鏈接Go和C++代碼。示例項目包括C++編譯的動態庫.so,C++源碼,C++風格的函數接口,C++風格的數據類型。這些特性大部分是Cgo不能直接支持的,也是在實際項目中常常遇到的。

  1. libl2.so,l2.h。一個動態庫及其頭文件,其中的函數int l2(const std::vector<int>& elements)用於計算一個向量的長度。咱們沒有該函數的實現代碼。
  2. compare_length.cxx,compare_length.h。一個cxx文件和其頭文件,這個cxx文件中的函數int compare(const std::vector<int>& vl, const std::vector<int>& vr)調用了在l2.h中定義l2函數,比較兩個向量的長度。這個函數是咱們要導出給Go的函數。

咱們沒有l2函數的實現代碼,只有動態庫,對應於在實際工程中用到第三方動態庫。咱們有compare函數的源代碼和頭文件,對應於已有的C++實現的一些功能,這部分功能咱們不想在Go中重複實現。

libl2.so位於$GOPATH/compare_length/路徑下。

l2.h的內容以下,位於$GOPATH/compare_length/路徑下:

#include <vector>

int l2(const std::vector<int>& elements);

compare_length.h的內容以下,位於$GOPATH/compare_length/路徑下:

#include<vector>
#include "l2.h"

int compare(const std::vector<int>& vl, const std::vector<int>& vr);

compare_length.cxx的源碼以下,位於$GOPATH/compare_length/路徑下:

#include <vector>
#include "l2.h"
#include "compare_length.h"

int compare(const std::vector<int>& vl, const std::vector<int>& vr) 
{
    int l2_l = l2(vl);
    int l2_r = l2(vr);
    return l2_l - l2_r;
}

4.2 編譯步驟

首先,寫一個SWIG接口文件compare_length.i,用於指定導出的函數和數據類型。

%module compare_length
%{
#include "compare_length.h"
%}

%include "typemaps.i"
%include "std_vector.i"

%template(VecInt) std::vector<int>;

int compare(const std::vector<int> vl, const std::vector<int> vr);

導出的模塊經過%module指定爲compare_length。導出一個函數compare和一個類型VecInt。

第二步,運行如下SWIG命令,生成compare_length_wrapper.cxx文件和compare_length.go文件。

> swig -c++ -go -intgosize 64 -cgo compare_length.i

運行上述命令後,在$GOPATH/compare_length/路徑下生成了連個文件compare_length_wrapper.cxx和compare_length.go文件。compare_length_wrapper.cxx文件將compare_length.i中指定的C++類型和接口導出爲C風格的接口和類型。compare_length.go文件在Go環境引用C接口,並將其封裝爲Go風格的接口。經過vim查看compare_length.go的內容,能夠看到import "C"。

extern _Bool _wrap_VecInt_isEmpty_compare_length_d0802815884ccdeb(uintptr_t arg1);
extern void _wrap_VecInt_clear_compare_length_d0802815884ccdeb(uintptr_t arg1);
extern void _wrap_VecInt_add_compare_length_d0802815884ccdeb(uintptr_t arg1, swig_intgo arg2);
extern swig_intgo _wrap_VecInt_get_compare_length_d0802815884ccdeb(uintptr_t arg1, swig_intgo arg2);
extern void _wrap_VecInt_set_compare_length_d0802815884ccdeb(uintptr_t arg1, swig_intgo arg2, swig_intgo arg3);
extern void _wrap_delete_VecInt_compare_length_d0802815884ccdeb(uintptr_t arg1);
extern swig_intgo _wrap_compare_compare_length_d0802815884ccdeb(uintptr_t arg1, uintptr_t arg2);
#undef intgo
*/
import "C"

這裏面導出的函數,都位於compare_length_wrapper.cxx文件,並且是SWIG自動生成的。

第三步,修改compare_length.go文件。添加對動態庫的連接參數。在import "C"以前的註釋部分添加這一句內容,#cgo LDFLAGS: -L${SRCDIR}/ -ll2。修改後的compare_length.go文件內容以下。

extern _Bool _wrap_VecInt_isEmpty_compare_length_d0802815884ccdeb(uintptr_t arg1);
extern void _wrap_VecInt_clear_compare_length_d0802815884ccdeb(uintptr_t arg1);
extern void _wrap_VecInt_add_compare_length_d0802815884ccdeb(uintptr_t arg1, swig_intgo arg2);
extern swig_intgo _wrap_VecInt_get_compare_length_d0802815884ccdeb(uintptr_t arg1, swig_intgo arg2);
extern void _wrap_VecInt_set_compare_length_d0802815884ccdeb(uintptr_t arg1, swig_intgo arg2, swig_intgo arg3);
extern void _wrap_delete_VecInt_compare_length_d0802815884ccdeb(uintptr_t arg1);
extern swig_intgo _wrap_compare_compare_length_d0802815884ccdeb(uintptr_t arg1, uintptr_t arg2);
#undef intgo
#cgo LDFLAGS: -L${SRCDIR}/ -ll2
*/
import "C"

這是本文第一部分講到的,添加Cgo指令。因爲咱們用到了動態庫,須要指定連接時的參數。

第四步,在$GOPATH/compare_length/路徑下運行go build。或者在任意位置運行go build compare_length。看到沒有報錯,就是build成功了。

4.3 測試使用

如今,GOPATH/compare_length/中的compare_length模塊已經和通常的Go模塊同樣,能夠被其餘Go代碼調用。咱們創建一個測試路徑GOPATH/compare_length_test/,在其中添加一個測試文件runme.go。

package main

import (
    "compare_length"
    "fmt"
)

func main() {
    l1 := compare_length.NewVecInt();
    l2 := compare_length.NewVecInt();
    l1.Add(1);
    l1.Add(2);
    l1.Add(3);

    l2.Add(1);
    l2.Add(2);
    l2.Add(4);
    ret := compare_length.Compare(l1, l2);
    fmt.Println(ret);
}

運行go build -o runme,能夠看到生成了可執行文件runme。而後在本地運行./runme,遇到報錯信息。

./runme: error while loading shared libraries: libl2.so: cannot open shared object file: No such file or directory

經過ldd runme看一下。能夠看到libl2.so未找到。

libl2.so => not found

因爲咱們用到了動態庫,所以要指定一下環境變量LD_LIBRARY_PATH。

export LD_LIBRARY_PATH=$GOPATH/src/compare_length/:$LD_LIBRARY_PATH
./rumme

能夠看到返回了正確內容。

-1

5 實際問題

個人實際問題,是用Go調用一個已有的NLP模塊,該模塊是用C++寫的。與上一節中的示例項目基本一致,只是連接的動態庫更多,導出的函數及類型更多。

以前這個模塊已經經過SWIG導出給Python,所以segment.i文件沒有作任何修改。

%module segment
%{
    #include "segment.h"
%}
%include "typemaps.i"
%include "std_string.i"
%include "std_vector.i"
%include "segment.h"

%template(VecDouble)    std::vector<double>;
%template(VecInt)       std::vector<int>;
%template(CoreSegmentItemVec) std::vector<CoreSegmentItem>;

int coreSegment(void* INOUT, const std::string& IN, std::vector<CoreSegmentItem>& OUT);
std::string postag2string(int wtype);
std::string t2sgchar(const std::string& IN, bool ifcase = true);
std::string sbc2dbc(const std::string& IN, bool ifcase = true);

運行SWIG命令,生成segment_wrapper.cxx和segment.go兩個文件。

swig -c++ -go -gointsize 64 -cgo segment.i

修改segment.go文件,添加連接參數,在import "C"以前的註釋裏添加。

#cgo LDFLAGS: -L${SRCDIR}/lib -lssplatform -lencoding -lCoreSegmentor

而後嘗試go build。沒有提示錯誤就是build成功。而後在$GOPATH下的另外一個目錄寫一段測試代碼。

package main

import (
        "github.com/terencezhou/segment"
        "github.com/axgle/mahonia"
        "fmt"
)

func main(){
        test_str := "中華人民共和國國家主席於今年10月對美國進行了訪問。";
        encoder_gbk := mahonia.NewEncoder("gbk")
        decoder_gbk := mahonia.NewDecoder("gbk")
        gbk_test_str := encoder_gbk.ConvertString(test_str)
        segment.Init();
        handler := segment.CreateCoreHandle();
        seg_res := segment.NewCoreSegmentItemVec();
        ret := segment.CoreSegment(handler, gbk_test_str, seg_res);
        fmt.Println(test_str);
        fmt.Printf("Segment status : %d\n", ret);
        for idx:=0; int64(idx) < seg_res.Size(); idx++{
                coreItem := seg_res.Get(idx);
                fmt.Println(decoder_gbk.ConvertString(coreItem.GetTxt()));
        }
}

能夠看到正確輸出了分詞。

中華人民共和國國家主席於今年10月對美國進行了訪問。
Segment status : 0
中華
人民
共和國
國家
主席
於
今年
10
月
對
美國
進行
了
訪問

6 總結及擴展閱讀

本文第二部分簡述了Go如何經過Cgo調用C接口,其餘細節能夠參考Cgo文檔。本文第二部分簡述了SWIG的使用,及SWIG的Go特性,詳細內容能夠參考SWIG文檔SWIG文檔中關於Go的部分。第三部分是一個示例工程,用來測試實際使用中用到的特性,l2.cpp的實如今下面給出,能夠手動生成libl2.so來進行測試,但測試編譯go模塊的時候,須要將l2.cpp從$GOPATH/src/compare_length/目錄中移出,否側Cgo會自動編譯它。第四部分是在實際工程中的使用。

#include <vector>
#include <math.h>

#include "l2.h"

using namespace std;

int l2(const vector<int>& elements)
{
    int sum = 0;
    for (vector<int>::const_iterator iter = elements.begin();
            iter != elements.end();
            iter++)
    {   
        sum += (*iter) * (*iter);
    }   
    float sq = sqrt(sum);
    int l2 = (int)sq;
    return l2; 
}

經過下面命令能夠生成用來測試的libl2.so。

g++ -g -o libl2.so -shared -fPIC -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H l2.cpp

原文出處:https://www.cnblogs.com/terencezhou/p/10059156.html