• Go 中如何构建 C 的静态链接库
  • Go 中如何构建 C 的动态链接库
  • Go 中如何构建 Go 的动态链接库

目录

c-archive

这里构建的是供 C 程序调用的库。更准确一些的说,这里是把 Go 程序构建为 archive(.a) 文件,这样 C 类的程序可以静态链接 .a 文件,并调用其中的代码。

Example Project:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Project 结构
c-archive/
|-- main.go
`-- utils
    `-- util.go

1 directory, 2 files

# main.go
package main

import "C"
import (
	"fmt"

	"instance.golang.com/go-build-modes/c-archive/utils"
)

func main() {
	utils.Version()
	fmt.Println()
	Hello()
}

//export Hello
func Hello() {
	fmt.Println("hello, world!")
}

# utils/utils.go
package utils

import (
	"fmt"
)

func Version() {
	fmt.Println("go1.10.3")
}

然后我们构建这个工程:

1
go build -x -v -buildmode=c-archive .

. 表示在当前路径下编译该工程,编译生成的文件名将以该工程的包名命名:即上面命令执行后,生成如下文件: - 一个是静态库文件: c-archive.a - 一个是 C 的头文件: c-archive.h

1
2
c-archive.a:  current ar archive random library
c-archive.h:  c program text, ASCII text

在所生成的 c-archive.h 的头文件中,我们可以看到 Go 的 Hello() 函数的定义:

1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern "C" {
#endif

extern void Hello();

#ifdef __cplusplus
}
#endif

然后我们在 c-archive.c 中引入头文件 c-archive.h, 并使用 Go 编译的静态库:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// c-archive.c

// Create and Edit by ourselves

#include "c-archive.h"

int main(void)
{
    Hello();
    return 0;
}

然后,我们构建 C 程序:

1
gcc c-archive.a c-archive.c -o hello.exe

踩坑一:gcc 构建 C 程序失败

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
>   Issue: 表面意思就是找不到 Hello 这个函数
>   > $ gcc c-archive.a c-archive.c -lWinMM -lntdll -lWS2_32 -
>   o hello.exe
>   C:\Users\zhe\AppData\Local\Temp\ccEfOZGq.o:c-archive.c:(.text+0xe): undefined reference to `Hello'
>   collect2.exe: error: ld returned 1 exit status
>   
>   Reason: 
>   >>> undefined reference to `Hello' 错误是发生在链接过程中
>   >>> GCC在链接时对依赖库的顺序是敏感的,被依赖的库必须放在后面。GCC链接规定,链接时,若A和B同时需要链接,不论A/B是目标文件还是库文件,若A中引用了B的符号,例如函数或者全局变量,则在链接时,必须将A写在B前面;因为,链接时从左向右搜索外部符号。
>   >>> c-archive.a  c-archive.c 的顺序不对
>
>   Resolve: 改变 c-archive.a  c-archive.c 的顺序
>   >>> gcc c-archive.a c-archive.c -lWinMM -lntdll -lWS2_32 -
>   o hello.exe
>   ```

最后,执行 ./hello.exe

    ./hello.exe
    hello, world!

# c-shared

和前面例子不同的是,这将用 Go 代码创建一个动态链接库(Unix: .so & Win: .dll), 然后用 C 语言程序动态加载执行。

Go  C 语言的代码和上面例子是一样的,但是构建过程不同:

```bash
$ go build -buildmode=c-shared -o c-shared.so main.go

这里我们使用了 c-shared 以构建 C 的动态链接库。

注:需要注意的是,这里明确指定了 -o hello.so,这里我和演讲者不同,如果不指定输出文件名,那么默认会使用 hello 作为文件名,导致后续的操作找不到 hello.so 文件。

这次也生成了两个文件,一个是 hello.so,一个是 hello.h:

1
2
hello.h:  c program text, ASCII text
hello.so: Mach-O 64-bit dynamically linked shared library x86_64

然后,编译对应的 C 程序:

1
$ gcc hello.c hello.so -o hello.exe

对比 c-archive 和 c-shared 例子中的 hello.exe 二进制可执行文件的大小,就要发现 c-shared 例子中的 hello.exe 要小很多:

