CGO的介绍

运行cgo的命令


windowns配置:下载 TDM-GCC-64

go run -n main.go

go build

例子

Mat.h

#ifndef MAT_H_
#define MAT_H_

class Mat {
private:
    int w;
    int h;
    float* data;
public:
    Mat(int iw, int ih);
    int GetW();
    int GetH();
    float At(int i, int j);
};
#endif

Mat.cpp

#include <stdlib.h>
#include "Mat.h"

Mat::Mat(int iw, int ih)
{
    data = (float*)malloc(iw * ih * sizeof(float));
    w = iw;
    h = ih;
   for (int i =0; i< h; i++)
    {
       for (int j=0;j <w;j++)
       {
           data[i*w + j] = (float)(i + j + 1);
       }
    }
}
int Mat::GetW()
{
    return w;
}
int Mat::GetH()
{
    return h;
}
float Mat::At(int i, int j)
{
    return data[j * w + i];
}

test.h

#ifndef TEST_H_
#define TEST_H_
#include <stdlib.h>
#include <stdio.h>
#ifdef __cplusplus
#include "Mat.h"
extern "C" {
#endif

#ifdef __cplusplus
typedef Mat* TMat;
#else
typedef void* TMat;
#endif

TMat New_Mat(int w, int h);
void Free_Mat(TMat m);
int  Mat_W(TMat m);
int  Mat_H(TMat m);
float Mat_At(TMat m, int i, int j);

void TestCpp(char* msg);
struct AAA{
    int a;
    int b;
};


struct AAA* GetAAA();
int TDiv(int a, int b);
//void SetMat(int* tt, int len);
void SetMat(void* tt, int len);
float* Process();
#ifdef __cplusplus
}
#endif
#endif

test.c

#include <stdlib.h>
#include <stdio.h>
#include "test.h"
void TestSayHello(const char* s) {
    puts(s);
}

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

struct AAA* GetAAA() {
    struct AAA* aaa = malloc(sizeof(struct AAA));
    aaa->a = 1;
    aaa->b = 2;
    return aaa;
}

float* Process()
{
   int w = 5;
   int h = 5;
   float* mat = (float*)malloc(w * h * sizeof(float));
   for (int i =0; i< h; i++)
    {
       for (int j=0;j <w;j++)
       {
           mat[i*w + j] = (float)(i + j + 1);
       }
    }
    // for (int i =0; i< h; i++)
    // {
    //    for (int j=0;j <w;j++)
    //    {
    //        printf("Process: %lf\n", mat[i*w + j]);
    //    }
    // }
   return mat;
}
void SetMat(void* tt, int len){
    int* ttt1 = (int*)tt;
    for (int i = 0; i < len; i ++){
        printf("SetMat: %d\n", ttt1[i]);
    }
    ttt1[2] = 3;

    fflush(stdout);  //刷新缓冲区到console
}

testcpp.cpp

#include <iostream>
#include <stdlib.h>
#include <errno.h>
#include "test.h"

TMat New_Mat(int w, int h)
{
    return new Mat(w, h);
}
void Free_Mat(TMat m)
{
    delete m;
}
int  Mat_W(TMat m)
{
    return m->GetW();
}
int  Mat_H(TMat m)
{
    return m->GetH();
}
float Mat_At(TMat m, int i, int j)
{
    return m->At(i, j);
}


void TestCpp(char* msg)
{
    std::cout<< "cpp: " << msg << std::endl;
}

int TDiv(int a, int b) {
    if(b == 0) {
        errno = EINVAL;
        return 0;
    }
    return a/b;
}

cgo.go

package main

/*
#cgo CXXFLAGS:   --std=c++11
#include <stdio.h>
#include "test.h"
*/
//int Add(int a, int b);
// #define WIN32_LEAN_AND_MEAN
// #include <windows.h>
import "C"
import (
	"fmt"
	"reflect"
	"syscall"
	"unsafe"
)

// C语言类型	            CGO类型	    Go语言类型
// char	                    C.char	     byte
// singed char     	        C.schar	     int8
// unsigned char	        C.uchar	     uint8
// short	                C.short	     int16
// unsigned short	        C.ushort	 uint16
// int	                    C.int	     int32
// unsigned int	            C.uint	     uint32
// long	                    C.long	     int32
// unsigned long	        C.ulong	     uint32
// long long int	        C.longlong	 int64
// unsigned long long int	C.ulonglong	 uint64
// float	                C.float	     float32
// double	                C.double	 float64
// size_t	                C.size_t	 uint

// type Cptr unsafe.Pointer

// //C语言类型void*对应于Go语言的类型unsafe.Pointer
// func CFree(p Cptr) {
// 	if p != nil {
// 		C.free(unsafe.Pointer(p))
// 		p = nil
// 	}
// }

//go访问结构体
func TestCtruct() {
	var aaa C.struct_AAA
	aaa.a = 1
	aaa.b = 2
	fmt.Println(aaa)
}

//go访问c接收c返回的结构体指针
func TestCReturnStructPtr() {
	res2 := C.GetAAA()

	fmt.Println(reflect.TypeOf(res2).String())
	fmt.Println(res2.a)
	fmt.Println(res2.b)

	//free memory
	if res2 != nil {
		C.free(unsafe.Pointer(res2))
		res2 = nil
	}
	fmt.Println(res2)
}
func TestCAdd() {
	res := C.Add(1, 2)
	fmt.Println("TestCAdd ", res)
}

//go访问windowns库
func TestGetCurrentDirectory() string {
	if bufLen := C.GetCurrentDirectoryW(0, nil); bufLen != 0 {
		buf := make([]uint16, bufLen)
		if bufLen := C.GetCurrentDirectoryW(bufLen, (*C.WCHAR)(&buf[0])); bufLen != 0 {
			return syscall.UTF16ToString(buf)
		}
	}
	return ""
}

//go传入char*到C
func TestCharPtrToC() {
	s := "Hello Cgo"
	cs := C.CString(s)

	C.TestCpp(cs) //访问cpp里的函数

	C.free(unsafe.Pointer(cs)) //free memory
}

//在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno错误状态
//返回错误到go
func TestCDiv() {
	v0, err0 := C.TDiv(2, 1)
	fmt.Println("TestCDiv==>", v0, err0)

	v1, err1 := C.TDiv(1, 0)
	fmt.Println(v1, err1)
}
//传输数组到C
func TestArrToC() {
	mat := make([]int32, 5)
	mat[0] = 200
	mat[1] = 100
	mat[2] = 101
	mat[3] = 102
	mat[4] = 1030
	var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&mat))
	//fmt.Println("TestArrToC==>", arr0Hdr.Len)
	// tmat := ([]C.int)(unsafe.Pointer(mat))
	//C.SetMat((*C.int)(unsafe.Pointer(arr0Hdr.Data)), C.int(arr0Hdr.Len))
	C.SetMat(unsafe.Pointer(arr0Hdr.Data), C.int(arr0Hdr.Len))

	for _, v := range mat {
		fmt.Println("TestArrToC:", v)
	}
}
//获取c的矩阵
// func TestMatFromC() {
// 	mat := C.Process()
// 	fmt.Println(reflect.TypeOf(mat).String())
// 	fmt.Println("TestMatFromC:", *mat)

// 	if mat != nil {
// 		C.free(unsafe.Pointer(mat))
// 		mat = nil
// 	}
// }
//获取c的矩阵
func TestMatFromC() {
	mat := (*float32)(unsafe.Pointer(C.Process()))
	tlen := uintptr(unsafe.Sizeof(float32(0)))
	pmat := uintptr(unsafe.Pointer(mat))
	for i := 0; i < 5; i++ {
		for j := 0; j < 5; j++ {
			kk := (*float32)(unsafe.Pointer(pmat + uintptr(i * 5 + j) * tlen))
			fmt.Println("TestMatFromC:", *kk)
		}
	}
	if mat != nil {
		C.free(unsafe.Pointer(mat))
		mat = nil
	}
}
func TestMatFromC2() {

	// type StringHeader struct {
	// 	Data uintptr
	// 	Len  int
	// }

	// type SliceHeader struct {
	// 	Data uintptr
	// 	Len  int
	// 	Cap  int
	// }
	//直接弄到切片
	res := unsafe.Pointer(C.Process())
	var mat []float32
	var tmat = (*reflect.SliceHeader)(unsafe.Pointer(&mat))
	tmat.Data = uintptr(res)
	tmat.Len = 25
	for _, v := range mat {
		fmt.Println("TestMatFromC2:", v)
	}
	if res != nil {
		C.free(unsafe.Pointer(res))
		res = nil
	}
}
//测试类
func TestMat() {
	mat := C.New_Mat(10000, 1000)
	fmt.Println(C.Mat_H(mat), C.Mat_W(mat), C.Mat_At(mat, 1, 1))
	for i := 0; i < int(C.Mat_H(mat)); i++ {
		for j := 0; j < int(C.Mat_W(mat)); j++ {
			//fmt.Println(C.Mat_At(mat, C.int(i), C.int(j)))
		}
	}
	C.Free_Mat(mat)
}

main.go

package main

func main() {
	// TestCtruct()
	// TestCReturnStructPtr()
	// TestCharPtrToC()
	// TestCAdd()
	//TestCDiv()
	//TestMatFromC()
	TestMat()
}

说明:指向库和include

/*
#cgo !windows pkg-config: opencv4
#cgo CXXFLAGS:   --std=c++11
#cgo windows  CPPFLAGS:   -IC:/opencv/build/install/include
#cgo windows  LDFLAGS:    -LC:/opencv/build/install/x64/mingw/lib -lopencv_core400 -lopencv_face400 -lopencv_videoio400 -lopencv_imgproc400 -lopencv_highgui400 -lopencv_imgcodecs400 -lopencv_objdetect400 -lopencv_features2d400 -lopencv_video400 -lopencv_dnn400 -lopencv_xfeatures2d400 -lopencv_plot400 -lopencv_tracking400 -lopencv_img_hash400 -lopencv_calib3d400
*/
test.c文件:
#include <stdlib.h>
#include <stdio.h>
void TestSayHello(const char* s) {
    puts(s);
}

testcpp.h文件:
#include <stdlib.h>
#include <stdio.h>

void TestCpp(char* msg);

testcpp.cpp文件:
#include <iostream>
extern "C" {
    #include "testcpp.h"
}

void TestCpp(char* msg)
{
    std::cout<< "cpp:" << msg;
}
package main

/*
#cgo LDFLAGS: -lm
#include <stdio.h>
#include "testcpp.h"
*/
/*
//c++
static void SayHello(const char* s) {
    puts(s);
}
void TestSayHello(const char*);
*/
import "C"

func main() {
	//C.puts(C.CString("Hello, 世界\n"))
	//C.TestSayHello(C.CString("Hello, World111111\n"))
	C.TestCpp(C.CString("cpp\n"))
}

使用动态库方法:

go工程目录结构

|-project
|  |-lib
|  |  |-Algorithm.dll
|  |-include
|  |  |-Algorithm.h
|  |-src
|  |  |-main.go
|  |-pkg
|  |-bin
//Algorithm.h
#ifndef ALGORITHM_DLL_H
#define ALGORITHM_DLL_H

namespace Algorithm {
    extern "C" __declspec(dllexport) int add(int a, int b);
    extern "C" __declspec(dllexport) int sub(int a, int b);
}

#endif // !ALGORITHM_DLL_H

#include "Algorithm.h"

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

int Algorithm::sub(int a, int b)
{
    return a - b;
}
//方法一
// main.go
package main

import (
    "fmt"
)

import (
    "syscall"
)

func callDll() {
    dll32 := syscall.NewLazyDLL("lib/Algorithm.dll")
    fmt.Println("call dll:", dll32.Name)
    g := dll32.NewProc("add")
    ret, _, _ := g.Call(uintptr(4), uintptr(5))
    fmt.Println("result is :", ret)
}

func main() {
    fmt.Println("Hello World!")
    callDll()
    //fmt.Println(C.add(2, 1))
}


//方法二
// main.go
package main

/*
#cgo CFLAGS: -Iinclude
#cgo LDFLAGS: -Llib -lAlgorithm
#include "Algorithm.h"
*/
import "C"

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World!")

    ret := int32(C.add(2, 3))

    if ret != 0 {
        fmt.Println("1111")
    } else {
        fmt.Println("2222")
    }
}

PS:方法二需要将动态库和go程序放在同一目录下

4.环境搭建

