CGO简明教程

Hello World

package main

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

import (
	"fmt"
)

func main() {
    fmt.Println(int(C.random()))
}

当然这并不是Hello World。我们不首先输出Hello World是有原因的,接下来就会讲到。不过首先我们分析一下现在这个程序。 首先从结构上来看可以知道这就是一个普通的Go程序,第一行 package main 声明这个代码是在main包里。然后下面有 func main 是程序的入口。

// #inlcude <stdlib.h>
import "C"

这三行是Go调C才这样的,import "C" 是为了可以在Go程序里直接使用C里的一些函数,例如main中 C.random(),而 import "C" 上边的注释叫做preamble,注意必须和 import "C"紧紧挨着中间不能有空格。注释的风格可以是 // #include... 也可以是 /*#include ...*/ 这样的。此外可以在 preamble 中加入 // #cgo 开头的注释,用于指示编译和链接中发生的一些事情,例如链接哪个动态链接库等。

接下来我们看看Hello World。

首先我们需要三个文件,helloworld.h:

#ifndef __helloworld
#define __helloworld

void Printf(char *s);

#endif

helloworld.c:

#include <stdio.h>

void Printf(char *s) {
    printf("%s", s);
}

main.go:

package main

// #include "helloworld.h"
import "C"

func main() {
    C.Printf("hello world")
}

为啥不直接 C.printf 输出呢,因为在wiki中提到cgo目前暂时还不支持变长参数的C函数,所以要我们自己包装一下。编译:

$ go build
./main.go:7: cannot use "hello world" (type string) as type *_Ctype_char in argument to _Cfunc_Printf

原因是C和Go的字符串不是通用的,我们要把Go的字符串转成C的字符串,但是因为不是在编译的这个过程申请内存,而是在堆里 申请内存存储字符串,而Go的垃圾回收是管不到C申请的内存,所以我们需要自行销毁对应的内存。

package main

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

import (
    "unsafe"
)

func main() {
    cs := C.CString("hello world\n")
    defer C.free(unsafe.Pointer(cs))

    C.Printf(cs)
}

也可以我们先把C编译成动态链接库,然后在Go里指示链接:

package main

// #cgo LDFLAGS: -L${SRCDIR}/ -Wl,-rpath,${SRCDIR}/ -lhelloworld
// #include <stdlib.h>
// #include "helloworld.h"
import "C"

import (
    "unsafe"
)

func main() {
    cs := C.CString("hello world\n")
    defer C.free(unsafe.Pointer(cs))

    C.Printf(cs)
}

我们先把 helloworld.c 编译成动态链接库 libhelloworld.so

类型

C和Go中有很多类型是对应的,但是需要我们自行转换类型。例如:

- C.int
- C.long
- C.ulong

如果是访问struct得这么用 C.struct_<struct的名字>,enum,union和sizeof也类似。

具体参考:https://golang.org/cmd/cgo/#hdr-Go_references_to_C

此外,对于指针类型,则是该咋用咋用,比如 *C.int。但是对于 void *,则需要用 unsafe.Pointer来表示。

其他知识

如果说需要什么其他知识,那就是编译链接相关的知识了,推荐《程序员的自我修养-链接、装载与库》。这本书非常的好,国产神书, 不过说实话,因为不经常接触,看过然后又忘记了大部分内容🤦‍♂️

CGO是如何运行的

在Go中调用C函数,cgo生成的代码调用 runtime.cgocall(_cgo_Cfunc_f, frame)_cgo_Cfunc_f 就是GCC编译 出来的代码。 runtime.cgocall 会调用 runtime.asmcgocall(_cgo_Cfunc_f, frame)

runtime.asmcgocall 会切换到m->go 的栈然后执行代码,因为 g0 的栈是操作系统分配的栈(大小为8k),足够 执行C代码。 _cgo_Cfunc_f 在frame中执行C函数,然后返回到 runtime.asmcgocall。之后再切回调用它的 G的栈。



更多文章
  • 有GitHub Copilot?那就可以搭建你的ChatGPT4服务
  • 窗口函数的使用(以PG为例)
  • 读《为什么学生不喜欢上学》
  • OpenAI Prompt Engineering 摘录和总结
  • 读《打造真正的新产品》
  • 2023年终总结
  • VueJS 总结
  • Linux 自动挂载 alist 提供的webdav
  • FreeBSD 使用 vm-bhyve 安装Debian虚拟机
  • FreeBSD 和 Linux 网卡聚合实现提速
  • GPT 帮我搞定了时区转换问题
  • 长任务系统如何处理?
  • macOS/Linux 编译 InputLeap
  • 使用开源软KVM - synergy-core
  • 解决 macOS 终端hostname一直变化问题