【GO】Golang/C++混合编程 - 进阶二

文章系列
【GO】Golang/C++混合编程 - SWIG
【GO】Golang/C++混合编程 - 初识
【GO】Golang/C++混合编程 - 入门
【GO】Golang/C++混合编程 - 基础
【GO】Golang/C++混合编程 - 进阶一
【GO】Golang/C++混合编程 - 进阶二
【GO】Golang/C++混合编程 - 实战

Golang/C++混合编程

C++类包装

CGO 是 C 语言和 GO 语言之间的桥梁,原则上无法直接支持 C++ 的类。CGO 不支持 C++ 语法的根本原因是 C++ 至今为止还没有一个二进制接口规范(ABI)。但是 C++ 是兼容 C 语言,所以我们可以通过增加一组 C 语言函数接口作为 C++ 类和 CGO 之间的桥梁,这样就可以间接地实现 C++ 和 GO 之间的互联。当然,因为 CGO 只支持 C 语言中值类型的数据类型,所以我们是无法直接使用 C++ 的引用参数等特性的。

C++ 类到 Go 语言对象

实现 C++ 类到 GO 语言对象的包装需要经过以下几个步骤:首先是用纯 C 函数接口包装该 C++ 类;其次是通过 CGO 将纯 C 函数接口映射到 GO 函数;最后是做一个 GO 包装对象,将 C++ 类到方法用 GO 对象的方法实现。

C++ 类

// my_buffer.h
#include <string>

// MyBuffer类实现了一个简单的字符串缓冲区,它有一个指定大小的构造函数,一个析构函数,一个获取缓冲区大小的函数和一个获取缓冲区数据的函数。
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 语言规范的名字修饰规则。在 C++ 源文件包含时需要用extern "C"语句说明。另外MyBuffer_T的实现只是从MyBuffer继承的类,这样可以简化包装代码的实现。同时和 CGO 通信时必须通过MyBuffer_T指针,我们无法将具体的实现暴露给 CGO,因为实现中包含了 C++ 特有的语法,CGO 无法识别 C++ 特性。

C 接口函数转为 GO 函数

// 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 标准,因为 C++11 才支持extern "C",否则编译器会认为extern "C"是无效的。

为了区分,我们在 GO 中的每个类型和函数名称前面增加了cgo_前缀,比如cgo_MyBuffer_T是对应 C 中的MyBuffer_T类型。

为了处理简单,在包装纯 C 函数到 GO 函数时,除了cgo_MyBuffer_T类型外,对输入参数和返回值的基础类型,我们依然是用的 C 语言的类型。

包装为Go对象

在将纯 C 接口包装为 GO 函数之后,我们就可以很容易地基于包装的 GO 函数构造出 GO 对象来。因为cgo_MyBuffer_T是从 C 语言空间导入的类型,它无法定义自己的方法,因此我们构造了一个新的MyBuffer类型,里面的成员持有cgo_MyBuffer_T指向的 C 语言缓存对象。

// 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)]
}

同时,因为 GO 语言的切片本身含有长度信息,我们将cgo_MyBuffer_Datacgo_MyBuffer_Size两个函数合并为MyBuffer.Data方法,它返回一个对应底层C语言缓存空间的切片。

// 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]))))
}

例子中,我们创建了一个1024字节大小的缓存,然后通过copy函数向缓存填充了一个字符串。为了方便 C 语言字符串函数处理,我们在填充字符串的默认用\0表示字符串结束。最后我们直接获取缓存的底层数据指针,用 C 语言的puts函数打印缓存的内容。

GO 语言对象到 C++ 类

要实现 GO 语言对象到 C++ 类的包装需要经过以下几个步骤:首先是将 GO 对象映射为一个 id;然后基于 id 导出对应的 C 接口函数;最后是基于 C 接口函数包装为 C++ 对象。

一个 GO 对象示例

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);

通过 CGO 导出 C 函数,输入参数和返回值类型都不支持const修饰,同时也不支持可变参数的函数类型。

// 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)
}

在创建 GO 对象后,我们通过NewObjectId将 GO 对应映射为 id。然后将 id 强制转义为person_handle_t类型返回。其它的接口函数则是根据person_handle_t所表示的 id,让根据 id 解析出对应的 GO 对象。