需要安装如下程序

mingw-w64-install.exe

注意事项:安装此程序安装过程中要选择64位版本的,安装完后要将mingw的bin目录添加到path环境变量中

其实这里的32位版本或者64位版本的要求就是要求和go下使用的gcc版本(即32位或64位一致)

CGo类型

在Go语言中访问C语言的符号时,一般是通过虚拟的“C”包访问,比如C.int对应C语言的int类型。有些C语言的类型是由多个关键字组成,但通过虚拟的“C”包访问C语言类型时名称部分不能有空格字符,比如unsigned int不能直接通过C.unsigned int访问。因此CGO为C语言的基础数值类型都提供了相应转换规则,比如C.uint对应C语言的unsigned int。

Go语言中数值类型和C语言数据类型基本上是相似的,以下是它们的对应关系表。

C语言类型

CGO类型

Go语言类型

char

C.char

byte

singed char

C.schar

int8

unsigned char

C.uchar

uint8

short

C.short

int16

unsigned short

C.ushort

uint16

int

C.int

int32

unsigned int

C.uint

uint32

long

C.long

int32

unsigned long

C.ulong

uint32

long long int

C.longlong

int64

unsigned long long int

C.ulonglong

uint64

float

C.float

float32

double

C.double

float64

size_t

C.size_t

uint

需要注意的是,虽然在C语言中int、short等类型没有明确定义内存大小,但是在CGO中它们的内存大小是确定的。在CGO中,C语言的int和long类型都是对应4个字节的内存大小,size_t类型可以当作Go语言uint无符号整数类型对待。

CGO中,虽然C语言的int固定为4字节的大小,但是Go语言自己的int和uint却在32位和64位系统下分别对应4个字节和8个字节大小。如果需要在C语言中访问Go语言的int类型,可以通过GoInt类型访问,GoInt类型在CGO工具生成的_cgo_export.h头文件中定义。其实在_cgo_export.h头文件中,每个基本的Go数值类型都定义了对应的C语言类型,它们一般都是以单词Go为前缀。下面是64位环境下,_cgo_export.h头文件生成的Go数值类型的定义,其中GoInt和GoUint类型分别对应GoInt64和GoUint64:

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef float GoFloat32;
typedef double GoFloat64;

除了GoInt和GoUint之外,我们并不推荐直接访问GoInt32、GoInt64等类型。更好的做法是通过C语言的C99标准引入的<stdint.h>头文件。为了提高C语言的可移植性,在<stdint.h>文件中,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。

C语言类型

CGO类型

Go语言类型

int8_t

C.int8_t

int8

uint8_t

C.uint8_t

uint8

int16_t

C.int16_t

int16

uint16_t

C.uint16_t

uint16

int32_t

C.int32_t

int32

uint32_t

C.uint32_t

uint32

int64_t

C.int64_t

int64

uint64_t

C.uint64_t

uint64

前文说过,如果C语言的类型是由多个关键字组成,则无法通过虚拟的“C”包直接访问(比如C语言的unsigned short不能直接通过C.unsigned short访问)。但是,在<stdint.h>中通过使用C语言的typedef关键字将unsigned short重新定义为uint16_t这样一个单词的类型后,我们就可以通过C.uint16_t访问原来的unsigned short类型了。对于比较复杂的C语言类型,推荐使用typedef关键字提供一个规则的类型命名,这样更利于在CGO中访问。

Go 字符串和切片

在CGO生成的_cgo_export.h头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型:

//在$(GOPATH)/src中执行以下命令,生成 _cgo_export.h
//go tool cgo test.go
typedef struct { const char *p; GoInt n; } GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

不过需要注意的是,其中只有字符串和切片在CGO中有一定的使用价值,因为CGO为他们的某些GO语言版本的操作函数生成了C语言版本,因此二者可以在Go调用C语言函数时马上使用;而CGO并未针对其他的类型提供相关的辅助函数,且Go语言特有的内存模型导致我们无法保持这些由Go语言管理的内存指针,所以它们C语言环境并无使用的价值。

在导出的C语言函数中我们可以直接使用Go字符串和切片。假设有以下两个导出函数:

//export helloString
func helloString(s string) {}

//export helloSlice
func helloSlice(s []byte) {}

CGO生成的_cgo_export.h头文件会包含以下的函数声明:

extern void helloString(GoString p0);
extern void helloSlice(GoSlice p0);

不过需要注意的是,如果使用了GoString类型则会对_cgo_export.h头文件产生依赖,而这个头文件是动态输出的。

Go1.10针对Go字符串增加了一个_GoString_预定义类型,可以降低在cgo代码中可能对_cgo_export.h头文件产生的循环依赖的风险。我们可以调整helloString函数的C语言声明为:

extern void helloString(_GoString_ p0);

因为_GoString_是预定义类型,我们无法通过此类型直接访问字符串的长度和指针等信息。Go1.10同时也增加了以下两个函数用于获取字符串结构中的长度和指针信息:

size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);

更严谨的做法是为C语言函数接口定义严格的头文件,然后基于稳定的头文件实现代码。

结构体、联合、枚举类型

C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。在Go语言中,我们可以通过C.struct_xxx来访问C语言中定义的struct xxx结构体类型。结构体的内存布局按照C语言的通用对齐规则,在32位Go语言环境C语言结构体也按照32位对齐规则,在64位Go语言环境按照64位的对齐规则。对于指定了特殊对齐规则的结构体,无法在CGO中访问。

结构体的简单用法如下:

/*
struct A {
    int i;
    float f;
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a.i)
    fmt.Println(a.f)
}

如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问:

/*
struct A {
    int type; // type 是 Go 语言的关键字
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 对应 type
}

但是如果有2个成员:一个是以Go语言关键字命名,另一个刚好是以下划线和Go语言关键字命名,那么以Go语言关键字命名的成员将无法访问(被屏蔽):

/*
struct A {
    int   type;  // type 是 Go 语言的关键字
    float _type; // 将屏蔽CGO对 type 成员的访问
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 对应 _type
}

语言结构体中位字段对应的成员无法在Go语言中访问,如果需要操作位字段成员,需要通过在C语言中定义辅助函数来完成。对应零长数组的成员,无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。

/*
struct A {
    int   size: 10; // 位字段无法访问
    float arr[];    // 零长的数组也无法访问
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a.size) // 错误: 位字段无法访问
    fmt.Println(a.arr)  // 错误: 零长的数组也无法访问
}

在C语言中,我们无法直接访问Go语言定义的结构体类型。

对于联合类型,我们可以通过C.union_xxx来访问C语言中定义的union xxx类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。

/*
#include <stdint.h>

union B1 {
    int i;
    float f;
};

union B2 {
    int8_t i8;
    int64_t i64;
};
*/
import "C"
import "fmt"

func main() {
    var b1 C.union_B1;
    fmt.Printf("%T\n", b1) // [4]uint8

    var b2 C.union_B2;
    fmt.Printf("%T\n", b2) // [8]uint8
}

如果需要操作C语言的联合类型变量,一般有三种方法:第一种是在C语言中定义辅助函数;第二种是通过Go语言的"encoding/binary"手工解码成员(需要注意大端小端问题);第三种是使用unsafe包强制转型为对应类型(这是性能最好的方式)。下面展示通过unsafe包访问联合类型成员的方式:

/*
#include <stdint.h>

union B {
    int i;
    float f;
};
*/
import "C"
import "fmt"

func main() {
    var b C.union_B;
    fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b)))
    fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b)))
}

虽然unsafe包访问最简单、性能也最好,但是对于有嵌套联合类型的情况处理会导致问题复杂化。对于复杂的联合类型,推荐通过在C语言中定义辅助函数的方式处理。

对于枚举类型,我们可以通过C.enum_xxx来访问C语言中定义的enum xxx结构体类型。

/*
enum C {
    ONE,
    TWO,
};
*/
import "C"
import "fmt"

func main() {
    var c C.enum_C = C.TWO
    fmt.Println(c)
    fmt.Println(C.ONE)
    fmt.Println(C.TWO)
}

在C语言中,枚举类型底层对应int类型,支持负数类型的值。我们可以通过C.ONE、C.TWO等直接访问定义的枚举值。

数组、字符串和切片

在C语言中,数组名其实对应于一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改;当把数组名传递给一个函数时,实际上传递的是数组第一个元素的地址。为了讨论方便,我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。

在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。

Go语言和C语言的数组、字符串和切片之间的相互转换可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。

CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

// 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

// Go []byte slice to C array
// The C array 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.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

其中C.CString针对输入的Go字符串,克隆一个C语言格式的字符串;返回的字符串由C语言的malloc函数分配,不使用时需要通过C语言的free函数释放。C.CBytes函数的功能和C.CString类似,用于从输入的Go语言字节切片克隆一个C语言版本的字节数组,同样返回的数组需要在合适的时候释放。C.GoString用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。C.GoStringN是另一个字符数组克隆函数。C.GoBytes用于从C语言数组,克隆一个Go语言字节切片。

该组辅助函数都是以克隆的方式运行。当Go语言字符串和切片向C语言转换时,克隆的内存由C语言的malloc函数分配,最终可以通过free函数释放。当C语言字符串或数组向Go语言转换时,克隆的内存由Go语言分配管理。通过该组转换函数,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。

在reflect包中有字符串和切片的定义:

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

如果不希望单独分配内存,可以在Go语言中直接访问C语言的内存空间:

/*
static char arr[10];
static char *s = "Hello";
*/
import "C"
import "fmt"

func main() {
    // 通过 reflect.SliceHeader 转换
    var arr0 []byte
    var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
    arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0]))
    arr0Hdr.Len = 10
    arr0Hdr.Cap = 10

    // 通过切片语法转换
    arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10]

    var s0 string
    var s0Hdr := (*reflect.StringHeader)(unsafe.Pointer(&s0))
    s0Hdr.Data = uintptr(unsafe.Pointer(C.s))
    s0Hdr.Len = int(C.strlen(C.s))

    sLen := int(C.strlen(C.s))
    s1 := string((*[31]byte)(unsafe.Pointer(&C.s[0]))[:sLen:sLen])
}

因为Go语言的字符串是只读的,用户需要自己保证Go字符串在使用期间,底层对应的C字符串内容不会发生变化、内存不会被提前释放掉。

在CGO中,会为字符串和切片生成和上面结构对应的C语言版本的结构体:

typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

在C语言中可以通过GoString和GoSlice来访问Go语言的字符串和切片。如果是Go语言中数组类型,可以将数组转为切片后再行转换。如果字符串或切片对应的底层内存空间由Go语言的运行时管理,那么在C语言中不能长时间保存Go内存对象。

指针间的转换

在C语言中,不同类型的指针是可以显式或隐式转换的,如果是隐式只是会在编译时给出一些警告信息。但是Go语言对于不同类型的转换非常严格,任何C语言中可能出现的警告信息在Go语言中都可能是错误!指针是C语言的灵魂,指针间的自由转换也是cgo代码中经常要解决的第一个重要的问题。

在Go语言中两个指针的类型完全一致则不需要转换可以直接通用。如果一个指针类型是用type命令在另一个指针类型基础之上构建的,换言之两个指针底层是相同完全结构的指针,那么我我们可以通过直接强制转换语法进行指针间的转换。但是cgo经常要面对的是2个完全不同类型的指针间的转换,原则上这种操作在纯Go语言代码是严格禁止的。

cgo存在的一个目的就是打破Go语言的禁止,恢复C语言应有的指针的自由转换和指针运算。以下代码演示了如何将X类型的指针转化为Y类型的指针:

var p *X
var q *Y

q = (*Y)(unsafe.Pointer(p)) // *X => *Y
p = (*X)(unsafe.Pointer(q)) // *Y => *X

为了实现X类型指针到Y类型指针的转换,我们需要借助unsafe.Pointer作为中间桥接类型实现不同类型指针之间的转换。unsafe.Pointer指针类型类似C语言中的void*类型的指针。

任何类型的指针都可以通过强制转换为unsafe.Pointer指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。

数值和指针的转换

不同类型指针间的转换看似复杂,但是在cgo中已经算是比较简单的了。在C语言中经常遇到用普通数值表示指针的场景,也就是说如何实现数值和指针的转换也是cgo需要面对的一个问题。

为了严格控制指针的使用,Go语言禁止将数值类型直接转为指针类型!不过,Go语言针对unsafe.Pointr指针类型特别定义了一个uintptr类型。我们可以uintptr为中介,实现数值类型到unsafe.Pointr指针类型到转换。再结合前面提到的方法,就可以实现数值和指针的转换了。

