温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

golang中cgo的示例分析

发布时间:2021-12-15 09:40:42 来源:亿速云 阅读:196 作者:小新 栏目:大数据

这篇文章主要为大家展示了“golang中cgo的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“golang中cgo的示例分析”这篇文章吧。

GO调C基本原理CGO是实现Go与C互操作的方式,包括Go调C和C调Go两个过程。其中Go调C的过程比较简单。对于一个在C中定义的函数add3,在Go中调用时需要显式的使用C.add3调用。其中C是在程序中引入的一个伪包

代码中的import “C”即为在Go中使用的伪包。这个包并不真实存在,也不会被Go的compile组件见到,它会在编译前被CGO工具捕捉到,并做一些代码的改写和桩文件的生成。

CGO 提供了 golang 和 C 语言相互调用的机制。某些第三方库可能只有 C/C++ 的实现,完全用纯 golang 的实现可能工程浩大,这时候 CGO 就派上用场了。可以通 CGO 在 golang 在调用 C 的接口,C++ 的接口可以用 C 包装一下提供给 golang 调用。被调用的 C 代码可以直接以源代码形式提供或者打包静态库或动态库在编译时链接。推荐使用静态库的方式,这样方便代码隔离,编译的二进制也没有动态库依赖方便发布也符合 golang 的哲学。

基本数值类型

golang 的基本数值类型内存模型和 C 语言一样,就是连续的几个字节(1 / 2 / 4 / 8 字节)。因此传递数值类型时可以直接将 golang 的基本数值类型转换成对应的 CGO 类型然后传递给 C 函数调用,反之亦然:

package main

/*

#include <stdint.h>

static int32_t add(int32_t a, int32_t b) {

    return a + b;

}

*/

import "C"

import "fmt"

func main() {

    var a, b int32 = 1, 2

    var c int32 = int32(C.add(C.int32_t(a), C.int32_t(b)))

    fmt.Println(c) // 3

}

golang 和 C 的基本数值类型转换对照表如下:

C语言类型 CGO类型 Go语言类型

char C.char byte

singed char C.schar int8

unsigned char C.uchar uint8

short C.short int16

unsigned short C.ushort uint16

int C.int int32

unsigned int C.uint uint32

long C.long int32

unsigned long C.ulong uint32

long long int C.longlong int64

unsigned long long int C.ulonglong uint64

float C.float float32

double C.double float64

size_t C.size_t uint

注意 C 中的整形比如 int 在标准中是没有定义具体字长的,但一般默认认为是 4 字节,对应 CGO 类型中 C.int 则明确定义了字长是 4 ,但 golang 中的 int 字长则是 8 ,因此对应的 golang 类型不是 int 而是 int32 。为了避免误用,C 代码最好使用 C99 标准的数值类型

