什么是cgo

C/C++经过几十年的发展,已经积累了庞大的软件资产,它们很多久经考验而且性能已经足够优化。Go语言必须能够站在C/C++这个巨人的肩膀之上,有了海量的C/C++软件资产兜底之后,我们才可以放心愉快地用Go语言编程。C语言作为一个通用语言,很多库会选择提供一个C兼容的API,然后用其他不同的编程语言实现。Go语言通过自带的一个叫CGO的工具来支持C语言函数调用,同时我们可以用Go语言导出C动态库接口给其它语言使用。本章主要讨论CGO编程中涉及的一些问题。

案例

hello word

调用C语言标准io打印字符串

  1. package main
  2. //#include <stdio.h>
  3. import "C"
  4. func main() {
  5. C.puts(C.CString("Hello, World\n"))
  6. }
go run main.go

简单调用自己的C语言函数

package main

/*
#include <stdio.h>

static void SayHello(const char* s) {
    puts(s);
}
*/
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

调用一个C语言源文件的函数

将SayHello函数放到当前目录下的一个C语言源文件中(后缀名必须是.c)。因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的static修饰符

#include <stdio.h>

void SayHello(const char* s) {
    puts(s);
}
package main

//void SayHello(const char* s);
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

注意,如果之前运行的命令是go run hello.go或go build hello.go的话,此处须使用go run “your/package”或go build “your/package”才可以。若本就在包路径下的话,也可以直接运行go run .或go build
image.png

调用静态库或者动态库

int number_add(int a, int b);
#include "number.h"

int number_add(int a, int b) {
    return a+b;
}

生成静态库

$ pushd number
$ gcc -c -o number.o number.c
$ ar rcs libnumber.a number.o
$ popd

cgo代码

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(11,10))
}

编译运行

go run .

用cgo 重写C函数

现在我们创建一个hello.go文件,用Go语言重新实现C语言接口的SayHello函数:

void SayHello(/*const*/ char* s);

通过CGO的//export SayHello指令将Go语言实现的函数SayHello导出为C语言函数。为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。需要注意的是,这里其实有两个版本的SayHello函数:一个Go语言环境的;另一个是C语言环境的。cgo生成的C语言版本SayHello函数最终会通过桥接代码调用Go语言版本的SayHello函数。

package main

import "C"

import "fmt"

//export SayHello
func SayHello(s *C.char) {
    fmt.Printf("go::SayHello() %s",C.GoString(s))
}
package main

//#include <hello.h>
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

image.png

go编译生成兼容C标准的so

package main

import "C"

//export add
func add(x, y int) int {
    return x + y
}

//export remove_int
func remove_int(x, y int) int {
    return x - y
}

func main() {
}

/*
这里有几点要注意

    package 一定要是 main(强制规定)
    一定要包含 main 函数(强制规定)
    import “C”, 不能少, 因为要编译出 c(c++)的头文件
    每个方法前要加//export 方法名, 这里要注意
    // 和 export间不能有空格
    方法名和 go 的方法名必须完全一样
    方法名不能是 c 内置的方法名, 比如remove就不行
    方法名需要和导出名一样,不受Golang大小写控制作用域
    方法的入参出参数据类型遵循类型映射规则,如使用string做入参, C语言代码就需要传入GoString,
        GoString str = {"Hi JXES", 7}; 还不如用 *C.char 作为参数

    编译命令:
    go build -buildmode=c-shared -o libdemo.so libdemo.go
    编译成功会生成 libdemo.so 和 libdemo.h
*/

用C来调用

#include<stdio.h>
#include"libdemo.h" //生成的头文件

void main(){
    printf("\n2+3=%lld\n",add(2,3));

}

/*
gcc goso.c  -L ./ -ldemo -o goso
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)
./goso
*/

image.png
image.png

cgo 核心

预编译注释

在import “C”语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。

// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"
  • CFLAGS部分
    • -D部分定义了宏PNG_DEBUG,值为1;
    • -I定义了头文件包含的检索目录;
  • DFLAGS部分
    • -L指定了链接时库文件检索目录;
    • -l指定了链接时需要链接png库;