下面流程图演示了如何实现int32类型到C语言的char字符串指针类型的相互转换:

图 2.3-2 int32和char指针转换

转换分为几个阶段,在每个阶段实现一个小目标:首先是int32到uintptr类型,然后是uintptr到unsafe.Pointr指针类型,最后是unsafe.Pointr指针类型到*C.char类型。

切片间的转换

在C语言中数组也一种指针,因此两个不同类型数组之间的转换和指针间转换基本类似。但是在Go语言中,数组或数组对应的切片都不再是指针类型,因此我们也就无法直接实现不同类型的切片之间的转换。

不过Go语言的reflect包提供了切片类型的底层结构,再结合前面讨论到不同类型之间的指针转换技术就可以实现[]X和[]Y类型的切片转换:

var p []X
var q []Y

pHdr := (*reflect.SliceHeader)(unsafe.Pointer(&p))
qHdr := (*reflect.SliceHeader)(unsafe.Pointer(&q))

pHdr.Data = qHdr.Data
pHdr.Len = qHdr.Len * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])

不同切片类型之间转换的思路是先构造一个空的目标切片,然后用原有的切片底层数据填充目标切片。如果X和Y类型的大小不同,需要重新设置Len和Cap属性。需要注意的是,如果X或Y是空类型,上述代码中可能导致除0错误,实际代码需要根据情况酌情处理。

下面演示了切片间的转换的具体流程:

图 2.3-3 X类型切片转Y类型切片

针对CGO中常用的功能,作者封装了 "github.com/chai2010/cgo" 包,提供基本的转换功能,具体的细节可以参考实现代码。

函数调用

C函数的返回值

对于有返回值的C函数,我们可以正常获取返回值。

/*
static int div(int a, int b) {
    return a/b;
}
*/
import "C"
import "fmt"

func main() {
    v := C.div(6, 3)
    fmt.Println(v)
}

上面的div函数实现了一个整数除法的运算,然后通过返回值返回除法的结果。

不过对于除数为0的情形并没有做特殊处理。如果希望在除数为0的时候返回一个错误,其他时候返回正常的结果。因为C语言不支持返回多个结果,因此<errno.h>标准库提供了一个errno宏用于返回错误状态。我们可以近似地将errno看成一个线程安全的全局变量,可以用于记录最近一次错误的状态码。

改进后的div函数实现如下:

#include <errno.h>

int div(int a, int b) {
    if(b == 0) {
        errno = EINVAL;
        return 0;
    }
    return a/b;
}

CGO也针对<errno.h>标准库的errno宏做的特殊支持:在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno错误状态。

/*
#include <errno.h>

static int div(int a, int b) {
    if(b == 0) {
        errno = EINVAL;
        return 0;
    }
    return a/b;
}
*/
import "C"
import "fmt"

func main() {
    v0, err0 := C.div(2, 1)
    fmt.Println(v0, err0)

    v1, err1 := C.div(1, 0)
    fmt.Println(v1, err1)
}

运行这个代码将会产生以下输出:

2 <nil>
0 invalid argument

我们可以近似地将div函数看作为以下类型的函数:

func C.div(a, b C.int) (C.int, [error])

第二个返回值是可忽略的error接口类型,底层对应 syscall.Errno 错误类型。

void函数的返回值

C语言函数还有一种没有返回值类型的函数,用void表示返回值类型。一般情况下,我们无法获取void类型函数的返回值,因为没有返回值可以获取。前面的例子中提到,cgo对errno做了特殊处理,可以通过第二个返回值来获取C语言的错误状态。对于void类型函数,这个特性依然有效。

以下的代码是获取没有返回值函数的错误状态码:

//static void noreturn() {}
import "C"
import "fmt"

func main() {
    _, err := C.noreturn()
    fmt.Println(err)
}

此时,我们忽略了第一个返回值,只获取第二个返回值对应的错误码。

我们也可以尝试获取第一个返回值,它对应的是C语言的void对应的Go语言类型:

//static void noreturn() {}
import "C"
import "fmt"

func main() {
    v, _ := C.noreturn()
    fmt.Printf("%#v", v)
}

运行这个代码将会产生以下输出:

main._Ctype_void{}

我们可以看出C语言的void类型对应的是当前的main包中的_Ctype_void类型。其实也将C语言的noreturn函数看作是返回_Ctype_void类型的函数,这样就可以直接获取void类型函数的返回值:

//static void noreturn() {}
import "C"
import "fmt"

func main() {
    fmt.Println(C.noreturn())
}

运行这个代码将会产生以下输出:

[]

其实在CGO生成的代码中,_Ctype_void类型对应一个0长的数组类型[0]byte,因此fmt.Println输出的是一个表示空数值的方括弧。

C调用Go导出函数

CGO还有一个强大的特性:将Go函数导出为C语言函数。这样的话我们可以定义好C语言接口,然后通过Go语言实现。在本章的第一节快速入门部分我们已经展示过Go语言导出C语言函数的例子。

下面是用Go语言重新实现本节开始的add函数:

import "C"

//export add
func add(a, b C.int) C.int {
    return a+b
}

add函数名以小写字母开头,对于Go语言来说是包内的私有函数。但是从C语言角度来看,导出的add函数是一个可全局访问的C语言函数。如果在两个不同的Go语言包内,都存在一个同名的要导出为C语言函数的add函数,那么在最终的链接阶段将会出现符号重名的问题。

CGO生成的 _cgo_export.h 文件回包含导出后的C语言函数的声明。我们可以在纯C源文件中包含 _cgo_export.h 文件来引用导出的add函数。如果希望在当前的CGO文件中马上使用导出的C语言add函数,则无法引用 _cgo_export.h 文件。因为_cgo_export.h 文件的生成需要依赖当前文件可以正常构建,而如果当前文件内部循环依赖还未生成的_cgo_export.h 文件将会导致cgo命令错误。

#include "_cgo_export.h"

void foo() {
    add(1, 1);
}

当导出C语言接口时,需要保证函数的参数和返回值类型都是C语言友好的类型,同时返回值不得直接或间接包含Go语言内存空间的指针。

内部机制

CGO特性主要是通过一个叫cgo的命令行工具来辅助输出Go和C之间的桥接代码

CGO生成的中间文件

要了解CGO技术的底层秘密首先需要了解CGO生成了哪些中间文件。我们可以在构建一个cgo包时增加一个-work输出中间生成文件所在的目录并且在构建完成时保留中间文件。如果是比较简单的cgo代码我们也可以直接通过手工调用go tool cgo命令来查看生成的中间文件。

在一个Go源文件中,如果出现了import "C"指令则表示将调用cgo命令生成对应的中间文件。下图是cgo生成的中间文件的简单示意图:

图 2.5-1 cgo生成的中间文件

包中有4个Go文件,其中nocgo开头的文件中没有import "C"指令,其它的2个文件则包含了cgo代码。cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件,比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。然后会为整个包创建一个 _cgo_gotypes.go Go文件,其中包含Go语言部分辅助代码。此外还会创建一个 _cgo_export.h 和 _cgo_export.c 文件,对应Go语言导出到C语言的类型和函数。

Go调用C函数是CGO最常见的应用场景,我们将从最简单的例子入手分析Go调用C函数的详细流程。

具体代码如下(main.go):

package main
//int sum(int a, int b) { return a+b; }
import "C"
func main() {
    println(C.sum(1, 1))
}

首先构建并运行该例子没有错误。然后通过cgo命令行工具在_obj目录生成中间文件:

$ go tool cgo main.go

查看_obj目录生成中间文件:

$ ls _obj | awk '{print $NF}'
_cgo_.o
_cgo_export.c
_cgo_export.h
_cgo_flags
_cgo_gotypes.go
_cgo_main.c
main.cgo1.go
main.cgo2.c

其中_cgo_.o、_cgo_flags和_cgo_main.c文件和我们的代码没有直接的逻辑关联,可以暂时忽略。

我们先查看main.cgo1.go文件,它是main.go文件展开虚拟C包相关函数和变量后的Go代码:

package main

//int sum(int a, int b) { return a+b; }
import _ "unsafe"

func main() {
    println((_Cfunc_sum)(1, 1))
}

其中C.sum(1, 1)函数调用被替换成了(_Cfunc_sum)(1, 1)。每一个C.xxx形式的函数都会被替换为_Cfunc_xxx格式的纯Go函数,其中前缀_Cfunc_表示这是一个C函数,对应一个私有的Go桥接函数。

_Cfunc_sum函数在cgo生成的_cgo_gotypes.go文件中定义:

//go:cgo_unsafe_args
func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) {
    _cgo_runtime_cgocall(_cgo_506f45f9fa85_Cfunc_sum, uintptr(unsafe.Pointer(&p0)))
    if _Cgo_always_false {
        _Cgo_use(p0)
        _Cgo_use(p1)
    }
    return
}

_Cfunc_sum函数的参数和返回值_Ctype_int类型对应C.int类型,命名的规则和_Cfunc_xxx类似,不同的前缀用于区分函数和类型。

其中_cgo_runtime_cgocall对应runtime.cgocall函数,函数的声明如下:

func runtime.cgocall(fn, arg unsafe.Pointer) int32

第一个参数是C语言函数的地址,第二个参数是存放C语言函数对应的参数结构体的地址。

在这个例子中,被传入C语言函数_cgo_506f45f9fa85_Cfunc_sum也是cgo生成的中间函数。函数在main.cgo2.c定义:

void _cgo_506f45f9fa85_Cfunc_sum(void *v) {
    struct {
        int p0;
        int p1;
        int r;
        char __pad12[4];
    } __attribute__((__packed__)) *a = v;
    char *stktop = _cgo_topofstack();
    __typeof__(a->r) r;
    _cgo_tsan_acquire();
    r = sum(a->p0, a->p1);
    _cgo_tsan_release();
    a = (void*)((char*)a + (_cgo_topofstack() - stktop));
    a->r = r;
}

这个函数参数只有一个void范型的指针,函数没有返回值。真实的sum函数的函数参数和返回值均通过唯一的参数指针类实现。

_cgo_506f45f9fa85_Cfunc_sum函数的指针指向的结构为:

    struct {
        int p0;
        int p1;
        int r;
        char __pad12[4];
    } __attribute__((__packed__)) *a = v;

其中p0成员对应sum的第一个参数,p1成员对应sum的第二个参数,r成员,__pad12用于填充结构体保证对齐CPU机器字的整倍数。

然后从参数指向的结构体获取调用参数后开始调用真实的C语言版sum函数,并且将返回值保持到结构体内返回值对应的成员。

因为Go语言和C语言有着不同的内存模型和函数调用规范。其中_cgo_topofstack函数相关的代码用于C函数调用后恢复调用栈。_cgo_tsan_acquire和_cgo_tsan_release则是用于扫描CGO相关的函数则是对CGO相关函数的指针做相关检查。

C.sum的整个调用流程图如下:

图 2.5-2 调用C函数

其中runtime.cgocall函数是实现Go语言到C语言函数跨界调用的关键。更详细的细节可以参考 内部的代码注释和 runtime.cgocall 函数的实现。

C调用Go函数

在简单分析了Go调用C函数的流程后,我们现在来分析C反向调用Go函数的流程。同样,我们现构造一个Go语言版本的sum函数,文件名同样为main.go:

package main
//int sum(int a, int b);
import "C"

//export sum
func sum(a, b C.int) C.int {
    return a + b
}
func main() {}

CGO的语法细节不在赘述。为了在C语言中使用sum函数,我们需要将Go代码编译为一个C静态库:

$ go build -buildmode=c-archive -o sum.a sum.go

如果没有错误的话,以上编译命令将生成一个sum.a静态库和sum.h头文件。其中sum.h头文件将包含sum函数的声明,静态库中将包含sum函数的实现。

要分析生成的C语言版sum函数的调用流程,同样需要分析cgo生成的中间文件:

$ go tool cgo main.go

_obj目录还是生成类似的中间文件。为了查看方便,我们刻意忽略了无关的几个文件:

$ ls _obj | awk '{print $NF}'
_cgo_export.c
_cgo_export.h
_cgo_gotypes.go
main.cgo1.go
main.cgo2.c

其中_cgo_export.h文件的内容和生成C静态库时产生的sum.h头文件是同一个文件,里面同样包含sum函数的声明。

