广阔天地大有作为

你想拥有什么,就去追求什么

24 Mar 2019

cgo笔记

看了下上一次提交还是去年刚搭建这个博客的时候,之前在博客园维护过一个博客,三天打鱼半年晒网写过几篇,后面打算转战到此争取能多写几篇,没想到还是没做到。今天难得有时间,先总结一篇cgo相关的吧。

目前公司的技术栈已经全面转go,而一些业务还需要依赖到一些老的C\C++ so,或者一些项目必须调用C/C++的so,比如ffmpeg。所以工作中经常会使用到cgo来调用这些C\C++ so,把cgo调用方法和遇到的问题简单记录下来。

cgo简单调用

在go中如果要调用C接口,要采用cgo来实现。比如下面的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

/*
#include <stdio.h>

int add(int a, int b) {
 	return a + b
}
#cgo CFLAGS: -g
*/
import "C"

func main() {
    sum := C.add(C.int(1), C.int(1))
}

如果要使用cgo,在go源文件中必须加入import “C"来标识,用于导入C实现的代码,而C源码则在import “C”上面以注释的方式加入。导入以后C源码可以在go代码中直接调用只要在函数、类型以C.开头即可。这里需要注意的一点是C代码和import “C”之间不能有空行

调用so

在实际应用中,一般会把C实现封装成so提供cgo调用,通过LDFLAGS指定lib即可。

1
2
3
4
5
6
7
8
9
package main

/*
#cgo CFLAGS: -I ../lib
#cgo LDFLAGS: -L ${SRCDIR}/../lib -lxl_stat -lxl_thunder_sdk
#include "call_so_interface.h"
#include <stdlib.h>
*/
import "C"

注意,在编译时指定lib或者include的路径,不能直接使用相对路径,但可以使用一个变量${SRCDIR},这个变量指的是源码路径。

类型转换

对于字符串,提供了CString、GoString两个函数进行转换,看下面的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"fmt"
	"unsafe"
)

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

char* pass_char_array(char* in) {
	printf("print in c is %s\n",in);
	char* out = malloc(64);
	sprintf(out, "%s", "hello world return from c");
	return out;
}
#cgo CFLAGS: -g
*/
import "C"

func main() {
	str := "hello world pass from go"
	in := C.CString(str)
	out := C.pass_char_array(in)
	outStr := C.GoString(out)
	C.free(unsafe.Pointer(in))    //注意需要手动调用free
	C.free(unsafe.Pointer(out))   //注意需要手动调用free
	fmt.Println("print in go ", outStr)
}

运行结果为

1
2
print in c is hello world pass from go
print in go  hello world return from c

唯一需要注意的是,对于C类型的字符串要记得手动调用C.free防止内存泄漏

对于其他数组类型的转换,最近在工作中遇到一个问题,先看我的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "unsafe"

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

int pass_int_array(int* in, int len) {
	int i = 0;
	for (i = 0; i < len; i++) {
		printf("%d ", in[i]);
	}
	return 0;
}
#cgo CFLAGS: -g
*/
import "C"

func main() {
	a := []int{1, 2, 3, 4}
	C.pass_int_array((*C.int)(unsafe.Pointer(&a[0])), 4)
}

我们可以看到,对于数组的转换是采用unsafe.Pointer取go数组a的首地址显式转换成C int指针类型。OK,运行一下,看看打印结果是什么。 1 0 2 0

这是什么情况,没毛病啊,说好的1 2 3 4呢?我猜想是不是两边int占用的内存大小不一致呢?go int 8个字节,C int 4个字节,通过一段代码来验证一下吧。

1
2
3
4
g := 1
c := C.int(1)
fmt.Println("sizeof go int", unsafe.Sizeof(g))
fmt.Println("sizeof c int", unsafe.Sizeof(c))

运行结果

1
2
sizeof go int 8
sizeof c int 4

果然有毛病啊老铁,于是把go中数组改成a := []int32{1, 2, 3, 4}再次运行,完美通过。

所以要注意,在这种跨语言调用中,对于int、double等类型的传递一定要明确指定字节大小

调用C++代码

无法直接调用C++代码,需要再封装一层,导出为C类型接口才行。