golang 中切片用起来有点像 C 中的数组,但实际的内存模型还是有点区别的。C 中的数组就是一段连续的内存,数组的值实际上就是这段内存的首地址。golang 切片的内存模型如下所示(参考源码 $GOROOT/src/runtime/chan.go

array len cap

  |

  v 

data

由于底层内存模型的差异,不能直接将 golang 切片的指针传给 C 函数调用,而是需要将存储切片数据的内部缓冲区的首地址及切片长度取出传传递:

package main

/*

#include <stdint.h>

static void fill_255(char* buf, int32_t len) {

    int32_t i;

    for (i = 0; i < len; i++) {

        buf[i] = 255;

    }

}

*/

import "C"

import (

    "fmt"

    "unsafe"

)

func main() {

    b := make([]byte, 5)

    fmt.Println(b) // [0 0 0 0 0]

    C.fill_255((*C.char)(unsafe.Pointer(&b[0])), C.int32_t(len(b)))

    fmt.Println(b) // [255 255 255 255 255]

}

字符串

golang 的字符串和 C 中的字符串在底层的内存模型也是不一样的:

golang 字串符串并没有用 '\0' 终止符标识字符串的结束,因此直接将 golang 字符串底层数据指针传递给 C 函数是不行的。一种方案类似切片的传递一样将字符串数据指针和长度传递给 C 函数后,C 函数实现中自行申请一段内存拷贝字符串数据然后加上未层终止符后再使用。更好的方案是使用标准库提供的 C.CString() 将 golang 的字符串转换成 C 字符串然后传递给 C 函数调用:

package main

/*

#include <stdint.h>

#include <stdlib.h>

#include <string.h>

static char* cat(char* str1, char* str2) {

    static char buf[256];

    strcpy(buf, str1);

    strcat(buf, str2);

    return buf;

}

*/

import "C"

import (

"fmt"

"unsafe"

)

func main() {

str1, str2 := "hello", " world"

// golang string -> c string

cstr1, cstr2 := C.CString(str1), C.CString(str2)

defer C.free(unsafe.Pointer(cstr1)) // must call

defer C.free(unsafe.Pointer(cstr2))

cstr3 := C.cat(cstr1, cstr2)

// c string -> golang string

str3 := C.GoString(cstr3)

fmt.Println(str3) // "hello world"

}

需要注意的是 C.CString() 返回的 C 字符串是在堆上新创建的并且不受 GC 的管理,使用完后需要自行调用 C.free() 释放,否则会造成内存泄露,而且这种内存泄露用前文中介绍的 pprof 也定位不出来。

其他类型

golang 中其他类型(比如 map) 在 C/C++ 中并没有对等的类型或者内存模型也不一样。传递的时候需要了解 golang 类型的底层内存模型,然后进行比较精细的内存拷贝操作。传递 map 的一种方案是可以把 map 的所有键值对放到切片里,然后把切片传递给 C++ 函数,C++ 函数再还原成 C++ 标准库的 map 。由于使用场景比较少,这里就不赘述了。

总结

本文主要介绍了在 golang 中使用 CGO 调用 C/C++ 接口涉及的一些细节问题。C/C++ 比较底层的语言,需要自己管理内存。使用 CGO 时需要对 golang 底层的内存模型有所了解。另外 goroutine 通过 CGO 进入到 C 接口的执行阶段后,已经脱离了 golang 运行时的调度并且会独占线程,此时实际上变成了多线程同步的编程模型。如果 C 接口里有阻塞操作,这时候可能会导致所有线程都处于阻塞状态,其他 goroutine 没有机会得到调度,最终导致整个系统的性能大大较低。总的来说,只有在第三方库没有 golang 的实现并且实现起来成本比较高的情况下才需要考虑使用 CGO ,否则慎用。

可以使用go tool cgo在本地目录生成这些桩文件

$go tool cgo main.go

.

|_____obj

| |_____cgo_.o

| |_____cgo_export.c

| |_____cgo_export.h

| |_____cgo_flags

| |_____cgo_gotypes.go

| |_____cgo_main.c

| |____main.cgo1.go

| |____main.cgo2.c

|____main.go

其中main.cgo1.go为主要文件,是用户代码main.go被cgo改写之后的文件:

$cat _obj/main.cgo1.go

// Created by cgo - DO NOT EDIT

//line /Users/didi/goLang/src/github.com/xiazemin/cgo/exp1/main.go:1

package main

//line /Users/didi/goLang/src/github.com/xiazemin/cgo/exp1/main.go:11

import "fmt"

func main() {

var a, b int32 = 1, 2

var c int32 = int32(_Cfunc_add(_Ctype_int32_t(a), _Ctype_int32_t(b)))

fmt.Println(c)

}

这个文件才是Go的compile组件真正看到的用户代码。可以看到原来文件中的import “C”被去掉,而用户写的C.int被改写为_Ctype_int,C.add3被改写为_Cfunc_add3。关于这个特性有两个点需要注意。一是在有import “C”的文件中,用户的注释信息全部丢失,使用的一些progma也不例外。二是在testing套件中import “C”不允许使用,表现为testing不支持CGO。但并不是没有办法在testing中使用CGO,可以利用上面的特性,在另外一个独立的Go文件中定义C函数,并使用import “C”;但是在使用testing的Go文件中直接使用_Cfunc_add3函数即可。_Cfunc_add3用户虽然没有显示定义,但是CGO自动产生了这一函数的定义。上面一系列的//line编译制导语句用做关联生成的Go与原来的用户代码的行号信息。

再次回到_Cfunc_add3函数,并不是C中的add3函数,是CGO产生的一个Go函数。它的定义在CGO产生的桩文件_cgo_gotypes.go中

$cat _obj/_cgo_gotypes.go

// Created by cgo - DO NOT EDIT

package main

import "unsafe"

import _ "runtime/cgo"

import "syscall"

var _ syscall.Errno

func _Cgo_ptr(ptr unsafe.Pointer) unsafe.Pointer { return ptr }

//go:linkname _Cgo_always_false runtime.cgoAlwaysFalse

var _Cgo_always_false bool

//go:linkname _Cgo_use runtime.cgoUse

func _Cgo_use(interface{})

type _Ctype_int int32

type _Ctype_int32_t _Ctype_int

type _Ctype_void [0]byte

//go:linkname _cgo_runtime_cgocall runtime.cgocall

func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32

//go:linkname _cgo_runtime_cgocallback runtime.cgocallback

func _cgo_runtime_cgocallback(unsafe.Pointer, unsafe.Pointer, uintptr, uintptr)

//go:linkname _cgoCheckPointer runtime.cgoCheckPointer

func _cgoCheckPointer(interface{}, ...interface{})

//go:linkname _cgoCheckResult runtime.cgoCheckResult

func _cgoCheckResult(interface{})

//go:cgo_import_static _cgo_3a42ad434848_Cfunc_add

//go:linkname __cgofn__cgo_3a42ad434848_Cfunc_add _cgo_3a42ad434848_Cfunc_add

var __cgofn__cgo_3a42ad434848_Cfunc_add byte

var _cgo_3a42ad434848_Cfunc_add = unsafe.Pointer(&__cgofn__cgo_3a42ad434848_Cfunc_add)

//go:cgo_unsafe_args

func _Cfunc_add(p0 _Ctype_int32_t, p1 _Ctype_int32_t) (r1 _Ctype_int32_t) {

_cgo_runtime_cgocall(_cgo_3a42ad434848_Cfunc_add, uintptr(unsafe.Pointer(&p0)))

if _Cgo_always_false {

_Cgo_use(p0)

_Cgo_use(p1)

}

return

}

_Cfunc_add3的参数传递与正常的函数有些不同,其参数并不在栈上,而是在堆上。函数中的_Cgo_use,其实是runtime.cgoUse,用来告诉编译器要把p0, p1, p2逃逸到堆上去,这样才能较为安全的把参数传递到C的程序中去。(因为go是动态栈不安全)

$    go build -gcflags "-m" main.go

# command-line-arguments

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:14:6: can inline _Cgo_ptr

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:14:35: leaking param: ptr to result ~r1 level=0

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:27:6: _cgo_runtime_cgocall assuming arg#2 is unsafe uintptr

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:30:6: _cgo_runtime_cgocallback assuming arg#3 is unsafe uintptr

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:30:6: _cgo_runtime_cgocallback assuming arg#4 is unsafe uintptr

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:47:11: p0 escapes to heap

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:48:11: p1 escapes to heap

/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:45:75: _Cfunc_add &p0 does not escape

./main.go:16: c escapes to heap

./main.go:16: main ... argument does not escape

函数中的__cgo_79f22807c129_Cfunc_add3是一个变量,记录了一个C函数的地址(注意,这并不是实际要调用add3函数),是一个真正定义在C程序中的函数。在Go中,通过编译制导语句//go:cgo_import_static在链接时拿到C中函数__cgo_79f22807c129_Cfunc_add3的地址,然后通过编译制导语句//go:linkname把这个函数地址与Go中的byte型变量__cgofn_cgo_79f22807c129_Cfunc_add3的地址对齐在一起。之后再利用一个新的变量__cgo_79f22807c129_Cfunc_add3记录这个byte型变量的地址。从而可以实现在Go中拿到C中函数的地址。做完,这些之后把C的函数地址和参数地址传给cgocall函数,进行Go与C之间call ABI操作。当然,cgocall里面会做一些调度相关的准备动作,后面有详细说明。

__cgo_79f22807c129_Cfunc_add3如上文所述,是定义在main.cgo2.c中的一个函数,其定义如下:

CGO_NO_SANITIZE_THREAD

void

_cgo_3a42ad434848_Cfunc_add(void *v)

{

struct {

int32_t p0;

int32_t p1;

int32_t r;

char __pad12[4];

} __attribute__((__packed__)) *a = v;

char *stktop = _cgo_topofstack();

__typeof__(a->r) r;

_cgo_tsan_acquire();

r = add(a->p0, a->p1);

_cgo_tsan_release();

a = (void*)((char*)a + (_cgo_topofstack() - stktop));

a->r = r;

}

在这个函数的定义中,并没有显式的参数拷贝;而是利用类型强转,在C中直接操作Go传递过来的参数地址。在这个函数中真正调用了用户定义的add3函数。

            cgocall即_Cfunc_add3中的_cgo_runtime_cgocall函数,是runtime中的一个从Go调C的关键函数。这个函数里面做了一些调度相关的安排。之所以有这样的设计,是因为Go调入C之后,程序的运行不受Go的runtime的管控。一个正常的Go函数是需要runtime的管控的,即函数的运行时间过长会导致goroutine的抢占,以及GC的执行会导致所有的goroutine被拉齐。C程序的执行,限制了Go的runtime的调度行为。为此,Go的runtime会在进入到C程序之后,会标记这个运行C的线程排除在runtime的调度之后,以减少这个线程对Go的调度的影响。此外,由于正常的Go程序运行在一个2K的栈上,而C程序需要一个无穷大的栈。这样的设计会导致在Go的栈上执行C函数会导致栈的溢出,因此在进去C函数之前需要把当前线程的栈从2K的栈切换到线程本身的系统栈上。栈切换发生在asmcgocall中,而线程的状态标记发生在cgocall中。

以上是“golang中cgo的示例分析”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注亿速云行业资讯频道!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI