文章系列
【GO】Golang/C++混合編程 - SWIG
【GO】Golang/C++混合編程 - 初識(shí)
【GO】Golang/C++混合編程 - 入門
【GO】Golang/C++混合編程 - 基礎(chǔ)
【GO】Golang/C++混合編程 - 進(jìn)階一
【GO】Golang/C++混合編程 - 進(jìn)階二
【GO】Golang/C++混合編程 - 實(shí)戰(zhàn)
Golang/C++混合編程
C++類包裝
CGO 是 C 語(yǔ)言和 GO 語(yǔ)言之間的橋梁膀息,原則上無(wú)法直接支持 C++ 的類尼斧。CGO 不支持 C++ 語(yǔ)法的根本原因是 C++ 至今為止還沒(méi)有一個(gè)二進(jìn)制接口規(guī)范(ABI)皿哨。但是 C++ 是兼容 C 語(yǔ)言雄可,所以我們可以通過(guò)增加一組 C 語(yǔ)言函數(shù)接口作為 C++ 類和 CGO 之間的橋梁,這樣就可以間接地實(shí)現(xiàn) C++ 和 GO 之間的互聯(lián)澎埠。當(dāng)然瞬铸,因?yàn)?CGO 只支持 C 語(yǔ)言中值類型的數(shù)據(jù)類型台妆,所以我們是無(wú)法直接使用 C++ 的引用參數(shù)等特性的。
C++ 類到 Go 語(yǔ)言對(duì)象
實(shí)現(xiàn) C++ 類到 GO 語(yǔ)言對(duì)象的包裝需要經(jīng)過(guò)以下幾個(gè)步驟:首先是用純 C 函數(shù)接口包裝該 C++ 類拘荡;其次是通過(guò) CGO 將純 C 函數(shù)接口映射到 GO 函數(shù)臼节;最后是做一個(gè) GO 包裝對(duì)象,將 C++ 類到方法用 GO 對(duì)象的方法實(shí)現(xiàn)珊皿。
C++ 類
// my_buffer.h
#include <string>
// MyBuffer類實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的字符串緩沖區(qū)网缝,它有一個(gè)指定大小的構(gòu)造函數(shù),一個(gè)析構(gòu)函數(shù)蟋定,一個(gè)獲取緩沖區(qū)大小的函數(shù)和一個(gè)獲取緩沖區(qū)數(shù)據(jù)的函數(shù)粉臊。
struct MyBuffer {
std::string* s_;
MyBuffer(int size) {
this->s_ = new std::string(size, char('\0'));
}
~MyBuffer() {
delete this->s_;
}
int Size() const {
return this->s_->size();
}
char* Data() {
return (char*)this->s_->data();
}
};
// my_buffer.cpp
#include "my_buffer.h"
// use in c++
int main() {
auto pBuf = new MyBuffer(1024);
auto data = pBuf->Data();
auto size = pBuf->Size();
delete pBuf;
}
C 接口封裝
// my_buffer.c
#include "my_buffer.h"
// use in c
int main() {
MyBuffer* pBuf = NewMyBuffer(1024);
char* data = MyBuffer_Data(pBuf);
auto size = MyBuffer_Size(pBuf);
DeleteMyBuffer(pBuf);
}
// my_buffer_capi.h
typedef struct MyBuffer_T MyBuffer_T;
MyBuffer_T* NewMyBuffer(int size);
void DeleteMyBuffer(MyBuffer_T* p);
char* MyBuffer_Data(MyBuffer_T* p);
int MyBuffer_Size(MyBuffer_T* p);
// my_buffer_capi.cc
#include "./my_buffer.h"
extern "C" {
#include "./my_buffer_capi.h"
}
struct MyBuffer_T: MyBuffer {
MyBuffer_T(int size): MyBuffer(size) {}
~MyBuffer_T() {}
};
MyBuffer_T* NewMyBuffer(int size) {
auto p = new MyBuffer_T(size);
return p;
}
void DeleteMyBuffer(MyBuffer_T* p) {
delete p;
}
char* MyBuffer_Data(MyBuffer_T* p) {
return p->Data();
}
int MyBuffer_Size(MyBuffer_T* p) {
return p->Size();
}
其中
my_buffer_capi.h
是用于 CGO 的橋接文件,必須是采用 C 語(yǔ)言規(guī)范的名字修飾規(guī)則驶兜。在 C++ 源文件包含時(shí)需要用extern "C"
語(yǔ)句說(shuō)明扼仲。另外MyBuffer_T
的實(shí)現(xiàn)只是從MyBuffer
繼承的類,這樣可以簡(jiǎn)化包裝代碼的實(shí)現(xiàn)抄淑。同時(shí)和 CGO 通信時(shí)必須通過(guò)MyBuffer_T
指針屠凶,我們無(wú)法將具體的實(shí)現(xiàn)暴露給 CGO,因?yàn)閷?shí)現(xiàn)中包含了 C++ 特有的語(yǔ)法肆资,CGO 無(wú)法識(shí)別 C++ 特性矗愧。
C 接口函數(shù)轉(zhuǎn)為 GO 函數(shù)
// my_buffer_capi.go
package main
/*
#cgo CXXFLAGS: -std=c++11
#include "my_buffer_capi.h"
*/
import "C"
type cgo_MyBuffer_T C.MyBuffer_T
func cgo_NewMyBuffer(size int) *cgo_MyBuffer_T {
p := C.NewMyBuffer(C.int(size))
return (*cgo_MyBuffer_T)(p)
}
func cgo_DeleteMyBuffer(p *cgo_MyBuffer_T) {
C.DeleteMyBuffer((*C.MyBuffer_T)(p))
}
func cgo_MyBuffer_Data(p *cgo_MyBuffer_T) *C.char {
return C.MyBuffer_Data((*C.MyBuffer_T)(p))
}
func cgo_MyBuffer_Size(p *cgo_MyBuffer_T) C.int {
return C.MyBuffer_Size((*C.MyBuffer_T)(p))
}
其中
-std=c++11
是告訴編譯器使用 C++11 標(biāo)準(zhǔn),因?yàn)?C++11 才支持extern "C"
郑原,否則編譯器會(huì)認(rèn)為extern "C"
是無(wú)效的唉韭。為了區(qū)分夜涕,我們?cè)?GO 中的每個(gè)類型和函數(shù)名稱前面增加了
cgo_
前綴,比如cgo_MyBuffer_T
是對(duì)應(yīng) C 中的MyBuffer_T
類型属愤。為了處理簡(jiǎn)單女器,在包裝純 C 函數(shù)到 GO 函數(shù)時(shí),除了
cgo_MyBuffer_T
類型外春塌,對(duì)輸入?yún)?shù)和返回值的基礎(chǔ)類型晓避,我們依然是用的 C 語(yǔ)言的類型。
包裝為Go對(duì)象
在將純 C 接口包裝為 GO 函數(shù)之后只壳,我們就可以很容易地基于包裝的 GO 函數(shù)構(gòu)造出 GO 對(duì)象來(lái)俏拱。因?yàn)?code>cgo_MyBuffer_T是從 C 語(yǔ)言空間導(dǎo)入的類型,它無(wú)法定義自己的方法吼句,因此我們構(gòu)造了一個(gè)新的
MyBuffer
類型锅必,里面的成員持有cgo_MyBuffer_T
指向的 C 語(yǔ)言緩存對(duì)象。
// my_buffer.go
package main
import "unsafe"
type MyBuffer struct {
cptr *cgo_MyBuffer_T
}
func NewMyBuffer(size int) *MyBuffer {
return &MyBuffer{
cptr: cgo_NewMyBuffer(size),
}
}
func (p *MyBuffer) Delete() {
cgo_DeleteMyBuffer(p.cptr)
}
func (p *MyBuffer) Data() []byte {
data := cgo_MyBuffer_Data(p.cptr)
size := cgo_MyBuffer_Size(p.cptr)
return ((*[1 << 31]byte)(unsafe.Pointer(data)))[0:int(size):int(size)]
}
同時(shí)惕艳,因?yàn)?GO 語(yǔ)言的切片本身含有長(zhǎng)度信息搞隐,我們將
cgo_MyBuffer_Data
和cgo_MyBuffer_Size
兩個(gè)函數(shù)合并為MyBuffer.Data
方法,它返回一個(gè)對(duì)應(yīng)底層C語(yǔ)言緩存空間的切片远搪。
// main.go
package main
//#include <stdio.h>
import "C"
import "unsafe"
func main() {
buf := NewMyBuffer(1024)
defer buf.Delete()
copy(buf.Data(), []byte("hello\x00"))
C.puts((*C.char)(unsafe.Pointer(&(buf.Data()[0]))))
}
例子中劣纲,我們創(chuàng)建了一個(gè)1024字節(jié)大小的緩存,然后通過(guò)
copy
函數(shù)向緩存填充了一個(gè)字符串谁鳍。為了方便 C 語(yǔ)言字符串函數(shù)處理癞季,我們?cè)谔畛渥址哪J(rèn)用\0
表示字符串結(jié)束。最后我們直接獲取緩存的底層數(shù)據(jù)指針倘潜,用 C 語(yǔ)言的puts
函數(shù)打印緩存的內(nèi)容绷柒。
GO 語(yǔ)言對(duì)象到 C++ 類
要實(shí)現(xiàn) GO 語(yǔ)言對(duì)象到 C++ 類的包裝需要經(jīng)過(guò)以下幾個(gè)步驟:首先是將 GO 對(duì)象映射為一個(gè) id;然后基于 id 導(dǎo)出對(duì)應(yīng)的 C 接口函數(shù)涮因;最后是基于 C 接口函數(shù)包裝為 C++ 對(duì)象废睦。
一個(gè) GO 對(duì)象示例
package main
type Person struct {
name string
age int
}
func NewPerson(name string, age int) *Person {
return &Person{
name: name,
age: age,
}
}
func (p *Person) Set(name string, age int) {
p.name = name
p.age = age
}
func (p *Person) Get() (name string, age int) {
return p.name, p.age
}
映射為 C 接口
// person_capi.h
#include <stdint.h>
typedef uintptr_t person_handle_t;
person_handle_t person_new(char* name, int age);
void person_delete(person_handle_t p);
void person_set(person_handle_t p, char* name, int age);
char* person_get_name(person_handle_t p, char* buf, int size);
int person_get_age(person_handle_t p);
通過(guò) CGO 導(dǎo)出 C 函數(shù),輸入?yún)?shù)和返回值類型都不支持
const
修飾养泡,同時(shí)也不支持可變參數(shù)的函數(shù)類型嗜湃。
// person_capi.go
package main
//#include "./person_capi.h"
import "C"
import "unsafe"
//export person_new
func person_new(name *C.char, age C.int) C.person_handle_t {
id := NewObjectId(NewPerson(C.GoString(name), int(age)))
return C.person_handle_t(id)
}
//export person_delete
func person_delete(h C.person_handle_t) {
ObjectId(h).Free()
}
//export person_set
func person_set(h C.person_handle_t, name *C.char, age C.int) {
p := ObjectId(h).Get().(*Person)
p.Set(C.GoString(name), int(age))
}
//export person_get_name
func person_get_name(h C.person_handle_t, buf *C.char, size C.int) *C.char {
p := ObjectId(h).Get().(*Person)
name, _ := p.Get()
n := int(size) - 1
bufSlice := ((*[1 << 31]byte)(unsafe.Pointer(buf)))[0:n:n]
n = copy(bufSlice, []byte(name))
bufSlice[n] = 0
return buf
}
//export person_get_age
func person_get_age(h C.person_handle_t) C.int {
p := ObjectId(h).Get().(*Person)
_, age := p.Get()
return C.int(age)
}
在創(chuàng)建 GO 對(duì)象后,我們通過(guò)
NewObjectId
將 GO 對(duì)應(yīng)映射為 id澜掩。然后將 id 強(qiáng)制轉(zhuǎn)義為person_handle_t
類型返回净蚤。其它的接口函數(shù)則是根據(jù)person_handle_t
所表示的 id,讓根據(jù) id 解析出對(duì)應(yīng)的 GO 對(duì)象输硝。
封裝C++對(duì)象
有了 C 接口之后封裝 C++ 對(duì)象就比較簡(jiǎn)單了今瀑。常見(jiàn)的做法是新建一個(gè)
Person
類,里面包含一個(gè)person_handle_t
類型的成員對(duì)應(yīng)真實(shí)的 GO 對(duì)象,然后在Person
類的構(gòu)造函數(shù)中通過(guò) C 接口創(chuàng)建 GO 對(duì)象橘荠,在析構(gòu)函數(shù)中通過(guò) C 接口釋放 GO 對(duì)象屿附。
// person.h
extern "C" {
#include "./person_capi.h"
}
struct Person {
person_handle_t goobj_;
Person(const char* name, int age) {
this->goobj_ = person_new((char*)name, age);
}
~Person() {
person_delete(this->goobj_);
}
void Set(char* name, int age) {
person_set(this->goobj_, name, age);
}
char* GetName(char* buf, int size) {
return person_get_name(this->goobj_ buf, size);
}
int GetAge() {
return person_get_age(this->goobj_);
}
}
// person.cpp
#include "person.h"
#include <stdio.h>
int main() {
auto p = new Person("gopher", 10);
char buf[64];
char* name = p->GetName(buf, sizeof(buf)-1);
int age = p->GetAge();
printf("%s, %d years old.\n", name, age);
delete p;
return 0;
}
封裝C++對(duì)象改進(jìn)
在前面的封裝 C++ 對(duì)象的實(shí)現(xiàn)中,每次通過(guò)
new
創(chuàng)建一個(gè)Person
實(shí)例需要進(jìn)行兩次內(nèi)存分配:一次是針對(duì) C++ 版本的Person
哥童,再一次是針對(duì) GO 語(yǔ)言版本的Person
挺份。其實(shí) C++ 版本的Person
內(nèi)部只有一個(gè)person_handle_t
類型的 id,用于映射 GO 對(duì)象贮懈。我們完全可以將person_handle_t
直接當(dāng)中 C++ 對(duì)象來(lái)使用匀泊。
// person.h
extern "C" {
#include "./person_capi.h"
}
struct Person {
static Person* New(const char* name, int age) {
return (Person*)person_new((char*)name, age);
}
void Delete() {
person_delete(person_handle_t(this));
}
void Set(char* name, int age) {
person_set(person_handle_t(this), name, age);
}
char* GetName(char* buf, int size) {
return person_get_name(person_handle_t(this), buf, size);
}
int GetAge() {
return person_get_age(person_handle_t(this));
}
};
我們?cè)?code>Person類中增加了一個(gè)叫
New
靜態(tài)成員函數(shù),用于創(chuàng)建新的Person
實(shí)例朵你。在New
函數(shù)中通過(guò)調(diào)用person_new
來(lái)創(chuàng)建Person
實(shí)例各聘,返回的是person_handle_t
類型的 id,我們將其強(qiáng)制轉(zhuǎn)型作為Person*
類型指針?lè)祷芈找健T谄渌某蓡T函數(shù)中躲因,我們通過(guò)將this
指針再反向轉(zhuǎn)型為person_handle_t
類型,然后通過(guò) C 接口調(diào)用對(duì)應(yīng)的函數(shù)忌傻。
靜態(tài)庫(kù)/動(dòng)態(tài)庫(kù)
CGO 在使用 C/C++ 資源的時(shí)候一般有三種形式:直接使用源碼大脉;鏈接靜態(tài)庫(kù);鏈接動(dòng)態(tài)庫(kù)水孩。直接使用源碼就是在
import "C"
之前的注釋部分包含 C 代碼镰矿,或者在當(dāng)前包中包含 C/C++ 源文件。鏈接靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的方式比較類似俘种,都是通過(guò)在LDFLAGS
選項(xiàng)指定要鏈接的庫(kù)方式鏈接衡怀。
使用 C 靜態(tài)庫(kù)
構(gòu)造一個(gè) C 靜態(tài)庫(kù)
// number/number.h
int number_add_mod(int a, int b, int mod);
// number/number.c
#include "number.h"
int number_add_mod(int a, int b, int mod) {
return (a+b)%mod;
}
cd ./number
gcc -c -o number.o number.c
ar rcs libnumber.a number.o
// main.go
package main
//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR}/number -lnumber
//
//#include "number.h"
import "C"
import "fmt"
func main() {
fmt.Println(C.number_add_mod(10, 5, 12))
}
其中有兩個(gè)
#cgo
命令,分別是編譯和鏈接參數(shù)安疗。CFLAGS
通過(guò)-I./number
將number
庫(kù)對(duì)應(yīng)頭文件所在的目錄加入頭文件檢索路徑。LDFLAGS
通過(guò)-L${SRCDIR}/number
將編譯后number
靜態(tài)庫(kù)所在目錄加為鏈接庫(kù)檢索路徑够委,-lnumber
表示鏈接libnumber.a
靜態(tài)庫(kù)荐类。需要注意的是,在鏈接部分的檢索路徑不能使用相對(duì)路徑(C/C++代碼的鏈接程序所限制)茁帽,我們必須通過(guò) CGO 特有的${SRCDIR}
變量將源文件對(duì)應(yīng)的當(dāng)前目錄路徑展開為絕對(duì)路徑玉罐。因?yàn)槲覀冇?code>number庫(kù)的全部代碼,所以我們可以用
go generate
工具來(lái)生成靜態(tài)庫(kù)潘拨,或者是通過(guò)Makefile
來(lái)構(gòu)建靜態(tài)庫(kù)吊输。因此發(fā)布 CGO 源碼包時(shí),我們并不需要提前構(gòu)建 C 靜態(tài)庫(kù)铁追。因?yàn)槎嗔艘粋€(gè)靜態(tài)庫(kù)的構(gòu)建步驟季蚂,這種使用了自定義靜態(tài)庫(kù)并已經(jīng)包含了靜態(tài)庫(kù)全部代碼的 GO 包無(wú)法直接用
go get
安裝。不過(guò)我們依然可以通過(guò)go get
下載,然后用go generate
觸發(fā)靜態(tài)庫(kù)構(gòu)建扭屁,最后才是go install
來(lái)完成安裝算谈。
使用 C 動(dòng)態(tài)庫(kù)
cd number
gcc -shared -o libnumber.so number.c
package main
//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR}/number -lnumber
//
//#include "number.h"
import "C"
import "fmt"
func main() {
fmt.Println(C.number_add_mod(10, 5, 12))
}
編譯時(shí) GCC 會(huì)自動(dòng)找到
libnumber.a
或libnumber.so
進(jìn)行鏈接。需要注意的是料滥,在運(yùn)行時(shí)需要將動(dòng)態(tài)庫(kù)放到系統(tǒng)能夠找到的位置然眼。
導(dǎo)出 C 靜態(tài)庫(kù)
// number.go
package main
import "C"
func main() {}
//export number_add_mod
func number_add_mod(a, b, mod C.int) C.int {
return (a + b) % mod
}
根據(jù) CGO 文檔的要求,我們需要在
main
包中導(dǎo)出 C 函數(shù)葵腹。對(duì)于 C 靜態(tài)庫(kù)構(gòu)建方式來(lái)說(shuō)高每,會(huì)忽略main
包中的main
函數(shù),只是簡(jiǎn)單導(dǎo)出 C 函數(shù)践宴。
go build -buildmode=c-archive -o number.a
在生成
number.a
靜態(tài)庫(kù)的同時(shí)鲸匿,CGO 還會(huì)生成一個(gè)number.h
文件。
// number.h
#ifdef __cplusplus
extern "C" {
#endif
extern int number_add_mod(int p0, int p1, int p2);
#ifdef __cplusplus
}
#endif
// main.c
#include "number.h"
#include <stdio.h>
int main() {
int a = 10;
int b = 5;
int c = 12;
int x = number_add_mod(a, b, c);
printf("(%d+%d)%%%d = %d\n", a, b, c, x);
return 0;
}
gcc -o a.out _test_main.c number.a
./a.out
導(dǎo)出 C 動(dòng)態(tài)庫(kù)
go build -buildmode=c-shared -o number.so
gcc -o a.out _test_main.c number.so
./a.out
GO 函數(shù)回調(diào)注冊(cè)
- 1浴井、通過(guò)export將Go函數(shù)聲明導(dǎo)出函數(shù)晒骇,Go函數(shù)要與被C回調(diào)的函數(shù)原型保持一致;
- 2磺浙、將回調(diào)函數(shù)轉(zhuǎn)換為C的函數(shù)指針洪囤,傳給C函數(shù)庫(kù),等待觸發(fā)調(diào)用撕氧;
- 3瘤缩、回調(diào)函數(shù)被觸發(fā),能在Go訪問(wèn)到C的內(nèi)存伦泥;
//main.go
package main
/*
#cgo LDFLAGS: -L${SRCDIR}/lib -lcallback
#cgo CFLAGS: -I callback
#include "callback.h"
int goFuncForCallback(struct info *, char *);
*/
import "C"
import "fmt"
func main(){
C.setcallback(C.callbackFuncProto(C.goFuncForCallback))
C.caller()
C.freeObject()
}
//導(dǎo)出為C函數(shù)
//export goFuncForCallback
func goFuncForCallback(info *C.struct_info, roomId *C.char) C.int{
fmt.Println("goFunc", info.size, C.GoString(roomId))
return 1
}
// callback/callback.h
#ifndef __TEST_H__
#define __TEST_H__
#ifdef __cplusplus
extern "C"{
#endif
typedef struct info{
void* a;
int size;
}CInfo;
//C函數(shù)指針剥啤,函數(shù)原型一致
typedef int(*callbackFuncProto) (CInfo* n, char *roomId);
//接收C的函數(shù)指針,用于被C回調(diào)
int setcallback(callbackFuncProto s);
//回調(diào)函數(shù)觸發(fā)器
void caller();
void freeObject();
#ifdef __cplusplus
}
#endif
#endif
// callback/callback.c
#include "callback.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
callbackFuncProto callback;
CInfo info;
int setcallback(callbackFuncProto foo){
callback = foo;
info.a = malloc(3);
info.size = 3;
char t[3] = "c";
memcpy(info.a, t, 3);
return 1;
}
void caller(){
int r = callback(&info, (char *)" call from C func");
printf("---%d", r);
}
void freeObject(){
free(info.a);
}