因为C/C++遗留的问题,C头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。在库文件的检索目录中可以通过${SRCDIR}变量表示当前包目录的绝对路径:

// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo

上面的代码在链接时将被展开为:

// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo

cgo语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。

  • LDFLAGS用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数(CFLAGS用于针对C语言代码设置编译参数)。
  • 对于在cgo环境混合使用C和C++的用户来说,可能有三种不同的编译选项:
    • 其中CFLAGS对应C语言特有的编译选项、CXXFLAGS对应是C++特有的编译选项、CPPFLAGS则对应C和C++共有的编译选项。
    • 但是在链接阶段,C和C++的链接选项是通用的,因此这个时候已经不再有C和C++语言的区别,它们的目标文件的类型是相同的。

cgo指令还支持条件选择,当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。比如下面是分别针对windows和非windows下平台的编译和链接选项:

// #cgo windows CFLAGS: -DX86=1 
// #cgo !windows LDFLAGS: -lm

其中在windows平台下,编译前会预定义X86宏为1;在非widnows平台下,在链接阶段会要求链接math数学库。这种用法对于在不同平台下只有少数编译选项差异的场景比较适用。
如果在不同的系统下cgo对应着不同的c代码,我们可以先使用#cgo指令定义不同的C语言的宏,然后通过宏来区分不同的代码:

package main

/*
#cgo windows CFLAGS: -DCGO_OS_WINDOWS=1
#cgo darwin CFLAGS: -DCGO_OS_DARWIN=1
#cgo linux CFLAGS: -DCGO_OS_LINUX=1

#if defined(CGO_OS_WINDOWS)
    const char* os = "windows";
#elif defined(CGO_OS_DARWIN)
    static const char* os = "darwin";
#elif defined(CGO_OS_LINUX)
    static const char* os = "linux";
#else
#    error(unknown os)
#endif
*/
import "C"

func main() {
    print(C.GoString(C.os))
}

这样我们就可以用C语言中常用的技术来处理不同平台之间的差异代码。

golang 预编译参数

// +build debug

package main

var buildMode = "debug"

使用如下命令构建

go build -tags="debug"
go build -tags="windows debug"

我们可以通过-tags命令行参数同时指定多个build标志,它们之间用空格分隔。
当有多个build tag时,我们将多个标志通过逻辑操作的规则来组合使用。比如以下的构建标志表示只有在”linux/386“或”darwin平台下非cgo环境“才进行构建。

// +build linux,386 darwin,!cgo

其中linux,386中linux和386用逗号链接表示AND的意思;而linux,386和darwin,!cgo之间通过空白分割来表示OR的意思。

类型映射

详见 这篇文章

C语言类型 CGO类型 Go语言类型 C语言类型 CGO类型 Go语言类型
char C.char byte float C.float float32
singed char C.schar int8 double C.double float64
unsigned char C.uchar uint8 size_t C.size_t uint
short C.short int16 int8_t C.int8_t int8
unsigned short C.ushort uint16 uint8_t C.uint8_t uint8
int C.int int32 int16_t C.int16_t int16
unsigned int C.uint uint32 uint16_t C.uint16_t uint16
long C.long int32 int32_t C.int32_t int32
unsigned long C.ulong uint32 uint32_t C.uint32_t uint32
long long int C.longlong int64 int64_t C.int64_t int64
unsigned long long int C.ulonglong uint64 uint64_t C.uint64_t uint64

虽然在C语言中int、short等类型没有明确定义内存大小,但是在CGO中它们的内存大小是确定的

结构体