1
2
c-archive:  hello.exe  2.1M
c-shared:   hello.exe  60K

这是因为前者,将 Go 的代码静态编译进了 C 的程序中;而后者,则是动态链接,C 的可执行文件内不包含我们写的 Go 的代码,所有这部分的函数都在动态链接库中 hello.so 中了:

1
hello.so 	2.2M

因此,执行的时候我们除了需要 hello.exe 这个可执行文件外,还需要 hello.so 这个动态链接库。如果默认的 LD_LIBRARY_PATH 包含了当前目录,并且 hello.so 就在当前目录,则可以直接:

1
2
$ ./hello.exe
hello, world!

否则,如果提示找不到 hello.so, 如:

1
dyld: Library not loaded: hello.so

那,可以手动指定 LD_LIBRARY_PATH 变量,告诉操作系统到哪里找动态链接库:

1
2
3
4
5
6
# On Linux
$ LD_LIBRARY_PATH=. ./hello
Hello, world.
# On macOS
$ DYLD_LIBRARY_PATH=. ./hello
Hello, world.

为什么会需要动态链接库

从开始使用 Go 我们就反反复复的听到人说 Go 的静态链接如何方便,既然如此,那么我们为什么需要动态链接?

因为动态链接在运行时,且在需要的时候,由程序决定加载,也可以在不需要的时候卸载,这样可以节约内存资源。

最后,附上编译所用的 Makefile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PYTHON:clean

gobuild:
	@echo Linux: c-shared: .so
	go build -buildmode=c-shared -o hello.so main.go

	@echo

	@echo Win: c-shared: .dll
	go build -buildmode=c-shared -o hello.dll main.go

build:
	gcc -o hello hello.c hello.so
	./hello

	@echo

	gcc -o hello.exe hello.c hello.dll
	./hello.exe

clean:
	go clean -x
	rm -rf *.h *.exe *.so *.dll

shared

shared 模式和 c-shared 模式有点相似,都是构建一个动态链接库,以便在运行时加载。所不同的是 shared 并非构建 C 语言的动态链接库,而是专门为 Go 可执行文件构建动态链接库。

这次还是 hello.go, 不过稍有不同:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
	fmt.Println("hello, world!")
}

这里就是独立的一个文件,一个 main(),执行后打印 Hello, World。我们可以像以前一样用 exe 模式构建,然后执行。不过这次我们用一种不同的方式构建。

1
2
go install -buildmode=shared std
go build -linkshared hello.go

这里,我们首先把标准库 std 构建并安装到 $GOPATH/pkg 下,然后使用 -linkshared 标记来构建 hello.go。

执行结果和前面一样,但是如果仔细观察生成的文件,就会发先和前面很不同。

1
2
$ ls -l hello
-rwxr-xr-x 1 root root 16032 Oct  3 13:27 hello

可以看到这个 Hello World 程序只有十几KB大小。对于 C 程序员来说,这没啥惊讶的,因为就应该这么大啊。但是对于 Go 程序员来说,这就是很奇怪了,因为一般不都得 7~8MB 么?

其原因就是使用了动态链接库,所有标准库部分,都用动态链接的办法来调用,构建的二进制可执行文件中只包含了程序部分。C 程序构建的 Hello World 之所以小,也是因为动态链接的原因。

如果我们查阅程序所调用的库就可以看到具体情况:

1
2
3
4
5
6
7
$ ldd hello
        linux-vdso.so.1 (0x00007ffed3d4e000)
        libstd.so => /usr/local/go/pkg/linux_amd64_dynlink/libstd.so (0x00007f608c409000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f608c06a000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f608be66000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f608bc49000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f608e866000)

如果我们进一步去查看 libstd.so,就会看到一个巨大的动态链接库,这就是 Go 的标准库:

1
-rw-r--r-- 1 root root 37M Oct  3 13:27 /usr/local/go/pkg/linux_amd64_dynlink/libstd.so

当然,要使用这个模式需要很多准备工作,所有的动态链接库都需要在指定的位置,版本都必须兼容等等,所以我们一般不常用这个模式。

Click here to checkout the Repo

See Also

Thanks to the authors 🙂

返回目录