Golang 中通过 cgo 调用 C++ 的动态库的功能封装


原文链接: Golang 中通过 cgo 调用 C++ 的动态库的功能封装

将C++warpper 文件写在go中 https://github.com/winlinvip/go-fdkaac/blob/master/fdkaac/dec.go
https://github.com/giorgisio/goav/blob/master/avfilter/avfilter.go

原文地址 http://yangxikun.com/golang/2018/03/09/golang-cgo.html

Examples of calls between Go and C/C++
Golang 中通过 cgo 调用 C++ 的动态库的功能封装
下面将通过一个示例程序,演示如何在 Golang 中通过 cgo 调用 C++。

extern "C" 的作用:

  1. extern "C" 只对c++编译器有效。使用场景是: 在 .cpp 文件中需要调用C函数时。
  2. extern "C" 告诉C++编译器,在编译和连接的时候使用C方式的函数声明
    在c++中,为了支持重载机制,在编译生成的汇编码中,要对函数的名字进行一些处理,加入比如函数的返回类型等等.而在C中,只是简单的函数名字而已
    例如: int f(void)
    C++ 编译器生成的函数名: _Z1fv()
    C 编译器生成的函数名: f()

示例代码目录:

.
├── bin
│   └── cgo
└── src
    └── cgo
        ├── c_src.cpp   // 在Golang中调用的C函数定义    使用C++ 编译器编译成`extern  "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 __cplusplus
extern "C" {
#endif // __cplusplus

typedef 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 warpper 源码:

#include "src.hpp"
#include "c_src.h"
#include <cstring>

// 返回c++ Foo对象,但转换为C的 void*
Foo FooNew()
{
    cxxFoo* ret = new cxxFoo("rokety");   // 创建 c++ 对象,并返回 void * 类型
    return (void*)ret;
}

void FooDestroy(Foo f)
{
    cxxFoo* foo = (cxxFoo*)f;             // 把 void * 强转回 c++ 类型并释放。
    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 GoFoo
	ret.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的malloc
	C.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

参考资料:

`