封装C++对象

有了 C 接口之后封装 C++ 对象就比较简单了。常见的做法是新建一个Person类,里面包含一个person_handle_t类型的成员对应真实的 GO 对象,然后在Person类的构造函数中通过 C 接口创建 GO 对象,在析构函数中通过 C 接口释放 GO 对象。

// 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++对象改进

在前面的封装 C++ 对象的实现中,每次通过new创建一个Person实例需要进行两次内存分配:一次是针对 C++ 版本的Person,再一次是针对 GO 语言版本的Person。其实 C++ 版本的Person内部只有一个person_handle_t类型的 id,用于映射 GO 对象。我们完全可以将person_handle_t直接当中 C++ 对象来使用。

// 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));
    }
};

我们在Person类中增加了一个叫New静态成员函数,用于创建新的Person实例。在New函数中通过调用person_new来创建Person实例,返回的是person_handle_t类型的 id,我们将其强制转型作为Person*类型指针返回。在其它的成员函数中,我们通过将this指针再反向转型为person_handle_t类型,然后通过 C 接口调用对应的函数。

静态库/动态库

CGO 在使用 C/C++ 资源的时候一般有三种形式:直接使用源码;链接静态库;链接动态库。直接使用源码就是在import "C"之前的注释部分包含 C 代码,或者在当前包中包含 C/C++ 源文件。链接静态库和动态库的方式比较类似,都是通过在LDFLAGS选项指定要链接的库方式链接。

使用 C 静态库

构造一个 C 静态库

// 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))
}

其中有两个#cgo命令,分别是编译和链接参数。CFLAGS通过-I./numbernumber库对应头文件所在的目录加入头文件检索路径。LDFLAGS通过-L${SRCDIR}/number将编译后number静态库所在目录加为链接库检索路径,-lnumber表示链接libnumber.a静态库。需要注意的是,在链接部分的检索路径不能使用相对路径(C/C++代码的链接程序所限制),我们必须通过 CGO 特有的${SRCDIR}变量将源文件对应的当前目录路径展开为绝对路径。

因为我们有number库的全部代码,所以我们可以用go generate工具来生成静态库,或者是通过Makefile来构建静态库。因此发布 CGO 源码包时,我们并不需要提前构建 C 静态库。

因为多了一个静态库的构建步骤,这种使用了自定义静态库并已经包含了静态库全部代码的 GO 包无法直接用go get安装。不过我们依然可以通过go get下载,然后用go generate触发静态库构建,最后才是go install来完成安装。

使用 C 动态库

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))
}

编译时 GCC 会自动找到libnumber.alibnumber.so进行链接。

需要注意的是,在运行时需要将动态库放到系统能够找到的位置。

导出 C 静态库

// 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
}

根据 CGO 文档的要求,我们需要在main包中导出 C 函数。对于 C 静态库构建方式来说,会忽略main包中的main函数,只是简单导出 C 函数。

go build -buildmode=c-archive -o number.a

在生成number.a静态库的同时,CGO 还会生成一个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

导出 C 动态库

go build -buildmode=c-shared -o number.so

gcc -o a.out _test_main.c number.so
./a.out

GO 函数回调注册

  • 1、通过export将Go函数声明导出函数,Go函数要与被C回调的函数原型保持一致;
  • 2、将回调函数转换为C的函数指针,传给C函数库,等待触发调用;
  • 3、回调函数被触发,能在Go访问到C的内存;
//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()
}

//导出为C函数
//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函数指针,函数原型一致
typedef int(*callbackFuncProto) (CInfo* n, char *roomId);

//接收C的函数指针,用于被C回调
int setcallback(callbackFuncProto s);

//回调函数触发器
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);
}

参考链接

  1. cgo链接C库,向C库传入Go函数作为回调
  2. 使用 cgo 将函数指针传递给 C 代码
  3. cgo 将goLang函数作为回调函数传递给C的简单示例
  4. 如何在Golang裡面實作Callback function讓C來呼叫
  5. CGO中处理C中的回调函数
  6. GO语言高级编程
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容