/*
struct A {
    int i;
    float f;
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a.i) // 引用结构体成员
    fmt.Println(a.f) 
}
func CArrayToGoArray(cArray unsafe.Pointer, size int) (goArray []uint64) {
    p := uintptr(cArray)
    for i :=0; i < size; i++ {
        j := *(*uint64)(unsafe.Pointer(p))
        goArray = append(goArray, j)
        p += unsafe.Sizeof(j)
    }
    return
}

CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

Go 中切片的使用方法类似 C 中的数组,但是内存结构并不一样。C 中的数组实际上指的是一段连续的内存,而 Go 的切片在存储数据的连续内存基础上,还有一个头结构体,其内存结构如下
image.png
因此 Go 的切片不能直接传递给 C 使用,而是需要取切片的内部缓冲区的首地址(即首个元素的地址)来传递给 C 使用。使用这种方式把 Go 的内存空间暴露给 C 使用,可以大大减少 Go 和 C 之间参数传递时内存拷贝的消耗。

package main

/*
int SayHello(char* buff, int len) {
    char hello[] = "Hello Cgo!";
    int movnum = len < sizeof(hello) ? len:sizeof(hello);
    memcpy(buff, hello, movnum); // go字符串没有'\0',所以直接内存拷贝, 而不是strcpy()
    return movnum;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    buff := make([]byte, 8)
    C.SayHello((*C.char)(unsafe.Pointer(&buff[0])), C.int(len(buff)))
    a := string(buff)
    fmt.Println(a)
}
package main

//#cgo CFLAGS: -I./
//#cgo LDFLAGS: -L./ -ldemo
//#include <demo.h>
//#include <string.h>
import "C"
import (
    "log"
    "unsafe"
)

func main() {
    var info = C.ai_string_t(C.CString(""))
    rcode := C.ai_get_engine_info(&info)
    log.Println(C.GoString(info))
    log.Println(rcode)
    log.Println(info)

    var info2 C.ai_string_t
    rcode = C.ai_get_engine_info(&info2)
    log.Println(C.GoString(info2))
    log.Println(rcode)

    log.Println(info2)
    var str = ""
    strSlice := (*String)(unsafe.Pointer(&str))
    strSlice.Array = unsafe.Pointer(info2)
    strSlice.Len = int(C.strlen(info2))
    log.Println(str)
}

type slice struct {
    Array unsafe.Pointer
    Len   int
    Cap   int
}

type String struct {
    Array unsafe.Pointer
    Len   int
}

/*
go build demo.go
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)
./demo
*/

image.png

cgo开发通常进行的是类型、方法绑定(bindings)工作,如
https://github.com/krig/go-sox
https://github.com/u2takey/ffmpeg-go
使得开发者调用go的库不必关系so或者c的细节,完成了指针的操作

如下示例:

/*
struct A {
    int i;
    float f;
};

int printA(A a);
int getA(A *a);
*/
import "C"
import "fmt"

type StructA C.struct_A

func PrintA(a StructA) {
    C.printA(C.struct_A(unsafe.Pointer(a)))
}
func GetA() StructA {
    var out C.struct_A{}
    C.getA(&out)
    return *((*StructA)(unsafe.Pointer(&out)))
}

动态库和静态库

CGO在使用C/C++资源的时候一般有三种形式:

  • 直接使用源码;
  • 链接静态库;
  • 链接动态库。

直接使用源码就是在import “C”之前的注释部分包含C代码,或者在当前包中包含C/C++源文件。
链接静态库和动态库的方式比较类似,都是通过在LDFLAGS选项指定要链接的库方式链接。

技巧

防止内存泄漏

package main

/*
#include <stdio.h>
#include <stdlib.h> 

void say(const char *s) {
    puts(s);
}
*/
import "C"
import "unsafe"

func main() {
    for {
        hello()
    }
}

func hello() {
    s := C.CString("Hello, World\n")
    defer C.free(unsafe.Pointer(s))
    C.say(s)
}

https://www.cnblogs.com/guochaoxxl/p/6960854.html
结构体内有指针,则需要先释放内部指针,再释放结构体,文章中能看懂内存对齐的规则

小知识,通常(部分场景go编译器会优化)string 和[]byte 之间的转化也会发生内存拷贝,因为在语义上,string内容是不可变的,slice内容是可变的。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
type stringStruct struct {
    str unsafe.Pointer
    len int
}

