golang —— 语言交互性

​ 为了能够重用已有的 C 语言库,我们在使用 Golang 开发项目或系统的时候难免会遇到 GoC语言 混合编程,这时很多人都会选择使用 cgo

1 Cgo

​ 话说 cgo 这个东西可算得上是让人又爱又恨,好处在于它可以让你快速重用已有的 C语言,无需再用 Gg 重造一遍轮子,而 坏处就在于它会在一定程度上削弱你的系统性能。关于cgo 的种种劣迹,Dave Cheney 大神在他的博客上有一篇专门的文章《cgo is not Go》

​ 但话说回来,有时候为了快速开发满足项目需求,使用 cgo 也实在是不得已而为之。

官方示例:

package main

import "fmt"

/*
#include <stdlib.h>
*/
import "C"

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}
               
func main() {
    Seed(100)
    fmt.Println("Random:", Random())
}

原理:

import "C" 是一个标记,告诉 Cgo 他应该开始工作了。Cgo 做的事情就是,对应这条 import 语句之前的块注释中的 C 源代码自动生成包装性质的 Go 代码。函数调用从汇编角度看,就是一个将参数按顺序压栈( push ),然后进行函数调用( call )的过程。Cgo 的代码只不过是帮你封装了这个压栈和调用的过程,从外面看起来就是一个普通的 Go 调用。

语法:

import"C" 前面需要紧贴在注释的下面。块注释还是行注释无所谓。注释中的任意合法的 C 源代码,Cgo 都给会进行相应的处理并生成对应的 Go 代码,如下所示:

package hello

/*
#include <stdlib.h>
void hello() {
    printf("Hello, Cgo! -- Frin C world.\n");
}
*/
import "C"

func Hello() int {
    C.hello()
}

2 关键难点

​ 跨语言交互中,比较复杂的问题有两个:类型映射 以及 跨越调用边界传递指针 所带来的对象生命周期和内存管理的问题。

2.1 类型映射

基础数据类型

C语言 Go语言
signed char C.char / C.schar
unsigned char C.uchar
unsigned short C.short / C.ushort
unsigned int C.int / C.uint
unsigned long C.long / C.ulong
long long C.longlong
unsigned long long C.ulonglong
float C.float
double C.double
void*(指针) unsafe.Pointer

数据结构类型

C语言 Go语言
struct struct_(前缀)
union union_(前缀)
enum enum_(前缀)

2.2 字符串映射

Cgo 提供了一系列函数来提供支持:C.CStringC.GoStringC.GoStringN

注意:

  • 每次转换都将导致一次内存复制
  • 无论是 C 语言还是 Go 语言都不允许对字符串的内容进行修改

内存问题

由于 C.CString 的内存管理方式与 Go 语言自身的内存管理方式不兼容,所以再使用完之后,必需要现实的释放调用 C.CSting 所产生的内存快,否则将会造成内存泄漏。示例如下:

2.3 函数调用

对于常规返回了一个值的函数,调用者可以用以下方式,顺便得到错误码:

n, err := C.atoi("1234");

在传递数组类型的参数时,在 Go 中,将第一个元素的地址作为整个数组的起始地址传入,示例如下:

n, err := C.func(&array[0]);  // 需要显示指定第一个元素的地址

2.4 C代码中依赖第三方库

​ 当需要依赖非 C 标准的第三方库时,Cgo 提供了 #cgo 这样的伪 C 文法,让开发者有机会指定依赖的第三方库和编译选项。示例如下:

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo linux CFLAGS: -DLINUX=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"

​ 以上代码中示范了如何通过 CFLAGS 来传入编译选项,使用 LDFLAGS 来传入链接选项。更简便的 #cgo 用法如下所示:

// #cgo pkg-config: png cairo
// #include <png.h>
import "C"

​ 在 Go 中使用 cgo 调用 C 库的时候,如果需要引用很多不同的第三方库,那么使用 #cgo CFLAGS:#cgo LDFLAGS: 的方式会引入很多行代码。首先这会导致代码很丑陋,最重要的是如果引用的不是标准库,头文件路径和库文件路径写死的话就会很麻烦,一旦第三方库的安装路径变化了,Go 的代码也要跟着变化。所以使用 pkg-config 无疑是一种更为优雅的方法,不管库的安装路径有何变化,都不需要修改 Go 代码。

3 编译 Cgo

Go 安装后,自带一个 cgo 命令行工具,它用于处理所有带有 Cgo 代码的 Go 文件,生成 Go 语言版本的调用封装代码。而 Go 工具集又对 cgo 再次进行了良好的封装,所以我们不需要做任何特殊的处理。

猜你喜欢

转载自www.cnblogs.com/zhance/p/10135142.html