既然C语言是主调用者,我们需要先从C语言版sum函数的实现开始分析。C语言版本的sum函数在生成的_cgo_export.c文件中(该文件包含的是Go语言导出函数对应的C语言函数实现):

int sum(int p0, int p1)
{
    __SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
    struct {
        int p0;
        int p1;
        int r0;
        char __pad0[4];
    } __attribute__((__packed__)) a;
    a.p0 = p0;
    a.p1 = p1;
    _cgo_tsan_release();
    crosscall2(_cgoexp_8313eaf44386_sum, &a, 16, _cgo_ctxt);
    _cgo_tsan_acquire();
    _cgo_release_context(_cgo_ctxt);
    return a.r0;
}

sum函数的内容采用和前面类似的技术,将sum函数的参数和返回值打包到一个结构体中,然后通过runtime/cgo.crosscall2函数将结构体传给_cgoexp_8313eaf44386_sum函数执行。

runtime/cgo.crosscall2函数采用汇编语言实现,它对应的函数声明如下:

func runtime/cgo.crosscall2(
    fn func(a unsafe.Pointer, n int32, ctxt uintptr),
    a unsafe.Pointer, n int32,
    ctxt uintptr,
)

其中关键的是fn和a,fn是中间代理函数的指针,a是对应调用参数和返回值的结构体指针。

中间的_cgoexp_8313eaf44386_sum代理函数在_cgo_gotypes.go文件:

func _cgoexp_8313eaf44386_sum(a unsafe.Pointer, n int32, ctxt uintptr) {
    fn := _cgoexpwrap_8313eaf44386_sum
    _cgo_runtime_cgocallback(**(**unsafe.Pointer)(unsafe.Pointer(&fn)), a, uintptr(n), ctxt);
}

func _cgoexpwrap_8313eaf44386_sum(p0 _Ctype_int, p1 _Ctype_int) (r0 _Ctype_int) {
    return sum(p0, p1)
}

内部将sum的包装函数_cgoexpwrap_8313eaf44386_sum作为函数指针,然后由_cgo_runtime_cgocallback函数完成C语言到Go函数的回调工作。

_cgo_runtime_cgocallback函数对应runtime.cgocallback函数,函数的类型如下:

func runtime.cgocallback(fn, frame unsafe.Pointer, framesize, ctxt uintptr)

参数分别是函数指针,函数参数和返回值对应结构体的指针,函数调用帧大小和上下文参数。

整个调用流程图如下:

图 2.5-3 调用导出的Go函数

其中runtime.cgocallback函数是实现C语言到Go语言函数跨界调用的关键。更详细的细节可以参考相关函数的实现。

CGO内存模型

如果在CGO处理的跨语言函数调用时涉及到了指针的传递,则可能会出现Go语言和C语言共享某一段内存的场景。我们知道C语言的内存在分配之后就是稳定的,但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针,那么以旧指针访问Go对象时会导致程序崩溃。

Go访问C内存

C语言空间的内存是稳定的,只要不是被人为提前释放,那么在Go语言空间可以放心大胆地使用。在Go语言访问C语言内存是最简单的情形,我们在之前的例子中已经见过多次。

因为Go语言实现的限制,我们无法在Go语言中创建大于2GB内存的切片(具体请参考makeslice实现代码)。不过借助cgo技术,我们可以在C语言环境创建大于2GB的内存,然后转为Go语言的切片使用:

package main

/*
#include <stdlib.h>

void* makeslice(size_t memsize) {
    return malloc(memsize);
}
*/
import "C"
import "unsafe"

func makeByteSlize(n int) []byte {
    p := C.makeslice(C.size_t(n))
    return ((*[1 << 31]byte)(p))[0:n:n]
}

func freeByteSlice(p []byte) {
    C.free(unsafe.Pointer(&p[0]))
}

func main() {
    s := makeByteSlize(1<<32+1)
    s[len[s]-1] = 1234
    print(s[len[s]-1])
    freeByteSlice(p)
}

例子中我们通过makeByteSlize来创建大于4G内存大小的切片,从而绕过了Go语言实现的限制(需要代码验证)。而freeByteSlice辅助函数则用于释放从C语言函数创建的切片。

因为C语言内存空间是稳定的,基于C语言内存构造的切片也是绝对稳定的,不会因为Go语言栈的变化而被移动。

C临时访问传入的Go内存

cgo之所以存在的一大因素是为了方便在Go语言中接纳吸收过去几十年来使用C/C语言软件构建的大量的软件资源。C/C很多库都是需要通过指针直接处理传入的内存数据的,因此cgo中也有很多需要将Go内存传入C语言函数的应用场景。

假设一个极端场景:我们将一块位于某goroutinue的栈上的Go语言内存传入了C语言函数后,在此C语言函数执行期间,此goroutinue的栈因为空间不足的原因发生了扩展,也就是导致了原来的Go语言内存被移动到了新的位置。但是此时此刻C语言函数并不知道该Go语言内存已经移动了位置,仍然用之前的地址来操作该内存——这将将导致内存越界。以上是一个推论(真实情况有些差异),也就是说C访问传入的Go内存可能是不安全的!

当然有RPC远程过程调用的经验的用户可能会考虑通过完全传值的方式处理:借助C语言内存稳定的特性,在C语言空间先开辟同样大小的内存,然后将Go的内存填充到C的内存空间;返回的内存也是如此处理。下面的例子是这种思路的具体实现:

package main

/*
void printString(const char* s) {
    printf("%s", s);
}
*/
import "C"

func printString(s string) {
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))

    C.printString(cs)
}

func main() {
    s := "hello"
    printString(s)
}

在需要将Go的字符串传入C语言时,先通过C.CString将Go语言字符串对应的内存数据复制到新创建的C语言内存空间上。上面例子的处理思路虽然是安全的,但是效率极其低下(因为要多次分配内存并逐个复制元素),同时也极其繁琐。

为了简化并高效处理此种向C语言传入Go语言内存的问题,cgo针对该场景定义了专门的规则:在CGO调用的C语言函数返回前,cgo保证传入的Go语言内存在此期间不会发生移动,C语言函数可以大胆地使用Go语言的内存!

根据新的规则我们可以直接传入Go字符串的内存:

package main

/*
#include<stdio.h>

void printString(const char* s, int n) {
    int i;
    for(i = 0; i < n; i++) {
        putchar(s[i]);
    }
    putchar('\n');
}
*/
import "C"

func printString(s string) {
    p := (*reflect.StringHeader)(unsafe.Pointer(&s))
    C.printString((*C.char)(unsafe.Pointer(p.Data)), C.int(len(s)))
}

func main() {
    s := "hello"
    printString(s)
}

现在的处理方式更加直接,且避免了分配额外的内存。完美的解决方案!

任何完美的技术都有被滥用的时候,CGO的这种看似完美的规则也是存在隐患的。我们假设调用的C语言函数需要长时间运行,那么将会导致被他引用的Go语言内存在C语言返回前不能被移动,从而可能间接地导致这个Go内存栈对应的goroutine不能动态伸缩栈内存,也就是可能导致这个goroutine被阻塞。因此,在需要长时间运行的C语言函数(特别是在纯CPU运算之外,还可能因为需要等待其它的资源而需要不确定时间才能完成的函数),需要谨慎处理传入的Go语言内存。

不过需要小心的是在取得Go内存后需要马上传入C语言函数,不能保存到临时变量后再间接传入C语言函数。因为CGO只能保证在C函数调用之后被传入的Go语言内存不会发生移动,它并不能保证在传入C函数之前内存不发生变化。

以下代码是错误的:

// 错误的代码
tmp := uintptr(unsafe.Pointer(&x))
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

因为tmp并不是指针类型,在它获取到Go对象地址之后x对象可能会被移动,但是因为不是指针类型,所以不会被Go语言运行时更新成新内存的地址。在非指针类型的tmp保持Go对象的地址,和在C语言环境保持Go对象的地址的效果是一样的:如果原始的Go对象内存发生了移动,Go语言运行时并不会同步更新它们。

C长期持有Go指针对象

作为一个Go程序员在使用CGO时潜意识会认为总是Go调用C函数。其实CGO中,C语言函数也可以回调Go语言实现的函数。特别是我们可以用Go语言写一个动态库,导出C语言规范的接口给其它用户调用。当C语言函数调用Go语言函数的时候,C语言函数就成了程序的调用方,Go语言函数返回的Go对象内存的生命周期也就自然超出了Go语言运行时的管理。简言之,我们不能在C语言函数中直接使用Go语言对象的内存。

虽然Go语言禁止在C语言函数中长期持有Go指针对象,但是这种需求是切实存在的。如果需要在C语言中访问Go语言内存对象,我们可以将Go语言内存对象在Go语言空间映射为一个int类型的id,然后通过此id来间接访问和控制Go语言对象。

以下代码用于将Go对象映射为整数类型的ObjectId,用完之后需要手工调用free方法释放该对象ID:

package main

import "sync"

type ObjectId int32

var refs struct {
    sync.Mutex
    objs map[ObjectId]interface{}
    next ObjectId
}

func init() {
    refs.Lock()
    defer refs.Unlock()

    refs.objs = make(map[ObjectId]interface{})
    refs.next = 1000
}

func NewObjectId(obj interface{}) ObjectId {
    refs.Lock()
    defer refs.Unlock()

    id := refs.next
    refs.next++

    refs.objs[id] = obj
    return id
}

func (id ObjectId) IsNil() bool {
    return id == 0
}

func (id ObjectId) Get() interface{} {
    refs.Lock()
    defer refs.Unlock()

    return refs.objs[id]
}

func (id *ObjectId) Free() interface{} {
    refs.Lock()
    defer refs.Unlock()

    obj := refs.objs[*id]
    delete(refs.objs, *id)
    *id = 0

    return obj
}

我们通过一个map来管理Go语言对象和id对象的映射关系。其中NewObjectId用于创建一个和对象绑定的id,而id对象的方法可用于解码出原始的Go对象,也可以用于结束id和原始Go对象的绑定。

下面一组函数以C接口规范导出,可以被C语言函数调用:

package main

/*
extern char* NewGoString(char* );
extern void FreeGoString(char* );
extern void PrintGoString(char* );

static void printString(const char* s) {
    char* gs = NewGoString(s);
    PrintGoString(gs);
    FreeGoString(gs);
}
*/
import "C"

//export NewGoString
func NewGoString(s *C.char) *C.char {
    gs := C.GoString(s)
    id := NewObjectId(gs)
    return (*C.char)(unsafe.Pointer(uintptr(id)))
}

//export FreeGoString
func FreeGoString(p *C.char) {
    id := ObjectId(uintptr(unsafe.Pointer(p)))
    id.Free()
}

//export PrintGoString
func PrintGoString(s *C.char) {
    id := ObjectId(uintptr(unsafe.Pointer(p)))
    gs := id.Get().(string)
    print(gs)
}

func main() {
    C.printString("hello")
}

在printString函数中,我们通过NewGoString创建一个对应的Go字符串对象,返回的其实是一个id,不能直接使用。我们借助PrintGoString函数将id解析为Go语言字符串后打印。该字符串在C语言函数中完全跨越了Go语言的内存管理,在PrintGoString调用前即使发生了栈伸缩导致的Go字符串地址发生变化也依然可以正常工作,因为该字符串对应的id是稳定的,在Go语言空间通过id解码得到的字符串也就是有效的。

静态库和动态库

CGO在使用C/C资源的时候一般有三种形式:直接使用源码;链接静态库;链接动态库。直接使用源码就是在import "C"之前的注释部分包含C代码,或者在当前包中包含C/C源文件。链接静态库和动态库的方式比较类似,都是通过在LDFLAGS选项指定要链接的库方式链接。

使用C静态库

如果CGO中引入的C/C资源有代码而且代码规模也比较小,直接使用源码是最理想的方式,但很多时候我们并没有源代码,或者从C/C源代码开始构建的过程异常复杂,这种时候使用C静态库也是一个不错的选择。静态库因为是静态链接,最终的目标程序并不会产生额外的运行时依赖,也不会出现动态库特有的跨运行时资源管理的错误。不过静态库对链接阶段会有一定要求:静态库一般包含了全部的代码,里面会有大量的符号,如果不同静态库之间出现了符号冲突则会导致链接的失败。

我们先用纯C语言构造一个简单的静态库。我们要构造的静态库名叫number,库中只有一个number_add_mod函数,用于表示数论中的模加法运算。number库的文件都在number目录下。

number/number.h头文件只有一个纯C语言风格的函数声明:
int number_add_mod(int a, int b, int mod);
number/number.c对应函数的实现:

#include "number.h"
int number_add_mod(int a, int b, int mod) {
    return (a+b)%mod;
}

因为CGO使用的是GCC命令来编译和链接C和Go桥接的代码。因此静态库也必须是GCC兼容的格式。

通过以下命令可以生成一个叫libnumber.a的静态库:

$ cd ./number
$ gcc -c -o number.o number.c
$ ar rcs libnumber.a number.o

生成libnumber.a静态库之后,我们就可以在CGO中使用该资源了 。

创建main.go文件如下:

package main

//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR}/number -lnumber
//
//#include "number.h"
import "C"
import "fmt"

func main() {
    fmt.Println(C.number_add_mod(10, 5, 12))
}

其中有两个#cgo命令,分别是编译和链接参数。CFLAGS通过-I./number将number库对应头文件所在的目录加入头文件检索路径。LDFLAGS通过-L${SRCDIR}/number将编译后number静态库所在目录加为链接库检索路径,-lnumber表示链接libnumber.a静态库。需要注意的是,在链接部分的检索路径不能使用相对路径(C/C++代码的链接程序所限制),我们必须通过cgo特有的${SRCDIR}变量将源文件对应的当前目录路径展开为绝对路径(因此在windows平台中绝对路径不能有空白符号)。

因为我们有number库的全部代码,所以我们可以用go generate工具来生成静态库,或者是通过Makefile来构建静态库。因此发布CGO源码包时,我们并不需要提前构建C静态库。

因为多了一个静态库的构建步骤,这种使用了自定义静态库并已经包含了静态库全部代码的Go包无法直接用go get安装。不过我们依然可以通过go get下载,然后用go generate触发静态库构建,最后才是go install来完成安装。

为了支持go get命令直接下载并安装,我们C语言的#include语法可以将number库的源文件链接到当前的包。

创建z_link_number_c.c文件如下:

#include "./number/number.c"

然后在执行go get或go build之类命令的时候,CGO就是自动构建number库对应的代码。这种技术是在不改变静态库源代码组织结构的前提下,将静态库转化为了源代码方式引用。这种CGO包是最完美的。

如果使用的是第三方的静态库,我们需要先下载安装静态库到合适的位置。然后在#cgo命令中通过CFLAGS和LDFLAGS来指定头文件和库的位置。对于不同的操作系统甚至同一种操作系统的不同版本来说,这些库的安装路径可能都是不同的,那么如何在代码中指定这些可能变化的参数呢?

在Linux环境,有一个pkg-config命令可以查询要使用某个静态库或动态库时的编译和链接参数。我们可以在#cgo命令中直接使用pkg-config命令来生成编译和链接参数。而且还可以通过PKG_CONFIG环境变量定制pkg-config命令。因为不同的操作系统对pkg-config命令的支持不尽相同,通过该方式很难兼容不同的操作系统下的构建参数。不过对于Linux等特定的系统,pkg-config命令确实可以简化构建参数的管理。关于pkg-config的使用细节在此我们不深入展开,大家可以自行参考相关文档。

使用C动态库

动态库出现的初衷是对于相同的库,多个进程可以共享同一个,以节省内存和磁盘资源。但是在磁盘和内存已经白菜价的今天,这两个作用已经显得微不足道了,那么除此之外动态库还有哪些存在的价值呢?从库开发角度来说,动态库可以隔离不同动态库之间的关系,减少链接时出现符号冲突的风险。而且对于windows等平台,动态库是跨越VC和GCC不同编译器平台的唯一的可行方式。

对于CGO来说,使用动态库和静态库是一样的,因为动态库也必须要有一个小的静态导出库用于链接动态库(Linux下可以直接链接so文件,但是在Windows下必须为dll创建一个.a文件用于链接)。我们还是以前面的number库为例来说明如何以动态库方式使用。

对于在macOS和Linux系统下的gcc环境,我们可以用以下命令创建number库的的动态库:

$ cd number
$ gcc -shared -o libnumber.so number.c

因为动态库和静态库的基础名称都是libnumber,只是后缀名不同而已。因此Go语言部分的代码和静态库版本完全一样:

package main
//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR}/number -lnumber
//
//#include "number.h"
import "C"
import "fmt"
func main() {
    fmt.Println(C.number_add_mod(10, 5, 12))
}

编译时GCC会自动找到libnumber.a或libnumber.so进行链接。

对于windows平台,我们还可以用VC工具来生成动态库(windows下有一些复杂的C++库只能用VC构建)。我们需要先为number.dll创建一个def文件,用于控制要导出到动态库的符号。

number.def文件的内容如下:

LIBRARY number.dll
EXPORTS
number_add_mod

其中第一行的LIBRARY指明动态库的文件名,然后的EXPORTS语句之后是要导出的符号名列表。

现在我们可以用以下命令来创建动态库(需要进入VC对应的x64命令行环境)。

$ cl /c number.c
$ link /DLL /OUT:number.dll number.obj number.def

这时候会为dll同时生成一个number.lib的导出库。但是在CGO中我们无法使用lib格式的链接库。

要生成.a格式的导出库需要通过mingw工具箱中的dlltool命令完成:

$ dlltool -dllname number.dll --def number.def --output-lib libnumber.a

生成了libnumber.a文件之后,就可以通过-lnumber链接参数进行链接了。

需要注意的是,在运行时需要将动态库放到系统能够找到的位置。对于windows来说,可以将动态库和可执行程序放到同一个目录,或者将动态库所在的目录绝对路径添加到PATH环境变量中。对于macOS来说,需要设置DYLD_LIBRARY_PATH环境变量。而对于Linux系统来说,需要设置LD_LIBRARY_PATH环境变量。

导出C静态库

CGO不仅可以使用C静态库,也可以将Go实现的函数导出为C静态库。我们现在用Go实现前面的number库的模加法函数。

创建number.go,内容如下:

package main

import "C"

func main() {}

//export number_add_mod
func number_add_mod(a, b, mod C.int) C.int {
    return (a + b) % mod
}

根据CGO文档的要求,我们需要在main包中导出C函数。对于C静态库构建方式来说,会忽略main包中的main函数,只是简单导出C函数。采用以下命令构建:

$ go build -buildmode=c-archive -o number.a

在生成number.a静态库的同时,cgo还会生成一个number.h文件。

number.h文件的内容如下(为了便于显示,内容做了精简):

#ifdef __cplusplus
extern "C" {
#endif

extern int number_add_mod(int p0, int p1, int p2);

#ifdef __cplusplus
}
#endif

其中extern "C"部分的语法是为了同时适配C和C++两种语言。核心内容是声明了要导出的number_add_mod函数。

然后我们创建一个_test_main.c的C文件用于测试生成的C静态库(用下划线作为前缀名是让为了让go build构建C静态库时忽略这个文件):

#include "number.h"
#include <stdio.h>
int main() {
    int a = 10;
    int b = 5;
    int c = 12;

    int x = number_add_mod(a, b, c);
    printf("(%d+%d)%%%d = %d\n", a, b, c, x);
    return 0;
}

通过以下命令编译并运行:

$ gcc -o a.out _test_main.c number.a
$ ./a.out
导出C动态库

CGO导出动态库的过程和静态库类似,只是将构建模式改为c-shared,输出文件名改为number.so而已:

$ go build -buildmode=c-shared -o number.so

_test_main.c文件内容不变,然后用以下命令编译并运行:

$ gcc -o a.out _test_main.c number.so
$ ./a.out
导出非main包的函数

通过go help buildmode命令可以查看C静态库和C动态库的构建说明:

-buildmode=c-archive
    Build the listed main package, plus all packages it imports,
    into a C archive file. The only callable symbols will be those
    functions exported using a cgo //export comment. Requires
    exactly one main package to be listed.

-buildmode=c-shared
    Build the listed main package, plus all packages it imports,
    into a C shared library. The only callable symbols will
    be those functions exported using a cgo //export comment.
    Requires exactly one main package to be listed.

文档说明导出的C函数必须是在main包导出,然后才能在生成的头文件包含声明的语句。但是很多时候我们可能更希望将不同类型的导出函数组织到不同的Go包中,然后统一导出为一个静态库或动态库。

要实现从是从非main包导出C函数,或者是多个包导出C函数(因为只能有一个main包),我们需要自己提供导出C函数对应的头文件(因为CGO无法为非main包的导出函数生成头文件)。

假设我们先创建一个number子包,用于提供模加法函数

package number
import "C"
//export number_add_mod
func number_add_mod(a, b, mod C.int) C.int {
    return (a + b) % mod
}

然后是当前的main包:

package main
import "C"
import (
    "fmt"
    _ "./number"
)
func main() {
    println("Done")
}
//export goPrintln
func goPrintln(s *C.char) {
    fmt.Println("goPrintln:", C.GoString(s))
}

其中我们导入了number子包,在number子包中有导出的C函数number_add_mod,同时我们在main包也导出了goPrintln函数。

通过以下命令创建C静态库:

$ go build -buildmode=c-archive -o main.a

这时候在生成main.a静态库的同时,也会生成一个main.h头文件。但是main.h头文件中只有main包中导出的goPrintln函数的声明,并没有number子包导出函数的声明。其实number_add_mod函数在生成的C静态库中是存在的,我们可以直接使用。

创建_test_main.c测试文件如下:

#include <stdio.h>

void goPrintln(char*);
int number_add_mod(int a, int b, int mod);

int main() {
    int a = 10;
    int b = 5;
    int c = 12;

    int x = number_add_mod(a, b, c);
    printf("(%d+%d)%%%d = %d\n", a, b, c, x);

    goPrintln("done");
    return 0;
}

我们并没有包含CGO自动生成的main.h头文件,而是通过手工方式声明了goPrintln和number_add_mod两个导出函数。这样我们就实现了从多个Go包导出C函数了。

编译和链接参数

编译参数:CFLAGS/CPPFLAGS/CXXFLAGS

编译参数主要是头文件的检索路径,预定义的宏等参数。理论上来说C和C是完全独立的两个编程语言,它们可以有着自己独立的编译参数。 但是因为C语言对C语言做了深度兼容,甚至可以将C理解为C语言的超集,因此C和C语言之间又会共享很多编译参数。 因此CGO提供了CFLAGS/CPPFLAGS/CXXFLAGS三种参数,其中CFLAGS对应C语言编译参数(以.c后缀名)、 CPPFLAGS对应C/C++ 代码编译参数(.c,.cc,.cpp,.cxx)、CXXFLAGS对应纯C++编译参数(.cc,.cpp,*.cxx)。

链接参数:LDFLAGS

链接参数主要包含要链接库的检索目录和要链接库的名字。因为历史遗留问题,链接库不支持相对路径,我们必须为链接库指定绝对路径。 cgo 中的 ${SRCDIR} 为当前目录的绝对路径。经过编译后的C和C++目标文件格式是一样的,因此LDFLAGS对应C/Cpp共同的链接参数。

pkg-config

为不同C/C++库提供编译和链接参数是一项非常繁琐的工作,因此cgo提供了对应pkg-config工具的支持。 我们可以通过#cgo pkg-config xxx命令来生成xxx库需要的编译和链接参数,其底层通过调用 pkg-config xxx --cflags生成编译参数,通过pkg-config xxx --libs命令生成链接参数。 需要注意的是pkg-config工具生成的编译和链接参数是C/C++公用的,无法做更细的区分。

pkg-config工具虽然方便,但是有很多非标准的C/C++库并没有实现对其支持。 这时候我们可以手工为pkg-config工具创建对应库的编译和链接参数实现支持。

比如有一个名为xxx的C/C++库,我们可以手工创建/usr/local/lib/pkgconfig/xxx.bc文件:

Name: xxx
Cflags:-I/usr/local/include
Libs:-L/usr/local/lib –lxxx2

其中Name是库的名字,Cflags和Libs行分别对应xxx使用库需要的编译和链接参数。如果bc文件在其它目录, 可以通过PKG_CONFIG_PATH环境变量指定pkg-config工具的检索目录。

而对应cgo来说,我们甚至可以通过PKG_CONFIG 环境变量可指定自定义的pkg-config程序。 如果是自己实现CGO专用的pkg-config程序,只要处理--cflags和--libs两个参数即可。

下面的程序是macos系统下生成Python3的编译和链接参数:

