Catalog
  1. 1. 调用 C 代码
  2. 2. 调用 C++
  3. 3. 总结
cog的使用

调用 C 代码

hello.h, c header file

1
2
3
4
#ifndef VIDEO_H
#define VIDEO_H
int hello(char* name); // 声明
#endif

hello.c, c source file

1
2
3
4
5
6
7
#include <stdio.h>
#include "hello.h"

int hello(char* name){ // 实现
printf("hello %s\n", name);
return 42;
}

编译动态库

1
gcc hello.c -fPIC -shared -o libhello.so

main.go

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
package main

/*
// 指定动态库搜索路径(可以是相对路径,这个相对路径是基于可执行程序所在位置),也可以将所在路径加入系统的动态库搜索路径。否则最后编译可以通过,但是执行时会找不到动态库。
#cgo LDFLAGS: -Wl,-rpath=${SRCDIR}

// 头文件路径
#cgo CFLAGS: -I.

// 动态库链接路径
#cgo LDFLAGS: -L. -lhello

// 头文件
#include "hello.h"
#include <stdio.h>
#include <stdlib.h>

*/
import "C" // 这一行要紧跟着上面的内容,不要有空行
import "fmt"

func main() {
// 调用 c 方法并接收返回
ret := C.hello(C.CString("42"))
fmt.Println("ret: ", int(ret))
}

编译

1
go build main.go

调用 C++

cgo 只支持调用 c 代码,如果需要调用 c++ 的话,就需要用 c 封装一层接口暴露给 go
使用。仔细看一下就知道, cgo 在编译时使用 gcc 编译器,那么第一,它不支持c++特性。第二,它无法连接 c++ 动态库,因为他们的函数签名规则不一致。所以需要暴露一层 C 接口,并用 extern “C” 修饰以生成未经修饰的函数签名。

c++ header

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef HELLO_H
#define HELLO_H
#include <string>
#include "wrap.h" // 包含这个文件,它需要被编译器处理,识别 extern "C" 以生成没有修饰的函数签名

class Hello {
public:
int p(std::string name);

};


#endif

c++ code

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include "hello.h"

int Hello::p(std::string name){ // 实现
printf("hello %s\n", name.c_str());
return 42;
}


int hello_wrap(char * name) {
return Hello().p(std::string(name));
}

c warp

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef WRAP_H
#define WRAP_H

#ifdef __cplusplus
extern "C" { // 当使用 c++ 编译时,生成未修饰的函数签名(c++ 编译器会以特殊规则生成函数签名)
#endif
int hello_wrap(char* name); // 声明
#ifdef __cplusplus
}
#endif

#endif

go

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
package main

/*
#cgo LDFLAGS: -Wl,-rpath=${SRCDIR}

#cgo CFLAGS: -I.

#cgo LDFLAGS: -L. -lhello

#include "wrap.h"
#include <stdio.h>
#include <stdlib.h>

*/
import "C"
import (
"fmt"
"unsafe"
)

func main() {
cs := C.CString("42")
ret := C.hello_wrap(C.CString("42"))
// 需要手动释放,C.CString 不由 gc 管理
C.free(unsafe.Pointer(cs))
fmt.Println("ret: ", int(ret))
}

编译

1
2
g++ hello.cpp -fPIC -shared -o libhello.so
go build main.go

我们看一下正常情况下,动态库的符号

1
nm -D libhello.so | grep hello_wrap

TIM图片20191223210552.png

再来看一下如果 wrap.h 没有被处理时(没有被任何文件包含,因为它只是定义,所以不包含不会报错,这里很容易采坑)的动态库符号, 也就是 c++ 代码的函数签名。
TIM图片20191223210530.png
而 cgo 使用功能 gcc 只能识别第一种签名。如果不是的话就会提示未定义的引用错误了。

总结

如果是 C 代码的话,基本没有什么问题,最多可能就是提示找不到库,这个通过 rpath 或者将动态库加入系统路径就能解决。再有一个就是 go 文件的 import “C” 要紧跟着上面的注释块,不可空行。最后,如果是 C++ 代码的话,就需要注意一下,封装的头文件需要被编译器处理,否则就不会生成未经修饰的函数签名了,当时我遇到了这个问题,才发现我的 wrap 文件没有被包含过(当时我也没注意,只想着头文件包含没啥问题)。

Author: 42
Link: http://blog.ikernel.cn/2019/12/23/cgo%E5%9F%BA%E6%9C%AC%E7%9A%84%E4%BD%BF%E7%94%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment