企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] ## 使用自己的C函数 文件名:testC.go ``` package main /* #include <stdio.h> #include <stdlib.h> void c_print(char *str) { printf("%s\n", str); } */ import "C" //import “C” 必须单起一行,并且紧跟在注释行之后 import "unsafe" func main() { s := "Hello Cgo" cs := C.CString(s) //字符串映射 C.c_print(cs) //调用C函数 defer C.free(unsafe.Pointer(cs)) //释放内存 } ``` 说明: 1、go代码中的C代码,需要用注释包裹,块注释和行注释均可,其次import “C”是必须的,并且和上面的C代码之间不能用空行分割,必须紧密相连 如果执行go run \*\*时出现 ``` # command-line-arguments could not determine kind of name for xxx ``` 那么就需要考虑 是不是improt “C”和上面的C代码没有紧挨着导致了 2、import “C” 并没有导入一个名为C的包,这里的import “C”类似于告诉Cgo将之前注释块中的C代码生成一段具有包装性质的Go代码 3、访问C语言中的函数需要在前面加上C.前缀,如C.Cstring C.go\_print C.free 4、对于C语中的原生类型,Cgo都有对应的Go语言中的类型 如go代码中C.int,C.char对应于c语言中的int,signed char,而C语言中void\*指针在Go语言中用特殊的unsafe.Pointer(cs)来对应 而Go语言中的string类型,在C语言中用字符数组来表示,二者的转换需要通过go提供的一系列函数来完成: * C.Cstring : 转换go的字符串为C字符串,C中的字符串是使用malloc分配的,所以需要调用C.free来释放内存 * C.Gostring : 转换C字符串为go字符串 * C.GoStringN : 转换一定长度的C字符串为go字符串 需要注意的是每次转换都会导致一次内存复制,所以字符串的内容是不可以修改的 5、**利用defer C.free 和unsafe.Pointer显示释放调用C.Cstring所生成的内存块** 然后我们编译以下测试看看。 ``` go run testC.go ``` ## C.CString的解释 在[cgo-1](https://golang.org/cmd/cgo/)中关于 C.CString 的注释里面已经写的很清楚了。 需要手动释放,C.CString 返回的指针。 ``` // Go string to C string // The C string is allocated in the C heap using malloc. // It is the caller's responsibility to arrange for it to be // freed, such as by calling C.free (be sure to include stdlib.h // if C.free is needed). func C.CString(string) *C.char ``` 在[cgo-2](http://blog.golang.org/c-go-cgo)中有释放 C.CString 返回指针的示例: ``` func Print(s string) { cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) C.fputs(cs, (*C.FILE)(C.stdout)) } ``` 这个问题我想后来也是引起了Go语言作者的注意了, 在go1.7新版本发布信息中我发现新出了一个 C.Bytes 的类型,C.Bytes 就不需要像 C.CString 一样需要手动释放内存了。 ## 用Go重新实现C函数 定义一个头文件 ``` //hello.h void SayHello(const char* c); ``` 定义hello.go文件 SayHello 实现 ``` //hello.go package main import "C" import "fmt" //export SayHello func SayHello(s *C.char) { fmt.Println(C.GoString(s)) } ``` 我们通过CGO的`//export SayHello`指令将Go语言实现的函数`SayHello`导出为C语言函数。为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。需要注意的是,这里其实有两个版本的`SayHello`函数:一个Go语言环境的;另一个是C语言环境的。cgo生成的C语言版本SayHello函数最终会通过桥接代码调用Go语言版本的SayHello函数。 通过面向C语言接口的编程技术,我们不仅仅解放了函数的实现者,同时也简化的函数的使用者。现在我们可以将SayHello当作一个标准库的函数使用(和puts函数的使用方式类似): ``` //say_hello.go package main //#include <hello.h> import "C" func main() { C.SayHello(C.CString("Hello, World\n")) } ``` ## 面向C接口的Go编程 尝试将例子中的几个文件重新合并到一个Go文件。下面是合并后的成果: ``` //c_say_hello.go package main import ( "fmt" "runtime" "strconv" "strings" ) /* void SayHello(char* c); */ import "C" //获取 gorutine id func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine ") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err != nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) } func main() { fmt.Println("1111111=", GetGoid()) C.SayHello(C.CString("hello world!\n")) } //export SayHello func SayHello(s *C.char) { fmt.Println("2222222222=", GetGoid()) fmt.Println(C.GoString(s)) } ``` 现在版本的CGO代码中C语言代码的比例已经很少了,但是我们依然可以进一步以Go语言的思维来提炼我们的CGO代码。通过分析可以发现`SayHello`函数的参数如果可以直接使用Go字符串是最直接的。在Go1.10中CGO新增加了一个`_GoString_`预定义的C语言类型,用来表示Go语言字符串。下面是改进后的代码: ``` //test_hello.go package main import ( "fmt" "runtime" "strconv" "strings" ) /* void SayHello(_GoString_ c); // 修改1 */ import "C" //获取 gorutine id func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine ") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err != nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) } func main() { fmt.Println("1111111=", GetGoid()) C.SayHello("hello world!\n") // 修改1 } //export SayHello func SayHello(s string) { fmt.Println("2222222222=", GetGoid()) fmt.Println(s) // 修改1 } ``` *思考题: main函数和SayHello函数是否在同一个Goroutine里执行?* ``` $ go run test_hello.go 1111111= 1 2222222222= 1 hello world! ``` 可以看到main函数和SayHello函数在统一个Goroutine里执行