CFLAGS

https://github.com/chai2010/advanced-go-programming-book/blob/master/ch2-cgo/ch2-02-basic.md
要使用CGO特性,需要安装C/C++构建工具链,在macOS和Linux下是要安装GCC,在windows下是需要安装MinGW工具。同时需要保证环境变量CGO_ENABLED被设置为1,这表示CGO是被启用的状态。在本地构建时CGO_ENABLED默认是启用的,当交叉构建时CGO默认是禁止的。比如要交叉构建ARM环境运行的Go程序,需要手工设置好C/C++交叉构建的工具链,同时开启CGO_ENABLED环境变量。然后通过import “C”语句启用CGO特性。

2.2.1 import “C”语句
如果在Go代码中出现了import “C”语句则表示使用了CGO特性,紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。当确保CGO启用的情况下,还可以在当前目录中包含C/C++对应的源文件。



举个最简单的例子:



package main



/*
#include



void printint(int v) {
printf(“printint: %d\n”, v);
}
*/
import “C”



func main() {
v := 42
C.printint(C.int(v))
}
这个例子展示了cgo的基本使用方法。开头的注释中写了要调用的C函数和相关的头文件,头文件被include之后里面的所有的C语言元素都会被加入到”C”这个虚拟的包中。需要注意的是,import “C”导入语句需要单独一行,不能与其他包一同import。向C函数传递参数也很简单,就直接转化成对应C语言类型传递就可以。如上例中C.int(v)用于将一个Go中的int类型值强制类型转换转化为C语言中的int类型值,然后调用C语言定义的printint函数进行打印。



需要注意的是,Go是强类型语言,所以cgo中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C”中的转化函数转换成对应的C类型,不能直接传入Go中类型的变量。同时通过虚拟的C包导入的C语言符号并不需要是大写字母开头,它们不受Go语言的导出规则约束。



cgo将当前包引用的C语言符号都放到了虚拟的C包中,同时当前包依赖的其它Go语言包内部可能也通过cgo引入了相似的虚拟C包,但是不同的Go语言包引入的虚拟的C包之间的类型是不能通用的。这个约束对于要自己构造一些cgo辅助函数时有可能会造成一点的影响。



比如我们希望在Go中定义一个C语言字符指针对应的CChar类型,然后增加一个GoString方法返回Go语言字符串:



package cgo_helper



//#include
import "C"



type CChar C.char



func (p CChar) GoString() string {
return C.GoString((
C.char)(p))
}



func PrintCString(cs *C.char) {
C.puts(cs)
}
现在我们可能会想在其它的Go语言包中也使用这个辅助函数:



package main



//static const char* cs = “hello”;
import “C”
import “./cgo_helper”



func main() {
cgo_helper.PrintCString(C.cs)
}
这段代码是不能正常工作的,因为当前main包引入的C.cs变量的类型是当前main包的cgo构造的虚拟的C包下的char类型(具体点是C.char,更具体点是main.C.char),它和cgo_helper包引入的C.char类型(具体点是cgo_helper.C.char)是不同的。在Go语言中方法是依附于类型存在的,不同Go包中引入的虚拟的C包的类型却是不同的(main.C不等cgo_helper.C),这导致从它们延伸出来的Go类型也是不同的类型(main.C.char不等*cgo_helper.C.char),这最终导致了前面代码不能正常工作。



有Go语言使用经验的用户可能会建议参数转型后再传入。但是这个方法似乎也是不可行的,因为cgo_helper.PrintCString的参数是它自身包引入的C.char类型,在外部是无法直接获取这个类型的。换言之,一个包如果在公开的接口中直接使用了C.char等类似的虚拟C包的类型,其它的Go包是无法直接使用这些类型的,除非这个Go包同时也提供了*C.char类型的构造函数。因为这些诸多因素,如果想在go test环境直接测试这些cgo导出的类型也会有相同的限制。



2.2.2 #cgo语句
在import “C”语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。



// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include
import "C"
上面的代码中,CFLAGS部分,-D部分定义了宏PNG_DEBUG,值为1;-I定义了头文件包含的检索目录。LDFLAGS部分,-L指定了链接时库文件检索目录,-l指定了链接时需要链接png库。