多lib里的同名函数

对于不同share lib中出现同名函数,如何同时使用?
背景是,当两个 so包中有同名函数,然后在cgo中引入,大概率不会出现编译错误,但是运行时就会发现,同名函数只有一个被引入了,同名抢占 github-go-issue

  1. excutable so(shared object) https://gcc.gnu.org/legacy-ml/gcc-help/2003-07/msg00232.html
  2. openld
  3. go plugin

    利用同名抢占机制,可以进行hook,详见: 基于LD_PRELOAD的动态库函数hook

如何定位内存泄漏

  1. c代码中的内存泄漏,依然可以使用valgrind检查。但是需要注意,像C.CString这种泄漏,valgrind无法给出泄漏的准确位置。bcc套件也内存分析的重要工具。
  2. go pprof无法检查c代码中的内存泄漏。
  3. nmap 命令可以比top看到更多内存细节信息

详见:https://zhuanlan.zhihu.com/p/368567370
go程序内存泄露问题快速定位

CoreDump捕获

  1. coredump 捕获https://gitcode.net/mirrors/fananchong/test_cgo_coredump

core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成第一个文件,许多的程序出错的时候都会产生一个core文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。


操作系统打开coredump
ulimit –c [size] , size的单位是blocks,一般1block=512bytes, size太小则不能产生core文件,如取值1,2,3

在/etc/profile中加入以下一行,这将允许生成coredump文件
ulimit-c unlimited

core文件位置:
core文件默认的存储位置与对应的可执行程序在同一目录下,文件名是core,大家可以通过下面的命令看到core文件的存在位置:
cat /proc/sys/kernel/core_pattern
缺省值是core

cgo简明教程
http://westfly.github.io/post/cpp/cgo-with-cpp/

通常是gdb工具读core文件定位错误位置
https://blog.csdn.net/tenfyguo/article/details/8159176

make教程
https://www.ruanyifeng.com/blog/2015/02/make.html

cmake教程
https://github.com/chaneyzorn/CMake-tutorial

undifiend reference to xxxxx
https://zhuanlan.zhihu.com/p/81681440

链接参数意义
https://zhuanlan.zhihu.com/p/450986377

gcc变异参数记录
https://blog.csdn.net/xiaoyan_yt/article/details/103718487

gcc编译命令简单命令
https://www.cnblogs.com/ibyte/p/5828445.html

gcc编译命令详解
https://zhuanlan.zhihu.com/p/380180101
https://getiot.tech/linux-command/gcc.html

# cmake对大小写不敏感
include_Directories(/root/include) # 添加头文件目录,多个可用空格分割,相当于g++的-I
link_directories(/root/lib /usr/lib /usr/share/lib) # 相当于g++命令的-L选项的作用,
# 也相当于环境变量中增加LD_LIBRARY_PATH的路径的作用

set(vara "Hello World") # 设置一个普通变量
set(ENV{PATH} "$ENV{PATH}:${torch_home}/bin") # 设置环境变量
# 获得一个目录的绝对路径, 内置常量CMAKE_CURRENT_SOURCE_DIR 代表 CMakeLists.txt所在目录
# CMAKE_CURRENT_BINARY_DIR 代表 cmake命令所在目录
get_filename_component(project_home "${CMAKE_CURRENT_SOURCE_DIR}/../.." 
    ABSOLUTE DIRECTORY)
message($ENV{PATH} ${vara}) # 相当于 println

find_package(Protobuf) # 检查Protobuf 这个库是否存在
add_library(targert_libname source1.c header1.c source2.c) # 生成 静态库
add_executable(main main.c) # 编译可执行文件
target_link_libraries(target_hello liba.so libb.a lc) # 给目标链接制定的库
target_include_directories(target_hello hello.c) # 给目标库指定的头文件
add_custom_command() # 设置一个【目标文件】的生成命令,看明白make教程会有助于理解
find_program() # 查找一个命令