Cgo | 简单使用
使用自己的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中关于 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中有释放 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里执行
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2020/11/cgo%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接