這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
工作上遇到一個需求,需要把一個C++的動態庫的功能封裝為Web介面。由於沒有C++開發經驗,C有點經驗,於是考慮了兩種方案:
- 封裝為PHP擴充
- 在Golang中使用CGO
兩種方案我都可以做,但最終決定採用第2種方案,主要考慮的因素是這個Web服務最終需要在客戶那裡進行私人化部署,採用PHP的話,部署的時候還需要Nginx、Fpm(當然也可以直接用Swoole),但是PHP代碼是明文的,雖然可以買一些商業軟體進行加密(比如Swoole Compiler)。如果直接用Golang的話,就可以直接給使用者部署一個二進位程式(需要strip掉符號資訊)就可以了,部署起來更方便。
下面將通過一個樣本程式,示範如何在Golang中通過cgo調用C++。
範例程式碼目錄:
.├── bin│ └── cgo└── src └── cgo ├── c_src.cpp // 在Golang中調用的C函數定義 ├── c_src.h // C標頭檔,聲明了哪些C函數會在Golang中使用,在main.go中包含 ├── main.go ├── src.cpp // C++代碼 └── src.hpp // C++標頭檔
c_src.h 源碼:
#ifndef WRAP_CPP_H#define WRAP_CPP_H#ifdef __cplusplusextern "C" {#endif // __cplusplustypedef void* Foo;Foo FooNew();void FooDestroy(Foo f);const char* FooGetName(Foo f, int* retLen);void FooSetName(Foo f, char* name);#ifdef __cplusplus}#endif // __cplusplus#endif // WRAP_CPP_H
extern "C"作用:Combining C++ and C - how does #ifdef __cplusplus work?
c_src.cpp 源碼:
#include "src.hpp"#include "c_src.h"#include <cstring>// 返回cxxFoo對象,但轉換為void*Foo FooNew(){ cxxFoo* ret = new cxxFoo("rokety"); return (void*)ret;}void FooDestroy(Foo f){ cxxFoo* foo = (cxxFoo*)f; delete foo;}// 封裝cxxFoo的get_name方法const char* FooGetName(Foo f, int* ret_len){ cxxFoo* foo = (cxxFoo*)f; std::string name = foo->get_name(); *ret_len = name.length(); const char* ret_str = (const char*)malloc(*ret_len); memcpy((void*)ret_str, name.c_str(), *ret_len); return ret_str;}// 封裝cxxFoo的set_name方法void FooSetName(Foo f, char* name){ cxxFoo* foo = (cxxFoo*)f; std::string _name(name, strlen(name)); foo->set_name(_name);}
c_src.cpp 可能的疑問:
- 為何需要定義Foo?因為在C中沒有Class的概念,所以需要把C++的Class轉換為C中的資料類型
- 為何在FooGetName中需要進行malloc和memcpy?因為name是局部變數,並且記憶體配置在棧上,當cgo調用返回後,name所佔用的記憶體會被釋放掉。
main.go 源碼:
package main// #include "c_src.h"// #include <stdlib.h>import "C"import ("fmt""unsafe")type GoFoo struct {foo C.Foo}func NewGoFoo() GoFoo {var ret GoFooret.foo = C.FooNew()return ret}func (f GoFoo) Destroy() {C.FooDestroy(f.foo)}func (f GoFoo) GetName() string {rLen := C.int(0)name := C.FooGetName(f.foo, &rLen)defer C.free(unsafe.Pointer(name)) // 必須使用C的free函數,釋放FooGetName中malloc的記憶體return C.GoStringN(name, rLen) // 從name構造出golang的string類型值}func (f GoFoo) SetName(name string) {cname := C.CString(name) // 將golang的string類型值轉換為c中的char*類型值,這裡會調用到c的mallocC.FooSetName(f.foo, cname)C.free(unsafe.Pointer(cname)) // 釋放上面malloc的記憶體}func main() {foo := NewGoFoo()fmt.Println(foo.GetName())foo.GetName()foo.SetName("new rokety")fmt.Println(foo.GetName())foo.Destroy()}
main.go 可能的疑問:
- unsafe.Pointer(…)相當於把變數強轉為C中的void*類型
- SetName中為何需要做轉換,因為name變數的記憶體是在Golang中分配的,且string類型是不可修改的,因此,需要在c中分配name所需要的記憶體,以便在FooSetName中使用
- 需要注意的一點是
import "C"上面必須緊跟// #include ...注釋
src.hpp 源碼:
#ifndef CXX_H#define CXX_H#include <string>class cxxFoo{public: cxxFoo(std::string name); ~cxxFoo(); std::string get_name(); void set_name(std::string name);private: std::string name;};#endif // CXX_H
src.cpp 源碼
#include "src.hpp"#include <iostream>cxxFoo::cxxFoo(std::string name){ this->name = name;}cxxFoo::~cxxFoo(){}std::string cxxFoo::get_name(){ return this->name;}void cxxFoo::set_name(std::string name){ this->name = name;}
小結:
- C中的資料類型會與Golang的C.xxx資料類型對應:CGO 類型(CGO Types)
- 在C/C++中申請的記憶體,就得在C/C++中釋放
- 對於需要連結C/C++動態庫,或加上編譯參數,可以在
import "C"加上對應注釋// #cgo CFLAGS: -DPNG_DEBUG=1
參考資料:
- How to use C++ in Go?
- Command cgo
- C? Go? Cgo!
- Golang CGO編程之調用返回char*指標及長度的C函數庫
- CGO: Go與C互操作技術(一):Go調C基本原理