使用自己的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里执行

--完--