BastenGao's Blog - Web, Rails, Ruby, Java

在 Go 语言中调用 C 代码


go cgo c 写于2017-12-24

Go 语言除了语法精炼、并发支持好外,还有一个优点就是可以调用 C 代码。可以直接在 Go 源代码里写 C 代码,也可以引 C 语言的外部库。这样在性能遇到瓶颈的地方可以重写,或者某些功能 Go 和第三方还缺失,但 C 语言有现成的库就可以直接用了。官方 Cgo 这块目前有一篇博客 https://blog.golang.org/c-go-cgo 和 命令行文档 https://golang.org/cmd/cgo/ 对 Cgo 进行了说明,其中某些地方还不够明确或者没有提到的地方。

内联 C 代码

下面例子是直接将 C 代码写在 Go 源代码里,引入了一个不存在的包 “C”, 然后将 C 代码写在了引入上面,注意只能写在 “C” 包上面。定义了一个方法 add, 然后通过 C.add 在 Go 代码里使用。 

package main

/* 下面是 C 代码 */

// int add(int a, int b) {
//     return a + b;
// }
import "C"

import "fmt"

func main() {
    a := C.int(1)
    b := C.int(2)
    value := C.add(a, b)
    fmt.Printf("%v\n", value)
    fmt.Printf("%v\n", int(value))
}

go run main.go

独立的 C 源代码

小巧的 C 代码可以方便的通过内联使用,但是代码比较庞大的话可以将 C 代码独立写。

有下面目录结构:

example/
  foo.h
  foo.c
  main.go

foo.h

int add(int, int);

foo.c

int add(int a, int b) {
    return a + b;
}

main.go

package main

// #include "foo.h"
import "C"

import "fmt"

func main() {
    a := C.int(1)
    b := C.int(2)
    value := C.add(a, b)
    fmt.Printf("%v\n", value)
}

上面例子将 C 代码拆成了 foo.h 和 foo.c , Go 代码只需要将 foo.h 引入即可调用 add 方法。

main 包引用多个文件后就不能使用 go run 命令运行了,只能使用 go build。go build 会检测 cgo 引用的文件,.c .h 等文件也会一起被编译。

cd example/
go build -o out
./out

调用外部库

目录结构:

example2/
  main.go
  foo/
    foo.h
    foo.c

C 代码和之前的一样,只是移到了 foo 目录下,目的是强制让 Go 不编译他们。假定 foo 就是我们需要用到的外部库。

先编译静态库, foo 目录下生成 libfoo.a 静态链接库。

cd foo
gcc -c foo.c -o foo.o
ar -crs libfoo.a foo.o

main.go

package main

// #cgo CFLAGS: -I./foo
// #cgo LDFLAGS: -L./foo -lfoo
// #include "foo.h"
import "C"
import "fmt"

func main() {
    value := C.add(C.int(1), C.int(2))
    fmt.Printf("%v\n", value)
}

调用外部库需要用到 #cgo 伪指令, 他可以指定编译和链接参数,如 CFLAGS, CPPFLAGS, CXXFLAGS, FFLAGS and LDFLAGS。 CFLAGS 可以配置 C 编译器参数,其中 -I 可以指定头文件目录,例如 “-I/path/to/include”。LDFLAGS 可以配置引用库的参数,其中 -L 指定引用库的目录,-l 指定库名称。如果引用的头文件或者库在系统默认的目录下(例如 /usr/include, /usr/local/include 和 /usr/lib, /usr/local/lib)则可以不用指定目录。

#cgo 指令中可以添加限制平台的参数,例如只针对 linux 或者 darwin 平台,详情参考 https://golang.org/pkg/go/build/#hdr-Build_Constraints 。同时可以使用 ${SRCDIR} 代替源代码目录。

// #cgo linux  CFLAGS: -I$./foo
// #cgo darwin CFLAGS: -I$./another_foo
// #cgo LDFLAGS: -L${SRCDIR}/foo -lfoo

数组指针的两个小技巧

C 语言中经常使用数组指针作为参数和返回值,下面例子展示了 Go 语言创建数组指针和转换数组指针到 slice 的过程。

package main

// #include <stdio.h>
// #include <stdlib.h>
//
// int pass_array_pointer(int *in, int n) {
//     int sum = 0;
//     for(int i = 0; i < n; i++) {
//         sum += in[i];
//         printf("%d\n" , in[i]);
//     }
//     return sum;
// }
//
// int *return_array_pointer(int n) {
//     int *out;
//     out = (int *)(calloc(3, sizeof(int)));
//     for(int i = 0; i < n; i++) {
//         out[i] = i * 2;
//     }
//     return out;
//  }
import "C"
import (
    "fmt"
    "reflect"
    "unsafe"
)

// 将 slice 转换为 C 数组指针
func pass() {
    in := []C.int{1, 2, 3, 4}
    inPointer := unsafe.Pointer(&in[0])
    inC := (*C.int)(inPointer)

    value := C.pass_array_pointer(inC, 4)
    fmt.Println(value)
}

// 将 C 数组指针转换为 slice
func get() {
    n := 4
    outC := C.return_array_pointer(4)
    defer C.free(unsafe.Pointer(outC))

    // 参考 https://github.com/golang/go/issues/13656#issuecomment-165867188
    sh := reflect.SliceHeader{uintptr(unsafe.Pointer(outC)), n, n}
    out := *(*[]C.int)(unsafe.Pointer(&sh))
    for _, v := range out {
        fmt.Println(v)
    }
}

func main() {
    pass()

    get()
}

限于篇幅就先到这,下篇说下如果通过 Go 代码调用 C++ 代码。

comments powered by Disqus