因为C/C++遗留的问题,C头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。在库文件的检索目录中可以通过${SRCDIR}变量表示当前包目录的绝对路径:



// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
上面的代码在链接时将被展开为:



// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo
#cgo语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。LDFLAGS用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数(CFLAGS用于针对C语言代码设置编译参数)。



对于在cgo环境混合使用C和C++的用户来说,可能有三种不同的编译选项:其中CFLAGS对应C语言特有的编译选项、CXXFLAGS对应是C++特有的编译选项、CPPFLAGS则对应C和C++共有的编译选项。但是在链接阶段,C和C++的链接选项是通用的,因此这个时候已经不再有C和C++语言的区别,它们的目标文件的类型是相同的。



#cgo指令还支持条件选择,当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。比如下面是分别针对windows和非windows下平台的编译和链接选项:



// #cgo windows CFLAGS: -DX86=1
// #cgo !windows LDFLAGS: -lm
其中在windows平台下,编译前会预定义X86宏为1;在非widnows平台下,在链接阶段会要求链接math数学库。这种用法对于在不同平台下只有少数编译选项差异的场景比较适用。



如果在不同的系统下cgo对应着不同的c代码,我们可以先使用#cgo指令定义不同的C语言的宏,然后通过宏来区分不同的代码:



package main



/*
#cgo windows CFLAGS: -DCGO_OS_WINDOWS=1
#cgo darwin CFLAGS: -DCGO_OS_DARWIN=1
#cgo linux CFLAGS: -DCGO_OS_LINUX=1



#if defined(CGO_OS_WINDOWS)
const char* os = “windows”;
#elif defined(CGO_OS_DARWIN)
const char* os = “darwin”;
#elif defined(CGO_OS_LINUX)
const char* os = “linux”;
#else


error(unknown os)


#endif
*/
import “C”



func main() {
print(C.GoString(C.os))
}
这样我们就可以用C语言中常用的技术来处理不同平台之间的差异代码。



2.2.3 build tag 条件编译
build tag 是在Go或cgo环境下的C/C++文件开头的一种特殊的注释。条件编译类似于前面通过#cgo指令针对不同平台定义的宏,只有在对应平台的宏被定义之后才会构建对应的代码。但是通过#cgo指令定义宏有个限制,它只能是基于Go语言支持的windows、darwin和linux等已经支持的操作系统。如果我们希望定义一个DEBUG标志的宏,#cgo指令就无能为力了。而Go语言提供的build tag 条件编译特性则可以简单做到。



比如下面的源文件只有在设置debug构建标志时才会被构建:



// +build debug



package main



var buildMode = “debug”
可以用以下命令构建:



go build -tags=”debug”
go build -tags=”windows debug”
我们可以通过-tags命令行参数同时指定多个build标志,它们之间用空格分隔。



当有多个build tag时,我们将多个标志通过逻辑操作的规则来组合使用。比如以下的构建标志表示只有在”linux/386“或”darwin平台下非cgo环境“才进行构建。



// +build linux,386 darwin,!cgo
其中linux,386中linux和386用逗号链接表示AND的意思;而linux,386和darwin,!cgo之间通过空白分割来表示OR的意思。



2.3.3 结构体、联合、枚举类型
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
}



编译和链接参数是每一个C/C++程序员需要经常面对的问题。构建每一个C/C++应用均需要经过编译和链接两个步骤,CGO也是如此。 本节我们将简要讨论CGO中经常用到的编译和链接参数的用法。



2.10.1 编译参数: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)。



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



2.10.3 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实现Python模块章节。



2.10.4 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原生的代码。 其中的依赖是相对目录,对于不同的平台支持可以保持最大的一致性。



2.10.5 多个非main包中导出C函数
官方文档说明导出的Go函数要放main包,但是真实情况是其它包的Go导出函数也是有效的。 因为导出后的Go函数就可以当作C函数使用,所以必须有效。但是不同包导出的Go函数将在同一个全局的名字空间,因此需要小心避免重名的问题。 如果是从不同的包导出Go函数到C语言空间,那么cgo自动生成的_cgo_export.h文件将无法包含全部到处的函数声明, 我们必须通过手写头文件的方式什么导出的全部函数。



Category golang