聊聊 Cgo 的二三事

David Chou @ Golang Taipei / Crescendo Lab

CC-BY-SA-3.0-TW

@ Crescendo Lab

@ Golang Taipei Co-organizer 🙋‍♂️


Software engineer, DevOps, and Gopher 👨‍💻

david74.chou @ gmail
david74.chou @ facebook
david74chou @ telegram

What is Cgo?

Cgo lets Go packages call C code

package main

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

func Random() int {
	r := C.random() // r is C.long
	return int(r)
}

func Seed(i int) {
	C.srandom(C.uint(i))
}

func main() {
	Seed(0)

	for i := 0; i < 10; i++ {
		fmt.Printf("rand(%d): %d\n", i, Random())
	}
}
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"

Cgo Preamble

  • CFLAGS / CPPFLAGS / LDFLAGS

  • Add build constraints

// #cgo pkg-config: png
// #cgo CFLAGS: -DPNG_DEBUG=1
// #include <png.h>
import "C"

Cgo Preamble

  • Use pkg-config tool

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

Cgo Preamble

  • Use pre-compiled library in the package

package native

/*
#cgo CFLAGS: -I${SRCDIR}/includes/
#cgo windows LDFLAGS: ${SRCDIR}/libs/winlibz.a
#cgo linux LDFLAGS: ${SRCDIR}/libs/linuxlibz.a
#cgo darwin LDFLAGS: ${SRCDIR}/libs/darwinlibz.a
*/
import "C"
.
├── cgo.go
├── libs
│   ├── darwinlibz.a
│   ├── linuxlibz.a
│   └── winlibz.a
└── includes
    ├── zconf.h
    └── zlib.h

C types in Cgo

  • char => C.char

  • unsigned char => C.uchar

  • int => C.int

  • void* => unsafe.Pointer

  • struct S => C.struct_S

  • enum E => C.enum_E

Data conversion in Cgo

func C.CString(string) *C.char
func C.CBytes([]byte) unsafe.Pointer

func C.free(unsafe.Pointer)
  • Copy Go string / []byte to C

  • Allocated with malloc()

  • Must be freed with C.free()

Data conversion in Cgo

func C.GoString(*C.char) string
func C.GoStringN(*C.char, C.int) string

func C.GoBytes(unsafe.Pointer, C.int) []byte
  • Copy C string / bytes to Go

/*
#include <stdlib.h>

char *get_string() {
	return strdup("hello from C");
}

void release_string(char *s) {
	free(s);
}

*/
import "C"

func main() {
	cs := C.CString("Hello from go\n")
	C.puts(cs)
	C.free(unsafe.Pointer(cs))

	cs = C.get_string()
	gs := C.GoString(cs)
	fmt.Printf("%s\n", gs)
	C.release_string(cs)
}

Cgo is not Go - Rob Pike

Why not to use Cgo

  • Break Go’s awesome tooling

  • Break your static binary

  • Break cross-compiling

  • Manage memory in C by hand

  • Cgo calls are much slower than native Go calls

//#include <unistd.h>
//void foo() { }
import "C"

//go:noinline
func foo() {}

func CallCgo(n int) {
	for i := 0; i < n; i++ {
		C.foo()
	}
}

func CallGo(n int) {
	for i := 0; i < n; i++ {
		foo()
	}
}
  • BenchmarkCGO-16     3772150     308.3 ns/op
  • BenchmarkGo-16    931552690     1.231 ns/op

When to use Cgo

  • No Go equivalent library

  • Integrate with legacy C code

  • Need to consume a proprietary library in C

Any other choice?

Cgo Tips and Pitfalls

Write your own C bridge func

  • Get more control with your C interface

  • More precise in C header

  • Merge multiple Cgo calls into 1

    • Do loop-calls in C is more efficient

foos := []C.struct_Foo{
	{ a: C.int(1), b: C.int(2) },
	{ a: C.int(3), b: C.int(4) },
}

C.pass_array((*C.struct_Foo)(unsafe.Pointer(&foos[0])), C.int(len(foos)))

Isolate the Cgo part

  • Isolate the Cgo wrapper into its own package

    • even in a standalone process and communicate with RPC

  • Can't call Cgo in Go test code

Use static link library

  • Try hard to static link your libraries, otherwise your dependency might be complicated

go build -ldflags='-extldflags "-static"'

Be careful for memory mgmt

  • Never keep any pointer outside of the called function

    • the memory might be GC or free