// py3-config.go
func main() {
    for _, s := range os.Args {
        if s == "--cflags" {
            out, _ := exec.Command("python3-config", "--cflags").CombinedOutput()
            out = bytes.Replace(out, []byte("-arch"), []byte{}, -1)
            out = bytes.Replace(out, []byte("i386"), []byte{}, -1)
            out = bytes.Replace(out, []byte("x86_64"), []byte{}, -1)
            fmt.Print(string(out))
            return
        }
        if s == "--libs" {
            out, _ := exec.Command("python3-config", "--ldflags").CombinedOutput()
            fmt.Print(string(out))
            return
        }
    }
}

然后通过以下命令构建并使用自定义的pkg-config工具:

$ go build -o py3-config py3-config.go
$ PKG_CONFIG=./py3-config go build -buildmode=c-shared -o gopkg.so main.go
go get 链

在使用go get获取Go语言包的同时会获取包依赖的包。比如A包依赖B包,B包依赖C包,C包依赖D包: pkgA -> pkgB -> pkgC -> pkgD -> ...。再go get获取A包之后会依次线获取BCD包。 如果在获取B包之后构建失败,那么将导致链条的断裂,从而导致A包的构建失败。

链条断裂的原因有很多,其中常见的原因有:

  • 不支持某些系统, 编译失败

  • 依赖 cgo, 用户没有安装 gcc

  • 依赖 cgo, 但是依赖的库没有安装

  • 依赖 pkg-config, windows 上没有安装

  • 依赖 pkg-config, 没有找到对应的 bc 文件

  • 依赖 自定义的 pkg-config, 需要额外的配置

  • 依赖 swig, 用户没有安装 swig, 或版本不对

仔细分析可以发现,失败的原因中和CGO相关的问题占了绝大多数。这并不是偶然现象, 自动化构建C/C代码一直是一个世界难题,到目前位置也没有出现一个大家认可的统一的C/C管理工具。

因为用了cgo,比如gcc等构建工具是必须安装的,同时尽量要做到对主流系统的支持。 如果依赖的C/C++包比较小并且有源代码的前提下,可以优先选择从代码构建。

比如github.com/chai2010/webp包通过为每个C/C++源文件在当前包建立关键文件实现零配置依赖:

// z_libwebp_src_dec_alpha.c
#include "./internal/libwebp/src/dec/alpha.c"

因此在编译z_libwebp_src_dec_alpha.c文件时,会编译libweb原生的代码。 其中的依赖是相对目录,对于不同的平台支持可以保持最大的一致性

