如何在golang 中调用c的静态库或者动态库

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在golang 中调用c的静态库或者动态库相关的知识,希望对你有一定的参考价值。

Cgo 使得Go程序能够调用C代码. cgo读入一个用特别的格式写的Go语言源文件, 输出Go和C程序, 使得C程序能打包到Go语言的程序包中.
举例说明一下. 下面是一个Go语言包, 包含了两个函数 -- Random 和 Seed -- 是C语言库中random和srandom函数的马甲.
package rand

/*
#include <stdlib.h>
*/ import "C" func Random() int return int(C.random()) func Seed(i int) C.srandom(C.uint(i))

我们来看一下这里都有什么内容. 开始是一个包的导入语句.
rand包导入了"C"包, 但你会发现在Go的标准库里没有这个包. 那是因为C是一个"伪包", 一个为cgo引入的特殊的包名, 它是C命名空间的一个引用.
rand 包包含4个到C包的引用: 调用 C.random和C.srandom, 类型转换 C.uint(i)还有引用语句.
Random函数调用libc中的random函数, 然后回返结果. 在C中, random返回一个C类型的长整形值, cgo把它轮换为C.long. 这个值必需转换成Go的类型, 才能在Go程序中使用. 使用一个常见的Go类型转换:
func Random() int return int(C.random())

这是一个等价的函数, 使用了一个临时变量来进行类型转换:
func Random() int var r C.long = C.random() return int(r)

Seed函数则相反. 它接受一个Go语言的int类型, 转换成C语言的unsigned int类型, 然后传递给C的srandom函数.
func Seed(i int) C.srandom(C.uint(i))

需要注意的是, cgo中的unsigned int类型写为C.uint; cgo的文档中有完整的类型列表.
这个例子中还有一个细节我们没有说到, 那就是导入语句上面的注释.
/*
#include <stdlib.h>
*/ import "C"

Cgo可以识别这个注释, 并在编译C语言程序的时候将它当作一个头文件来处理. 在这个例子中, 它只是一个include语句, 然而其实它可以是使用有效的C语言代码. 这个注释必需紧靠在import "C"这个语句的上面, 不能有空行, 就像是文档注释一样.
Strings and things
与Go语言不同, C语言中没有显式的字符串类型. 字符串在C语言中是一个以0结尾的字符数组.
Go和C语言中的字符串转换是通过C.CString, C.GoString,和C.GoStringN这些函数进行的. 这些转换将得到字符串类型的一个副本.
下一个例子是实现一个Print函数, 它使用C标准库中的fputs函数把一个字符串写到标准输出上:
package print // #include <stdio.h> // #include <stdlib.h> import "C" import "unsafe" func Print(s string) cs := C.CString(s) C.fputs(cs, (*C.FILE)(C.stdout)) C.free(unsafe.Pointer(cs))

在C程序中进行的内存分配是不能被Go语言的内存管理器感知的. 当你使用C.CString创建一个C字符串时(或者其它类型的C语言内存分配), 你必需记得在使用完后用C.free来释放它.
调用C.CString将返回一个指向字符数组开始处的指错, 所以在函数退出前我们把它转换成一个unsafe.Pointer(Go中与C的void 等价的东西), 使用C.free来释放分配的内存. 一个惯用法是在分配内存后紧跟一个defer(特别是当这段代码比较复杂的时候), 这样我们就有了下面这个Print函数:
func Print(s string) cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) C.fputs(cs, (*C.FILE)(C.stdout))

构建 cgo 包
如果你使用goinstall, 构建cgo包就比较容易了, 只要调用像平常一样使用goinstall命令, 它就能自动识别这个特殊的import "C", 然后自动使用cgo来编译这些文件.
如果你想使用Go的Makefiles来构建, 那在CGOFILES变量中列出那些要用cgo处理的文件, 就像GOFILES变量包含一般的Go源文件一样.
rand包的Makefile可以写成下面这样:
include $(GOROOT)/src/Make.inc

TARG=goblog/rand
CGOFILES=\ rand.go\ include $(GOROOT)/src/Make.pkg

然后输入gomake开始构建.
更多 cgo 的资源
cgo的文档中包含了关于C伪包的更多详细的说明, 以及构建过程. Go代码树中的cgo的例子给出了更多更高级的用法.
一个简单而又符合Go惯用法的基于cgo的包是Russ Cox写的gosqlite. 而Go语言的网站上也列出了更多的的cgo包.
最后, 如果你对于cgo的内部是怎么运作这个事情感到好奇的话, 去看看运行时包的cgocall.c文件的注释吧.
参考技术A   1. 无论调用动态库还是静态库都只需要include库的头文件就可以了
  2. 要在调用该静态库的地方添加库引用,并设置路径。
  结论:其实静态库调用动态库或者静态库,只是在用到库方法的地方把该方法添加到LIB当中,真正使用的地方才会把这些库LINK起来生成可执行文件。
参考技术B 1. 无论调用动态库还是静态库都只需要include库的头文件就可以了
2. 要在调用该静态库的地方添加库引用,并设置路径。
结论:其实静态库调用动态库或者静态库,只是在用到库方法的地方把该方法添加到LIB当中,真正使用的地方才会把这些库LINK起来生成可执行文件。

CGO静态库和动态库

CGO静态库和动态库

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

使用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} -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库的源文件链接到当前的包。

#include "./number/number.c"

然后在执行go getgo 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} -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库的模加法函数。

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
", a, b, c, x);

    return 0;
}

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

$ gcc -o a.out _test_main.c number.a
$ ./a.out

使用CGO创建静态库的过程非常简单。

导出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
", a, b, c, x);

    goPrintln("done");
    return 0;
}

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

以上是关于如何在golang 中调用c的静态库或者动态库的主要内容,如果未能解决你的问题,请参考以下文章

C语言中静态库和动态库的区别,如何使用它们

python调用golang编写的动态链接库

kotlin-native04 调用c动态库

CGO静态库和动态库

CGO静态库和动态库

NDK下如何使用第三方静态库?