Be careful for memory mgmt

  • Be aware of the C-memory ownership

  • Use "defer C.free()" to release memory when func returns

d := C.GoBytes([]byte{...})
C.foo(d) // <- if foo() takes the ownership 
// C.free(d)

Be careful for memory mgmt

  • ZeroCopy: Go => C

func Foo(payload []byte) {
	p := (*C.uchar)(unsafe.Pointer(&payload[0]))
	size := C.int(len(payload))
	C.foo(p, size)
}
func Foo(str string) {
	strHdr := (*reflect.StringHeader)(unsafe.Pointer(&str))
	p := (*C.char)(unsafe.Pointer(strHdr.Data))
	size := C.int(len(payload))
	C.foo(p, size)
}

ZeroCopy (Go => C): []bytes

ZeroCopy (Go => C): string

Be careful for memory mgmt

  • ZeroCopy: C => Go

// before 1.17
func toByteSlize(data unsafe.Pointer, size int) []byte {
	return (*[1 << 30]byte)(data)[:size:size]
}

// after 1.17
func toByteSlize(data unsafe.Pointer, size int) []byte {
	return unsafe.Slze(data, size)
}

ZeroCopy (C => Go): []bytes

Cgoroutines != Goroutines

  • Cgo calls block your system thread

func main() {
  var wg sync.WaitGroup
  wg.Add(1000)
  for i := 0; i < 1000; i++ {
    go func() {
        time.Sleep(100 * time.Second)
        wg.Done()
    }()
  }
  
  wg.Wait()
}

func main() {
  var wg sync.WaitGroup
  wg.Add(1000)
  for i := 0; i < 1000; i++ {
    go func() {
        time.Sleep(100 * time.Second)
        wg.Done()
    }()
  }
  
  wg.Wait()
}

// use 13 threads

// use 1004 threads

Cgoroutines != Goroutines

// Call from Go to C.
func cgocall(fn, arg unsafe.Pointer) int32 {
	...
    
	// Announce we are entering a system call
	// so that the scheduler knows to create another
	// M to run goroutines while we are in the
	// foreign code.
	//
	entersyscall()
	
	errno := asmcgocall(fn, arg)

	exitsyscall()

	...
}

C callbacks to Go

  • Go function could be exported for use by C

//export Add
func Add(arg1, arg2 int) int {...}
  • It will be available in the C code as

extern int Add(int arg1, int arg2);

C callbacks to Go

  • C can't callbacks to a receiver function

    • because of Cgo pointer passing rule

    • need to be creative

func (c *MyCounter) Add(i int) {
	c.count += i
}
var mu sync.Mutex
var index int
var fns = make(map[int]func(int))

func register(fn func(int)) int {
	mu.Lock()
	defer mu.Unlock()
	index++
	for fns[index] != nil {
		index++
	}
	fns[index] = fn
	return index
}

func lookup(i int) func(int) { ... }

func unregister(i int) { ... }
  • Use an extra map to keep the callbacks

/*
extern void go_callback_int(
	int foo, 
	int p1);

static inline void CallMyFunction(int foo) {
	go_callback_int(foo, 5);
}
*/
import "C"

//export go_callback_int
func go_callback_int(idx C.int, p1 C.int) {
	fn := lookup(int(idx))
	fn(int(p1))
}

func (c *MyCounter) Add(i int) { ... }

func main() {
	c := &MyCounter{count: 0}
	i := register(c.Add)
	C.CallMyFunction(C.int(i))
	unregister(i)
}
/*
extern void go_callback_int(
	uintptr_t h, 
	int p1);

static inline void CallMyFunction(uintptr_t h) {
	go_callback_int(h, 5);
}
*/
import "C"

//export go_callback_int
func go_callback_int(h C.uintptr_t, p C.int){
	v := cgo.Handle(h).Value()
	fn := v.(func(C.int))
	fn(p)
}

func (c *MyCounter) Add(i int) { ... }

func main() {
	c := &MyCounter{count: 0}
	i := register(c.Add)
	h := cgo.NewHandle(c.Add)
	C.CallMyFunction(C.uintptr_t(h))
	h.Delete()
}

Go 1.17
cgo.Handle

  • Cgo preamble

  • C <=> Go type conversion

  • When to / not-to use Cgo

  • Cgo tips and pitfalls

Recap

We are hiring now! Feel free to chat with us in Room TR 312 (研揚大樓 312室).

聊聊 CGO 的二三事

By Ting-Li Chou

聊聊 CGO 的二三事

  • 127