package main
/*
#include <stdio.h>
#include <stdlib.h>
#include "haha.h"
#cgo linux CFLAGS: -I./
#cgo linux LDFLAGS: -L./ -lhaha
void c_print(char *str)
{
    printf("%s\n", str);
}
void foo() {
    printf("i am foo\n");
}
*/
import "C" //import "C" 必须单起一行,并且紧跟在注释行之后
import "unsafe"
func main() {
s := "Hello Cgo"
cs := C.CString(s) //将go的字符串变成c的字符串
C.c_print(cs) //调用C函数
defer C.free(unsafe.Pointer(cs)) //释放内存
C.foo()cgo也是一个Go语言自带的特殊工具。一般情况下,我们使用命令go tool cgo来运行它。这个工具可以使我们创建能够调用C语言代码的Go语言源码文件。这使得我们可以使用Go语言代码去封装一些C语言的代码库,并提供给Go语言代码或项目使用。

在执行go tool cgo命令的时候,我们需要加入作为目标的Go语言源码文件的路径。这个路径可以是绝对路径也可以是相对路径。但是,作者强烈建议在目标源码文件所属的代码包目录下执行go tool cgo命令并以目标源码文件的名字作为参数。因为,go tool cgo命令会在当前目录(也就是我们执行go tool cgo命令的目录)中生成一个名为_obj的子目录。该目录下会包含一些Go源码文件和C源码文件。这个子目录及其包含的文件理应被保存在目标代码包目录之下。至于原因,我们稍后再做解释。

我们现在来看可以作为go tool cgo命令参数的Go语言源码文件。这个源码文件必须要包含一行只针对于代码包C的导入语句。其实,Go语言标准库中并不存在代码包C。代码包C是一个伪造的代码包。导入这个代码包是为了告诉cgo工具在这个源码文件中需要调用C代码,同时也是给予cgo所产生的代码一个专属的命名空间。除此之外,我们还需要在这个代码包导入语句之前加入一些注释,并且在注释行中写出我们真正需要使用的C语言接口文件的名称。像这样:

// #include <stdlib.h>
import "C"
在Go语言的规范中,把在代码包C导入语句之前的若干注释行叫做序文(preamble)。 在引入了C语言的标准代码库stdlib.h之后,我们就可以在后面的源码中调用这个库中的接口了。像这样:

func Random() int {
    return int(C.rand())
}

func Seed(i int) {
    C.srand(C.uint(i))
}
我们把上述的这些Go语言代码写入Go语言的库源码文件rand.go中,并将这个源码文件保存在goc2项目的代码包basic/cgo/lib的对应目录中。

在Go语言源码文件rand.go中对代码包C有四处引用,分别是三个函数调用语句C.rand、C.srand和C.uint,以及一个导入语句import "C"。其中,在Go语言函数Random中调用了C语言标准库代码中的函数rand并返回了它的结果。但是,C语言的rand函数返回的结果的类型是C语言中的int类型。在cgo工具的环境中,C语言中的int类型与C.int相对应。作为一个包装C语言接口的函数,我们必须将代码包C的使用限制在当前代码包内。也就是说,我们必须对当前代码包之外的Go代码隐藏代码包C。这样做也是为了遵循代码隔离原则。我们在设计接口或者接口适配程序的时候经常会用到这种方法。因此,rand函数的结果的类型必须是Go语言的。所以,我们在这里使用函数int对C.int类型的C语言接口的结果进行了转换。当然,为了更清楚的表达,我们也可以将函数Random中的代码return int(C.rand())拆分成两行,像这样:

var r C.int = C.rand()
return int(r)
而Go语言函数Seed则恰好相反。C语言标准代码库中的函数srand接收一个参数,且这个参数的类型必须为C语言的uint类型,即C.uint。而Go语言函数Seed的参数为Go语言的int类型。为此,我们需要使用代码包C的函数unit对其进行转换。

实际上,标准C语言的数字类型都在cgo工具中有对应的名称,包括:C.char、C.schar(有符号字符类型)、C.uchar(无符号字符类型)、C.short、C.ushort(无符号短整数类型)、C.int、C.uint(无符号整数类型)、C.long、C.ulong(无符号长整数类型)、C.longlong(对应于C语言的类型long long,它是在C语言的C99标准中定义的新整数类型)、C.ulonglong(无符号的long long类型)、C.float和C.double。另外,C语言类型void*对应于Go语言的类型unsafe.Pointer。

如果想直接访问C语言中的struct、union或enum类型的话,就需要在名称前分别加入前缀struct_、union或enum。比如,我们需要在Go源码文件中访问C语言代码中的名为command的struct类型的话,就需要这样写:C.structcommand。那么,如果我们想在Go语言代码中访问C语言类型struct中的字段需要怎样做呢?解决方案是,同样以C语言类型struct的实例名以及选择符“.”作为前导,但需要在字段的名称前加入下划线“”。例如,如果command1是名为command的C语言struct类型的实例名,并且这个类型中有一个名为name的字段,那么我们在Go语言代码中访问这个字段的方式就是command1._name。需要注意的是,我们不能在Go的struct类型中嵌入C语言类型的字段。这与我们在前面所说的代码隔离原则具有相同的意义。

在上面展示的库源码文件rand.go中有多处对C语言函数的访问。实际上,任何C语言的函数都可以 被Go语言代码调用。只要在源码文件中导入了代码包C。并且,我们还可以同时取回C语言函数的结果,以及一个作为错误提示信息的变量。这与我们在Go语言中同时获取多个函数结果的方法一样。同样的,我们可以使用下划线“_”直接丢弃取回的值。这在调用无结果的C语言函数时非常有用。请看下面的例子:

package cgo

/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"

func Sqrt(p float32) (float32, error) {
    n, err := C.sqrt(C.double(p))
    return float32(n), err
}
上面这段代码被保存在了Go语言库源码文件math.go中,并与源码文件rand.go在同一个代码包目录。在Go语言函数Sqrt中的C.sqrt是一个在C语言标准代码库math.h中的函数。它会返回参数的平方根。但是在第一行代码中,我们接收由函数C.sqrt返回的两个值。其中,第一个值即为C语言函数sqrt的结果。而第二个值就是我们上面所说的那个作为错误提示信息的变量。实际上,这个变量的类型是Go语言的error接口类型。它包装了一个C语言的全局变量errno。这个全局变量被定义在了C语言代码库errno.h中。cgo工具在为我们生成C语言源码文件时会默认引入两个C语言标准代码库,其中一个就是errno.h。所以我们并不用在Go语言源码文件中使用指令符#include显式的引入这个代码库。cgo工具默认为我们引入的另一个是C语言标准代码库string.h。它包含了很多用于字符串处理和内存处理的函数。

在我们以“C.*”的形式调用C语言代码库时,有一点需要特别注意。在C语言中,如果一个函数的参数是一个具有固定尺寸的数组,那么实际上这个函数所需要的是指向这个数组的第一个元素的指针。C编译器能够正确识别和处理这个调用惯例。它可以自行获取到这个指针并传给函数。但是,这在我们使用cgo工具调用C语言代码库时是行不通的。在Go语言中,我们必须显式的将这个指向数组的第一个元素的指针传递给C语言的函数,像这样:``C.func1(&x[0])````。

另一个需要特别注意的地方是,在C语言中没有像Go语言中独立的字符串类型。C语言使用最后一个元素为‘\0’的字符数组来代表字符串。在Go语言的字符串和C语言的字符串之间进行转换的时候,我们就需要用到代码包C中的C.C.CString、C.GoString和C.GoStringN等函数。这些转换操作是通过对字符串数据的拷贝来完成的。Go语言内存管理器并不能感知此类内存分配操作。因为它们是由C语言代码引发的。所以,我们在使用与C.CString函数类似的会导致内存分配操作的函数之后,需要调用代码包C的free函数以手动的释放内存。这里有一个小技巧,即我们可以把对C.free函数的调用放到defer语句中或者放入在defer之后的匿名函数中。这样就可以保证在退出当前函数之前释放这些被分配的内存了。请看下面这个示例:

func Print(s string) {
        cs := C.CString(s)
        defer C.free(unsafe.Pointer(cs))
        C.myprint(cs)
}
上面这段代码被存放在goc2p项目的代码包basic/cgo/lib的库源码文件print.go中。其中的函数C.myprint是我们在该库源码文件的序文中自定义的。关于这种C语言函数定义方式,我们一会儿再解释。在这段代码中,我们首先把Go语言的字符串转换为了C语言的字符串。注意,变量cs的值实际上是指向字符串(在C语言中,字符串由字符数组代表)中第一个字符的指针。在cgo工具对应的上下文环境中,cs变量的类型是*C.Char。然后,我们通过defer语句和C.free函数保证由C语言代码分配的内存得以释放。请注意子语句unsafe.Pointer(cs)。正因为cs变量在C语言中的类型是指针类型,且与之相对应的Go语言类型是unsafe.Pointer。所以,我们需要先将其转换为Go语言可以识别的类型再作为参数传递给函数C.free。最后,我们将这个字符串打印到标准输出。

再次重申,我们在使用C.CString函数将Go语言的字符串转换为C语言字符串后,需要显式的调用C.free函数以释放用于数据拷贝的内存。而最佳实践是,将在defer语句中调用C.free函数。

在前面我们已经提到过,在导入代码包C的语句之上可以加入若干个为cgo工具而写的若干注释行(也被叫做序文)。并且,以#include和一个空格开始的注释行可以用来引入一个C语言的接口文件。我们也把序文中这种形式的字符串叫做指令符。指令符#cgo的用途是为编译器和连接器提供标记。这些标记在编译当前源码文件中涉及到代码包C的那部分代码时会被用到。

标记CFLAGS和LDFLAGS``可以被放在指令符#cgo```之后,并用于定制编译器gcc的行为。gcc(GNU Compiler Collection,GNU编译器套装),是一套由GNU开发的开源的编程语言编译器。它是GNU项目的关键部分,也是类Unix操作系统(也包括Linux操作系统)中的标准编译器。gcc(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。gcc原名为GNU C语言编译器(GNU C Compiler),因为它原本只能处理C语言。不过,gcc变得可以处理更多的语言。现在,gcc中包含了很多针对特定编程语言的编译器。我们在本节第一小节的末尾提及的gccgo就是这款套件中针对Go语言的编译器。标记CFLAGS可以指定用于gcc中的C编译器的选项。它尝尝用于指定头文件(.h文件)的路径。而标记LDFLAGS则可以指定gcc编译器会用到的一些优化参数,也可以用来告诉链接器需要用到的C语言代码库文件的位置。

为了清晰起见,我们可以把这些标记及其值拆分成多个注释行,并均以指令符#cgo作为前缀。另外,在指令符#cgo和标记之间,我们也可以加入一些可选的内容,即环境变量GOOS和GOARCH中的有效值。这样,我们就可以使这些标记只在某些操作系统和/或某些计算架构的环境下起作用了。示例如下:

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo linux CFLAGS: -DLINUX=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"
在上面的示例中,序文由四个注释行组成。第一行注释的含义是预定义一个名为PNG_DEBUG的宏并将它的值设置为1。而第二行注释的意思是,如果在Linux操作系统下,则预定义一个名为LINUX的宏并将它的值设置为1。第三行注释是与链接器有关的。它告诉链接器需要用到一个库名为png的代码库文件。最后,第四行注释引入了C语言的标准代码库png.h。

如果我们有一些在所有场景下都会用到的CFLAGS标记或LDFLAGS标记的值,那么就可以把它们分别作为环境变量CGO_CFLAGS和CGO_LDFLAGS的值。而对于需要针对某个导入了“C”的代码包的标记值就只能连同指令符#cgo一起放入Go语言源码文件的注释行中了。

相信读者对指令符#cgo和#include的用法已经有所了解了。

实际上,我们几乎可以在序文中加入任何C代码。像这样:

/*
#cgo LDFLAGS: -lsqlite3

#include <sqlite3.h>
#include <stdlib.h>

// These wrappers are necessary because SQLITE_TRANSIENT
// is a pointer constant, and cgo doesn't translate them correctly.
// The definition in sqlite3.h is:
//
// typedef void (*sqlite3_destructor_type)(void*);
// #define SQLITE_STATIC      ((sqlite3_destructor_type)0)
// #define SQLITE_TRANSIENT   ((sqlite3_destructor_type)-1)

static int my_bind_text(sqlite3_stmt *stmt, int n, char *p, int np) {
        return sqlite3_bind_text(stmt, n, p, np, SQLITE_TRANSIENT);
}
static int my_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) {
        return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT);
}

*/
上面这段代码摘自开源项目gosqlite的Go语言源码文件sqlite.go。gosqlite项目是一个开源数据SQLite的Go语言版本的驱动代码库。实际上,它只是把C语言版本的驱动代码库进行了简单的封装。在Go语言的世界里,这样的封装随处可见,尤其是在Go语言发展早期。因为,这样可以非常方便的重用C语言版本的客户端程序,而大多数软件都早已拥有这类程序了。并且,封装C语言版本的代码库与从头开发一个Go语言版本的客户端程序相比,无论从开发效率还是运行效率上来讲都会是非常迅速的。现在让我们看看在上面的序文中都有些什么。很显然,在上面的序文中直接出现了两个C语言的函数my_bind_text和my_bind_blob。至于为什么要把C语言函数直接写到这里,在它们前面的注释中已经予以说明。大意翻译如下:这些包装函数是必要的,这是因为SQLITE_TRANSIENT是一个指针常量,而cgo并不能正确的翻译它们。看得出来,这是一种备选方案,只有在cgo不能帮我们完成工作时才会被选用。不管怎样,在序文中定义的这两个函数可以直接在当前的Go语言源码文件中被使用。具体的使用方式同样是通过“C.*”的形式来调用。比如源码文件sqlite.go中的代码:

rv := C.my_bind_text(s.stmt, C.int(i+1), cstr, C.int(len(str)))
和

rv := C.my_bind_blob(s.stmt, C.int(i+1), unsafe.Pointer(p), C.int(len(v)))
上述示例中涉及到的源码文件可以通过这个网址访问到。有兴趣的读者可以前往查看。

我们再来看看我们之前提到过的库源码文件print.go(位于goc2p项目的代码包basic/cgo/lib之中)的序文:

/*
#include <stdio.h>
#include <stdlib.h>

void myprint(char* s) {
        printf("%s", s);
}
*/
import "C"
我们在序文中定义一个名为myprint的函数。在这个函数中调用了C语言的函数printf。自定义函数myprint充当了类似于适配器的角色。之后,我们就可以在后续的代码中直接使用这个自定义的函数了:

C.myprint(cs)
关于在序文中嵌入C语言代码的方法我们就介绍到这里。

现在,让我们来使用go tool cgo命令并以rand.go作为参数生成_obj子目录和相关源码文件:

hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ go tool cgo rand.go 
hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls
_obj  rand.go
hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls _obj
_cgo_defun.c   _cgo_export.h  _cgo_gotypes.go  _cgo_.o       rand.cgo2.c
_cgo_export.c  _cgo_flags     _cgo_main.c      rand.cgo1.go
子目录_obj中一共包含了九个文件。

其中,cgo工具会把作为参数的Go语言源码文件rand.go转换为四个文件。其中包括两个Go语言源码文件rand.cgo1.go和_cgo_gotypes.go,以及两个C语言源码文件_cgo_defun.c和rand.cgo2.c。

文件rand.cgo1.go用于存放cgo工具对原始源码文件rand.go改写后的内容。改写具体细节包括去掉其中的代码包C导入语句,以及替换涉及到代码包C的语句,等等。最后,这些替换后的标识符所对应的Go语言的函数、类型或变量的定义,将会被写入到文件_cgo_gotypes.go中。

需要说明的是,替换涉及到代码包C的语句的具体做法是根据xxx的种类将标识符C.xxx替换为_Cfunc_xxx或者_Ctype_xxx。比如,作为参数的源码文件rand.go中存在如下语句:

C.srand(C.uint(i))
cgo工具会把它改写为:

_Cfunc_srand(_Ctype_uint(i))
其中,标识符C.srand被替换为_Cfunc_srand,而标识符C.uint被替换为了_Ctype_uint。并且,新的标识符_Cfunc_srand和_Ctype_uint的定义将会在文件_cgo_gotypes.go中被写明:

type _Ctype_uint uint32

type _Ctype_void [0]byte

func _Cfunc_srand(_Ctype_uint) _Ctype_void
其中,类型_Ctype_void可以表示空的参数列表或空的结果列表。

文件_cgo_defun.c中包含了相应的C语言函数的定义和实现。例如,C语言函数_Cfunc_srand的实现如下:

#pragma cgo_import_static _cgo_54716c7dc6a7_Cfunc_srand
void _cgo_54716c7dc6a7_Cfunc_srand(void*);

void
·_Cfunc_srand(struct{uint8 x[4];}p)
{
    runtime·cgocall(_cgo_54716c7dc6a7_Cfunc_srand, &p);
}
其中,十六进制数“54716c7dc6a7”是cgo工具由于作为参数的源码文件的内容计算得出的哈希值。这个十六进制数作为了函数名称_cgo_54716c7dc6a7_Cfunc_srand的一部分。这样做是为了生成一个唯一的名称以避免冲突。我们看到,在源码文件_cgo_defun.c中只包含了函数_cgo_54716c7dc6a7_Cfunc_srand的定义。而其实现被写入到了另一个C语言源码文件中。这个文件就是rand.cgo2.c。函数_cgo_54716c7dc6a7_Cfunc_srand对应的实现代码如下:

void
_cgo_f290d3e89fd1_Cfunc_srand(void *v)
{
    struct {
        unsigned int p0;
    } __attribute__((__packed__)) *a = v;
    srand(a->p0);
}
这个函数从指向函数_Cfunc_puts的参数帧中抽取参数,并调用系统C语言函数srand,最后将结果存储在帧中并返回。

下面我们对在子目录_obj中存放的其余几个文件进行简要说明:

文件_cgo_flags用于存放CFLAGS标记和LDFLAGS标记的值。

文件_cgo_main.c用于存放一些C语言函数的存根,也可以说是一些函数的空实现。函数的空实现即在函数体重没有任何代码(return语句除外)的实现。其中包括在源码文件_cgo_export.c出现的声明为外部函数的函数。另外,文件_cgo_main.c中还会有一个被用于动态链接处理的main函数。

在文件_cgo_export.h中存放了可以暴露给C语言代码的与Go语言类型相对应的C语言声明语句。

文件_cgo_export.c中则包含了与可以暴露给C语言代码的Go语言函数相对应的C语言函数定义和实现代码。

文件cgo.o是gcc编译器在编译C语言源码文件rand.cgo2.c、_cgo_export.c和_cgo_main.c之后生成的结果文件。
在上述的源码文件中,文件rand.cgo1.go和_cgo_gotypes.go将会在构建代码包时被Go官方Go语言编译器(6g、8g或5g)编译。文件_cgo_defun.c会在构建代码包时被Go官方的C语言的编译器(6c、8c或5c)编译。而文件rand.cgo2.c、_cgo_export.c和_cgo_main.c 则会被gcc编译器编译。

如果我们在执行go tool cgo命令时加入多个Go语言源码文件作为参数,那么在当前目录的_obj子目录下会出现与上述参数数量相同的x.cgo1.go文件和x.cgo2.c文件。其中,x为作为参数的Go语言源码文件主文件名。

通过上面的描述,我们基本了解了由cgo工具生成的文件的内容和用途。

与其它go命令一样,我们在执行go tool cgo命令的时候还可以加入一些标记。如下表。

表0-24 go tool cgo命令可接受的标记

名称	默认值	说明
-cdefs	false	将改写后的源码内容以C定义模式打印到标准输出,而不生成相关的源码文件。
-godefs	false	将改写后的源码内容以Go定义模式打印到标准输出,而不生成相关的源码文件。
-objdir	""	gcc编译的目标文件所在的路径。若未自定义则为当前目录下的_obj子目录。
-dynimport	""	如果值不为空字符串,则打印为其值所代表的文件生成的动态导入数据到标准输出。
-dynlinker	false	记录在dynimport模式下的动态链接器信息。
-dynout	""	将-dynimport的输出(如果有的话)写入到其值所代表的文件中。
-gccgo	false	生成可供gccgo编译器使用的文件。
-gccgopkgpath	""	对应于gccgo编译器的-fgo-pkgpath选项。
-gccgoprefix	""	对应于gccgo编译器的-fgo-prefix选项。
-debug-define	false	打印相关的指令符#defines及其后续内容到标准输出。
-debug-gcc	false	打印gcc调用信息到标准输出。
-import_runtime_cgo	true	在生成的代码中加入语句“import runtime/cgo”。
-import_syscall	true	在生成的代码中加入语句“import syscall”。
在上表中,我们把标记分为了五类并在它们之间以空行分隔。

在第一类标记中,-cdefs标记和-godefs标记都可以打印相应的代码到标准输出,并且使cgo工具不生成相应的源码文件。cgo工具在获取目标源码文件内容之后会改写其中的内容,包括去掉代码包C的导入语句,以及对代码包C的调用语句中属于代码包C的类型、函数和变量进行等价替换。如果我们加入了标记-cdefs或-godefs,那么cgo工具随后就会把改写后的目标源码打印到标准输出了。需要注意的是,我们不能同时使用这两个标记。使用这两个标记打印出来的源码内容几乎相同,而最大的区别也只是格式方面的。

第二类的三个标记都与动态链接库有关。在类Unix系统下,标记-dynimport的值可以是一个ELF(Executable and Linkable Format)格式或者Mach-O(Mach Object)格式的文件的路径。ELF即可执行链接文件格式。ELF格式的文件保存了足够的系统相关信息,以至于使它能够支持不同平台上的交叉编译和交叉链接,可移植性很强。同时,它在执行中支持动态链接共享库。我们在Linux操作系统下使用go命令生成的命令源码文件的可执行文件就是ELF格式的。而Mach-O是一种用于可执行文件、目标代码、动态链接库和内核转储的文件格式。在Windows下,这个标记的值应该是一个PE(Portable Execute)格式的文件的路径。在Windows操作系统下,使用go命令生成的命令源码文件的可执行文件就是PE格式的。

实质上,加入标记-dynimport的go tool cgo命令相当于一个被构建在cgo工具内部的独立的帮助命令。使用方法如go tool cgo -dynimport='cgo_demo.go'。这个命令会扫描这个标记的值所代表的可执行文件,并将其中记录的与已导入符号和已导入代码库相关的信息打印到标准输出。go build命令程序中有专门为cgo工具制定的规则。这使得它可以在编译直接或间接依赖了代码包C的命令源码文件时可以生成适当的可执行文件。在这个可执行文件中,直接包含了相关的已导入符号和已导入代码库的信息,以供之后使用。这样就无需使链接器复制gcc编译器的所有关于如何寻找已导入的符号以及使用它的位置的专业知识了。下面我们来试用一下go tool cgo -dynimport命令。

首先,我们创建一个命令源码文件cgo_demo.go,并把它存放在goc2p项目的代码包basic/cgo对应的目录下。命令源码文件cgo_demo.go的内容如下:

package main

import (
    cgolib "basic/cgo/lib"
    "fmt"
)

func main() {
    input := float32(2.33)
    output, err := cgolib.Sqrt(input)
    if err != nil {
        fmt.Errorf("Error: %s\n", err)
    }
    fmt.Printf("The square root of %f is %f.\n", input, output)
}
在这个命令源码文件中,我们调用了goc2p项目的代码包basic/cgo/lib中的函数Sqrt。这个函数是被保存在库源码文件math.go中的。而在文件math.go中,我们导入了代码包C。也就是说,命令源码文件cgo_demo.go间接的依赖了代码包C。现在,我们使用go build命令将这个命令源码文件编译成ELF格式的可执行文件。然后,我们就能够使用go tool cgo -dynimport命令查看其中的导入信息了。请看如下示例:

hc@ubt:~/golang/goc2p/basic/cgo$ go build cgo_demo.go
hc@ubt:~/golang/goc2p/basic/cgo$ go tool cgo -dynimport='cgo_demo'
#pragma cgo_import_dynamic pthread_attr_init pthread_attr_init#GLIBC_2.1 
    "libpthread.so.0"
#pragma cgo_import_dynamic pthread_attr_destroy pthread_attr_destroy#GLIBC_2.0 
    "libpthread.so.0"
#pragma cgo_import_dynamic stderr stderr#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic sigprocmask sigprocmask#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic free free#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic fwrite fwrite#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic malloc malloc#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic strerror strerror#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic srand srand#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic setenv setenv#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic __libc_start_main __libc_start_main#GLIBC_2.0 
    "libc.so.6"
#pragma cgo_import_dynamic fprintf fprintf#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic pthread_attr_getstacksize
     pthread_attr_getstacksize#GLIBC_2.1 "libpthread.so.0"
#pragma cgo_import_dynamic sigfillset sigfillset#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic __errno_location __errno_location#GLIBC_2.0 
    "libpthread.so.0"
#pragma cgo_import_dynamic sqrt sqrt#GLIBC_2.0 "libm.so.6"
#pragma cgo_import_dynamic rand rand#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic pthread_create pthread_create#GLIBC_2.1 
    "libpthread.so.0"
#pragma cgo_import_dynamic abort abort#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic _ _ "libm.so.6"
#pragma cgo_import_dynamic _ _ "libpthread.so.0"
#pragma cgo_import_dynamic _ _ "libc.so.6"
从上面示例的输出信息中,我们可以看到可执行文件cgo_demo所涉及到的所有动态链接库文件以及相关的函数名和代码库版本等信息。

如果我们再加入一个标记-dynlinker,那么在命令的输出信息还会包含动态链接器的信息。示例如下:

hc@ubt:~/golang/goc2p/src/basic/cgo$ go tool cgo -dynimport='cgo_demo' -dynlinker
#pragma cgo_dynamic_linker "/lib/ld-linux.so.2"
<省略部分输出内容>
如果我们在命令go tool cgo -dynimport后加入标记-dynout,那么命令的输出信息将会写入到指定的文件中,而不是被打印到标准输出。比如命令go tool cgo -dynimport='cgo_demo' -dynlinker -dynout='cgo_demo.di'就会将可执行文件cgo_demo中的导入信息以及动态链接器信息转储到当前目录下的名为“cgo_demo.di”的文件中。

第四类标记包含了-gccgo、-gccgopkgpath和-gccgoprefix。它们都与编译器gccgo有关。标记-gccgo的作用是使cgo工具生成可供gccgo编译器使用的源码文件。这些源码文件会与默认情况下生成的源码文件在内容上有一些不同。实际上,到目前为止,cgo工具还不能很好的与gccgo编译器一同使用。但是,按照gccgo编译器的主要开发者Ian Lance Taylor的话来说,gccgo编译器并不需要cgo工具,也不应该使用gcc工具。不管怎样,这种情况将会在Go语言的1.3版本中得到改善。

第五类标记用于打印调试信息,包括标记-debug-define和-debug-gcc。gcc工具不但会生成新的Go语言源码文件以保存其对目标源码改写后的内容,还会生成若干个C语言源码文件。cgo工具为了编译这些C语言源码文件,就会用到gcc编译器。在加入-debug-gcc标记之后,gcc编译器的输出信息就会被打印到标准输出上。另外,gcc编译器在对C语言源码文件进行编译之后会产生一个结果文件。这个结果文件就是在_obj子目录下的名为cgo.o的文件。

第六类标记的默认值都为true。也就是说,在默认情况下cgo工具生成的_obj子目录下的Go语言源码文件_cgogotypes.go中会包含代码包导入语句```import "runtime/cgo"和import "syscall"。代码包导入语句import _ "runtime/cgo"只是引发了代码包runtime/cgo中的初始化函数的执行而没有被分配到一个具体的命名空间上。在这些初始化函数中,包含了对一些C语言的全局变量和函数声明的初始化过程。需要注意的是,只要我们在执行go tool cgo命令的时候加入了标记-gccgo,即使标记-import_runtime_cgo有效,在Go语言源码文件_cgo_gotypes.go中也不会包含import _ "runtime/cgo"```语句。

至此,我们在本小节讨论的都是Go语言代码如果通过cgo工具调用标准C语言编写的函数。其实,我们利用cgo工具还可以把Go语言编写的函数暴露给C语言代码。

Go语言可以使它的函数被C语言代码所用。这是通过使用一个特殊的注释“//export”来实现的。示例如下:

package cgo

/*
#include <stdio.h>
extern void CFunction1();
*/
import "C"

import "fmt"

//export GoFunction1
func GoFunction1() {
        fmt.Println("GoFunction1() is called.")
}

func CallCFunc() {
        C.CFunction1()
}
在这个示例中,我们使用注释行“//export GoFunction1”把Go语言函数GoFunction1暴露给了C语言代码。注意,注释行中在“//export ”之后的字符串必须与其下一行的那个函数的名字一致。我们也可以把字符串“//export”看成一种指令符,就像#cgo和#include。这里有一个限制,就是只要我们使用了指令符“//export”,在当前源码文件的序文中就不能包含任何C语言定义语句,只可以包含C语言声明语句。上面示例的序文中的extern void CFunction1();就是一个很好的例子。序文中的这一行C语言声明语句会被拷贝到两个不同的cgo工具生成的C语言源码文件中。这也正是其序文中不能包含C语言定义语句的原因。那么C语言函数CFunction1的定义语句我们应该放在哪儿呢?答案是放到在同目录的其它Go语言源码文件的序文中,或者直接写到C语言源码文件中。

我们把上面示例中的内容保存到名为go_export.go的文件中,并放到goc2p项目的basic/cgo/lib代码包中。现在我们使用go tool cgo来处理这个源码文件。如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go
之后,我们会发现在_obj子目录下的C语言头文件_cgo_export.h中包含了这样一行代码:

extern void GoFunction1();
这说明C语言代码已经可以对函数GoFunction1进行调用了。现在我们使用go build命令构建goc2p项目的代码包basic/cgo,如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build
# basic/cgo/lib
/tmp/go-build477634277/basic/cgo/lib/_obj/go_export.cgo2.o: In function `_cgo_cc103c85817e_Cfunc_CFunction1':
./go_export.go:34: undefined reference to `CFunction1'
collect2: ld return 1
构建并没有成功完成。根据错误提示信息我们获知,C语言函数CFunction1未被定义。这个问题的原因是我们并没有在Go语言源码文件go_export.go的序文中写入C语言函数CFunction1的实现,也即未对它进行定义。我们之前说过,在这种情况下,对应函数的定义应该被放到其它Go语言源码文件的序文或者C语言源码文件中。现在,我们在当前目录下创建一个新的Go语言源码文件go_export_def.go。其内容如下:

package cgo

/*
#include <stdio.h>
void CFunction1() {
        printf("CFunction1() is called.\n");
        GoFunction1();
} 
*/
import "C"
这个文件是专门用于存放C语言函数定义的。注意,由于C语言函数printf来自C语言标准代码库stdio.h,所以我们需要在序文中使用指令符#include将它引入。保存好源码文件go_export_def.go之后,我们重新使用go tool cgo命令处理这两个文件,如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go go_export_def.go
然后,我们再次执行go build命令构建代码包basic/cgo/lib:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build
显然,这次的构建成功完成。当然单独构建代码包basic/cgo/lib并不是必须的。我们在这里是为了检查该代码包中的代码(包括Go语言代码和C语言代码)是否都能够被正确编译。

还记得goc2p项目的代码包basic/cgo中的命令源码文件cgo_demo.go。现在我们在它的main函数的最后加入一行新代码:cgo.CallCFunc(),即调用在代码包``basic/cgo/lib```中的库源码文件go_export.go的函数。然后,我们运行这个命令源码文件:

hc@ubt:~/golang/goc2p/basic/cgo$ go run cgo_demo.go
The square root of 2.330000 is 1.526434.
ABC
CFunction1() is called.
GoFunction1() is called.
从输出的信息可以看出,我们定义的C语言函数CFunction1和Go语言函数GoFunction1都已被调用,并且调用顺序正如我们所愿。这个例子也说明,我们可以非常方便的使用cgo工具完成如下几件事:

Go语言代码调用标准C语言的代码。这也使得我们可以使用Go语言封装任何已存在的C语言代码库,并提供给其他Go语言代码使用。

可以在Go语言源码文件的序文中自定义任何C语言代码并由Go语言代码使用。这使得我们可以更灵活的对C语言代码进行封装。同时,我们还可以利用这一特性在我们自定义的C语言代码中使用Go语言代码。

通过指令符“//export”,可使C语言代码能够使用Go语言代码。这里所说的C语言代码是指我们在Go语言源码文件的序文中自定义的C语言代码。但是,go tool cgo命令会将序文中的C语言代码声明和定义分别写入到其生成的C语言头文件和C语言源码文件中。所以,从原则上讲,这已经具备了让外部C语言代码使用Go语言代码的能力。
综上所述,cgo工具不但可以使Go语言直接使用现存的非常丰富的C语言代码库,还可以使用Go语言代码扩展现有的C语言代码库。

至此,我们介绍了怎样独立的使用cgo工具。但实际上,我们可以直接使用标准go命令构建、安装和运行导入了代码包C的代码包和源码文件。标准go命令能够认出代码包C的导入语句并自动使用cgo工具进行处理。示例如下:

hc@ubt:~/golang/goc2p/src/basic/cgo$ rm -rf lib/_obj
hc@ubt:~/golang/goc2p/src/basic/cgo$ go run cgo_demo.go
The square root of 2.330000 is 1.526434.
ABC
CFunction1() is called.
GoFunction1() is called.
在上例中,我们首先删除了代码包basic/cgo/lib目录下的子目录_obj,以此来保证原始的测试环境。然后,我们直接运行了命令源码文件cgo_demo.go。在这个源码文件中,包含了对代码包basic/cgo/lib中函数的调用语句,而在这些函数中又包含了对代码包C的引用。从输出信息我们可以看出,命令源码文件cgo_demo.go的运行成功的完成了。这也验证了标准go命令在这方面的功能。不过,有时候我们还是很有必要单独使用go tool cgo命令,比如对相关的Go语言代码和C语言代码的功能进行验证或者需要通过标记定制化运行cgo工具的时候。另外,如果我们通过标准go命令构建或者安装直接或间接导入了代码C的命令源码文件,那么在生成的可执行文件中就会包含动态导入数据和动态链接器信息。我们可以使用go tool cgo命令查看可执行文件中的这些信息。
C.haha()
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值