pkg-config结合CGO开发小记
这一次开发小记主要分为两部分,关于pkg-config的配置相关,以及CGO的基本使用并结合pkg-config的例子
引入
最初开始捣腾这个pkg-config
,是在关于GO接入ffmpeg库的过程中,使用的接入库采用了这个。这个工具其实是pkgconf
的别名,是一个帮助编译器获得目标库相关信息的工具,在使用诸如gcc
或者g++
这类编译器中,编译器需要经过编译+链接的过程才能生成完整的可执行文件,而如果采用的第三方库则需要找到对应的库,并将其代码链接到目标文件。在Linux下,如果相关的库是通过包管理或者make install
安装后,库文件会复制到/usr/lib
下,头文件复制到/usr/include
下,而编译器默认包含了这些路径,因此如果是正常编译,就会自动找到需要的库并进行链接。
但如果没有将其安装到系统默认目录,或者有多个不同的环境,就需要指定参数告诉编译器所需文件的位置,这个过程是通过三个参数来指定的。-I
指定头文件查找的路径,-L
指定库文件查找的路径,-l
指定需要链接的库,值得一提的是库文件基本上都是以libxxx.so
或libxxx.a
,在指定-l
时需要去掉lib
的前缀。对于gcc
而言,因为参数是要传递给链接器ld
的,有时需要使用-Wl
参数
-Wl
后面的东西是作为参数传递给链接器ld
的。比如:
gcc -Wl,aaa,bbb,ccc
最后会被解释为:
ld aaa bbb ccc
gcc在未指定时默认采用的是动态链接,失败后再去找静态链接,而Bstatic
和Bdynamic
可以指定要链接的是动态库或者静态库,在目录下同时存在动态和静态库时很有用
-Wl,-Bstatic -laaa -lbbb -lccc -Wl,-Bdynamic -lddd -l ccc
不过大多数时候其实不一定非得直接传参给链接器,gcc本身也可以直接指定使用静态链接,例如
gcc -static main.cc -L=$HOME/cpp-code/demo/static -lmymath -o main-static.out
as-needed
和no-as-needed
可以指定在链接过程中,是否要检查所有的依赖库,只会将实际被引用的库写入文件头
-Wl,--as-needed
-Wl,--no-as-needed
库依赖的自动管理
其实写完上面一大堆之后,关于编译相关的选项依旧是一头雾水,有说gcc
的-L
只能指定静态库的,有说用-Wl,-rpath
的,更何况如果同时引入了多个库,那就要一个个将路径配置正确,这个过程会非常痛苦。而pkg-config
就能代劳这些工作,它本质上利用的是*.pc
文件描述了这个库的信息,在需要的时候,会自动解析信息并生成编译器所需要的参数
在安装了pkg-config
后,后续安装的库会自动为其生成.pc
文件,输入pkg-config --list-all
可以看到所有的模块相关信息
bash-completion bash-completion - programmable completion for the bash shell
iso-codes iso-codes - ISO country, language, script and currency codes and translations
libcrypt libxcrypt - Extended crypt library for DES, MD5, Blowfish and others
libdmmp libdmmp - Device mapper multipath management library
libnsl libnsl - Library containing NIS functions using TI-RPC (IPv6 enabled)
libtirpc libtirpc - Transport Independent RPC Library
libxcrypt libxcrypt - Extended crypt library for DES, MD5, Blowfish and others
opus Opus - Opus IETF audio codec (floating-point build)
shared-mime-info shared-mime-info - Freedesktop common MIME database
systemd systemd - systemd System and Service Manager
udev udev - udev
x264 x264 - H.264 (MPEG4 AVC) encoder library
x265 x265 - H.265/HEVC video encoder
xkeyboard-config XKeyboardConfig - X Keyboard configuration data
而如果输入pkg-config 模块名 --libs --cflags
,就能看到实际上传给编译器的参数了
~$ pkg-config opus --libs --cflags
-I/usr/include/opus -lopus
那么他是如何实现的,答案就在这个描述文件中,以目前所需的ffmpeg库为例,打开位于源码的lib/pkgconfig
,以libavcodec.pc
为例
prefix=${pcfiledir}/../..
exec_prefix=${prefix}
libdir=${prefix}/lib
includedir=${prefix}/include
Name: libavcodec
Description: FFmpeg codec library
Version: 59.37.100
Requires:
Requires.private: libswresample >= 4.7.100, libavutil >= 57.28.100
Conflicts:
Libs: -L${libdir} -lavcodec
Cflags: -I${includedir}
可以看到,这里面描述了这个模块所需编译参数的所有信息,结合上述信息,就能生成对应的编译参数,那么要如何使用它,pkg-config
默认会在/usr/lib/pkgconfig
下查找所有.pc
文件,如果遇到无法安装到系统目录的情况,可以通过指定PKG_CONFIG_PATH
来额外添加查找的位置,多个目录用:
隔开
$ export PKG_CONFIG_PATH="/home/ldai/go-astiav/tmp/lib/pkgconfig"
$ pkg-config --list-all
bash-completion bash-completion - programmable completion for the bash shell
iso-codes iso-codes - ISO country, language, script and currency codes and translations
libavcodec libavcodec - FFmpeg codec library
libavdevice libavdevice - FFmpeg device handling library
libavfilter libavfilter - FFmpeg audio/video filtering library
libavformat libavformat - FFmpeg container format library
libavutil libavutil - FFmpeg utility library
libcrypt libxcrypt - Extended crypt library for DES, MD5, Blowfish and others
libdmmp libdmmp - Device mapper multipath management library
libnsl libnsl - Library containing NIS functions using TI-RPC (IPv6 enabled)
libpostproc libpostproc - FFmpeg postprocessing library
libswresample libswresample - FFmpeg audio resampling library
libswscale libswscale - FFmpeg image rescaling library
libtirpc libtirpc - Transport Independent RPC Library
libxcrypt libxcrypt - Extended crypt library for DES, MD5, Blowfish and others
opus Opus - Opus IETF audio codec (floating-point build)
shared-mime-info shared-mime-info - Freedesktop common MIME database
systemd systemd - systemd System and Service Manager
udev udev - udev
x264 x264 - H.264 (MPEG4 AVC) encoder library
x265 x265 - H.265/HEVC video encoder
xkeyboard-config XKeyboardConfig - X Keyboard configuration data
可以看到,ffmpeg相关的库被添加进来了,接下来尝试输出它的编译器路径
$ pkg-config libavcodec --libs --cflags
-I/home/ldai/go-astiav/tmp/lib/pkgconfig/../../include -L/home/ldai/go-astiav/tmp/lib/pkgconfig/../../lib -lavcodec
然后只需要这样用,他就会自动生成参数(注意其中使用的是反引号)
gcc test.c -o test `pkg-config libavcodec libavformat libavutil --cflags --libs`
在Windows使用pkg-config
很显然,pkg-config
是一个在Linux的工具,想要在Win上面使用并不容易。其实并不是很推荐这种用法,之所以需要这么做是因为在Go那边的第三方库使用了这个工具来配置编译信息。
期间略过一大堆复杂的资料查找后,要在Win底下使用pkg-config
,需要安装MSYS2
,简单来说就是一个软件分发和编译平台,和Cygwin
这种还原类Linux开发环境的不同,MSYS2
倾向于提供原生Win软件的开发环境,包管理器是pacman
,它提供最新的原生工具,如GCC, mingw-w64, CPython, CMake, Meson, OpenSSL, FFmpeg, Rust, Ruby等等,关于两者的比较,可以参考这篇文章
如果嫌慢,可以使用国内镜像源
进入官网完成安装,打开MSYS2命令行,安装过程中需要访问网络的部分会有点慢,如果想要集成到Windows Terminal中,不妨参考这个文章
推荐使用
UCRT64
的environment,它的C标准库使用的是ucrt
,原始的MSYS
使用的是cygwin
然后,可以先使用pacman -Syuu
更新下仓库,然后开始进行软件包的安装,如果先前没有GCC编译器,从这里安装不妨是个好选择
pacman其实是ArchLinux的包管理工具
安装GCC编译器(可选)
最简单的方式就是只安装gcc组件包,这样就只会安装基本编译器而不安装其他库
pacman -S mingw-w64-x86_64-gcc
或者,一次性安装所有组件,会给出一个软件包列表,留空可以全部安装,或者输入用空格隔开的编号来选择安装
pacman -S mingw-w64-x86_64-toolchain base-devel
:: 在组 mingw-w64-x86_64-toolchain 中有 19 成员:
:: 软件仓库 mingw64
1) mingw-w64-x86_64-binutils 2) mingw-w64-x86_64-crt-git 3) mingw-w64-x86_64-gcc 4) mingw-w64-x86_64-gcc-ada
5) mingw-w64-x86_64-gcc-fortran 6) mingw-w64-x86_64-gcc-libgfortran 7) mingw-w64-x86_64-gcc-libs
8) mingw-w64-x86_64-gcc-objc 9) mingw-w64-x86_64-gdb 10) mingw-w64-x86_64-gdb-multiarch
11) mingw-w64-x86_64-headers-git 12) mingw-w64-x86_64-libgccjit 13) mingw-w64-x86_64-libmangle-git
14) mingw-w64-x86_64-libwinpthread-git 15) mingw-w64-x86_64-make 16) mingw-w64-x86_64-pkgconf
17) mingw-w64-x86_64-tools-git 18) mingw-w64-x86_64-winpthreads-git 19) mingw-w64-x86_64-winstorecompat-git
输入某个选择 ( 默认=全部选定 ):
安装pkg-config
接下来就是pkg-config
的安装了,而且就算是你进行了上面的可选操作,依然要进行下面的步骤,甚至,如果你选择了全部安装,可以看到,软件包列表里面是有我们想要的mingw-w64-x86_64-pkgconf
,但是在这里,甚至要额外卸载它
pacman -R mingw-w64-x86_64-pkgconf // 如果安装了需要卸载
为什么要卸载这个版本,是因为在mingw64
下的这个pkg-config
,在处理Windows的反斜杠路径时,是有问题的,在生成编译器参数时,反斜杠等一些特殊字符会被直接吃掉
pkg-config libavfilter --libs
-LD:/Code/go_test/gomedia/ffmpeg-n5.1.2-win/lib -lavfilter // 预期的输出
-LDCodego_testgomediaffmpeg-n5.1.2-winlib -lavfilter // 实际的输出
结果就是直接导致编译无法进行,在这里没少踩坑,后来在偶然的机会发现以前用过的vcpkg
时,其内部也使用了pkg-config
,发现那个版本的处理结果是正确的,经过比对后发现,是需要使用mingw32
的版本,因此需要安装的是这个版本
pacman -S mingw-w64-i686-pkgconf
安装完成后,mingw64
的版本在mingw64/bin
下,刚刚安装的mingw32
版本在mingw32/bin
下,接下来就是配置环境变量让命令行能访问这些工具。在PATH
中添加以下变量,其中C:\msys64
是你的MSYS2的安装路径
C:\msys64\mingw64\bin
C:\msys64\mingw32\bin
C:\msys64\ucrt64\bin
C:\msys64\usr\bin
这里又有一点需要注意了,环境变量的优先级是有顺序的,一定要确保usr/bin
的路径在上面两个mingw路径的底下,因为usr/bin
目录下也有个pkg-config
,而且那个版本也是同样有问题的,而且,一定要确保mingw64
版本的已经正确卸载,不然按照这个顺序,也会优先使用到错误的版本上
如果你不确定使用的版本是否正确,可以使用MSYS2提供的which
命令来确认使用的版本
which pkg-config
/cygdrive/c/msys64/mingw32/bin/pkg-config
接下来,就可以在Linux当中一样,可以在系统环境变量中配置,或者在终端临时设置PKG_CONFIG_PATH
变量,然后执行--libs
参数查看输出的编译参数,自此Win版本的就配置完成了
Go与C的结合:在CGO中使用
Go引入c相关的实现还是比较容易的,这里概述一下较为通用的方式
使用CGO前提:要使用
CGO_ENABLED=1
打开CGO的支持,并且已经配置好gcc编译器的支持,不支持MSVC以及其他编译器,如果涉及到不同平台的交叉编译,需要参考这篇来配置相关工具链的支持
通用引入方式
Go使用一种比较特殊的注释来引入对于C代码的支持,可以参考下面这个例子
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)) //释放内存
}
- 其中
import "C"
是必不可少的,这支持编译器使用对于CGO的支持,而且必须紧跟于被“注释”的C相关代码之后 - 同样的,从C中导入的方法和变量都需要通过访问
C
这个包进行,而且需要特别注意内存管理,因为使用纯C相关的实现将会导致不再受到Go内存管理的影响,例如上述的C.CString
是一个char *
字符串,而unsafe.Pointer
就相当于void *
,因此需要记得进行释放否则将会导致泄漏
当然,你可以直接在上面的注释块中编写任何C语言代码,或者将写好的C代码include进来,但更多时候,我们希望Go能使用的是现成的C相关库,无论是出于高性能还是具体实现的原因,这意味着需要使用动态链接,关于动态链接库的使用,可以参考下面这个例子:
package paddle
// #cgo CFLAGS: -I${SRCDIR}/paddle_inference_c/paddle/include
// #cgo LDFLAGS: -L${SRCDIR}/paddle_inference_c/paddle/lib -lpaddle_inference_c
import "C"
${SRCDIR}
是当前文件所在的目录,注意这个目录是你写这个注释的源代码文件所在的目录,而不是main
包所在的目录,例如你的这个lib.go
在/paddle/lib.go
,那么这个路径指的是/paddle
而不是/
- 这个路径风格虽然采用的是POSIX的正斜杠方式,但其实这种写法是可以在POSIX和Windows通用的
- 这个库也需要是通过gcc编译器产生的,在Win平台上如果是用MSVC的,唯一的一条路就是通过系统调用,这部分不在本文讨论范围内
// #cgo
注释指示了Go编译器关于链接库的相关信息,其中CFLAGS
是编译器头文件需要include的位置,LDFLAGS
是连接时库的位置,如果你有印象,可能会想到--cflags --libs
这两个参数在上文提到pkg-config时也有用到过
如果想了解更多关于CGO的使用,这几篇文章是一个很好的入门和参考材料
结合pkg-config来使用外部库
既然刚刚提到,上文CFLAGS
和LDFLAGS
这两个参数和pkg-config如此相像,那么能不能利用pkg-config的快速提供库路径的能力,来简化相关依赖的配置。答案是可以的,我上文正在倒腾的ffmpeg库采用的就是这种方式。
要使用这种方式导入,只需在上文保证pkg-config配置正确的情况下,将cgo
注释替换成这样:
//#cgo pkg-config: libswscale libswresample
import "C"
那么在编译时,它就会实时展开这些路径,并指引编译器和链接器找到正确的库文件,对于Windows和Linux而言,这个ffmpeg对库的需求还不一样,因此还需要通过Go的条件编译来进行区分编译,这里采用的是文件名后缀的方式
// ffmpegLib_windows.go
//#cgo pkg-config: libswscale libswresample
import "C"
// ffmpegLib_linux.go
//#cgo pkg-config: libswscale libpostproc libswresample
import "C"
当然,你也可以用// +build <tags>
的方式,更多关于条件编译用法,可以参考这篇文章
建议
为什么要多此一举再加个这个章节,因为这一轮倒腾下来,其实我个人是不推荐使用pkg-config这个方式的,之所以硬要用,纯粹只是因为找的这个ffmpeg库直接就用了这种方式,因此没得选
之所以不推荐,除了在Win上的关于pkg-config这各种坑之外,还在与对其跨平台的限制性太大,使用这种方式来管理依赖,意味着无论是在何种平台都必须事先通过这种方式来配置相关的库,这样一路下来很容易得不偿失,还不如直接将相关实现或者库直接嵌入项目中来得更快。虽然这篇文章看下来,似乎是为了能在Go使用pkg-config特地包的饺子,但实际上在最初研究的时候,是因为在Win平台各种编译问题下,通过定位到这种引入方式导致路径展开错误的问题后,再倒推回第一部分里面关于pkg-config的各种配置的,因此实际踩坑的过程要比这文中描述的痛苦的多,更别提这个库作者特别指定了一个特定版本ffmpeg-n5.1.2
进行适配,光是找到对应版本就倒腾了半天
因此并不建议采用这种引入方式,建议在更多时候考虑用通用的引入方式或者直接使用系统调用来进行
而且即便是最后成功跑通了之后,在前期测试时发现还会不定时地从ffmpeg底层抛出一些奇怪的错误,基本上是没法用,竹篮打水一场空了属于是