Go 调用 C 写的动态库完整例子(Linux版)

总有那么一些老的,或高效的库是用 C/C++ 实现的,于是在其他语言中如果使用动态共享库就成了个问题。Java 要调用动态库需要用 JNI, 更快捷的话可使用第三方包装好的 JNI 调用库。在 Java 中要映射 C/C++ 的类型麻烦些,因为 Java 没有指针类型,所以从这方面来讲 Go 调用动态库幸许会更简单些。

下面我们自己在 Linux 下做一个动态库(.so 文件  - Shared Object),然在用 Go 来使用它。本文所用的操作系统为 Ubuntu20.04, 以 gcc  作为编译器。动态库的生成过程参考自 Linux动态库生成与使用指南

我们用动态库实现一个拼接字符串与整数的函数,首选是 add.h  文件中的函数声明
1#ifndef __ADD_H__
2#define __ADD_H__
3
4char* Add(char* src, int n);
5
6#endif
然后是 add 函数的实现 add.c 文件,内容为
 1#include <string.h>
 2#include <stdio.h>
 3#include <stdlib.h>
 4
 5char* Add(char* src, int n)
 6{
 7    char str[20];
 8    sprintf(str, "%d", n);
 9    char *result = malloc(strlen(src)+strlen(str)+1);
10    strcpy(result, src);
11    strcat(result, str);
12    return result;
13}
接着用命令编译生成动态库,在 Linux 下的文件名是  libadd.so
$ gcc -fPIC -shared -o libadd.so add.c
会在当前目录下生成 libadd.so 文件, 在 Linux 下可用 nm -D libadd.so  查看其中的方法

现在用一个 C 语言代码来使用它,代码文件为 test.c, 内容
1#include <stdio.h>
2#include "add.h"
3
4int main(int argc, char *argv[])
5{
6    char* aa = "giter";
7    printf("%s\n", Add(aa, 8));
8    return 0;
9}
链接动态库生成可执行文件
$ gcc test.c -L . -ladd -o test
-L .表示搜索要链接的库文件时包含当前目录
-ladd  表示要链接动态库 libadd.so
-o test 生成可执行文件 test

运行 test

由于 libadd.so 是动态库,也就是执行期需要加载它,假如直接执行 test 会怎么样呢?
$ ./test
./test: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
找不到动态库 libadd.so,  Linux 通过 ldconfig 的指示在某些目录中(如 /lib, /user/lib) 搜索动态库。更简单的办法是用 LD_LIBRARY_PATH 环境变量,如
$ LD_LIBRARY_PATH=. ./test
giter8
至此,动态库 libadd.so 准备好了,并且用 test 验证了它是可用的,接下来就在 Go 语言中使用该动态库的函数。

以下是成功的例子,测试完之后发现很简单,可是过程中碰到许多的问题。后面会列出所遇到见的问题

假设项目目录为 /home/vagrant/testgo (用的 Vagrant 启动的 Ubuntu 20.04 进行本文中的测试),目录结构如下
testgo
├── lib
│     └── libadd.so
└── src
        ├── add.h
        └── main.go
main.go 的代码如下:
 1package main
 2
 3/*
 4#cgo CFLAGS: -I.     // 头文件的位置,相对于源文件是当前目录,所以是 .,头文件在多个目录时写多个  #cgo CFLAGS: ...
 5#cgo LDFLAGS: -L../lib -ladd -Wl,-rpath,lib  // 从哪里加载动态库,位置与文件名,-ladd 加载 libadd.so 文件
 6#include "add.h"
 7*/
 8import "C"
 9import "fmt"
10
11func main() {
12  val := C.Add(C.CString("go"), 2021)
13  fmt.Println("Hello c value: ", C.GoString(val))
14}
通过注释代码来告诉 Go 编译器从哪里引入头文件与加载动态库. 本例中 *.h 和 *.go 文件在同一个目录的情况下, #cgo CFLAGS: -I. 可不写。

CFLAGS: -I 和 LDFLAGS: -L 都是相对于源文件 main.go 的位置

执行,命令行进到 /home/vagrant/testgo 目录
~/testgo$ go run src/main.go
Hello c value: go2021
成功调用 C 实现的 add 函数

下面列出一些问题

import "C" 要紧挨着 /*...*/ 注释块,如果写成
1/*
2#cgo ...
3*/
4import "C"
会出现错误
# command-line-arguments
src/main.go:15:10: could not determine kind of name for C.add
import "C" 要独占一行, 试图同时引入其他的库,如 import ("C"; "fmt") 也会报上面同样的错误

加载不到头文件的错误很明显,#include "add.h" 时会告诉你该文件不存在,如果没有加载到正确的头文件调用 C.Add() 函数时就会报错
# command-line-arguments
src/main.go:13:10: could not determine kind of name for C.Add
还有一个关键是能否加载到动态库 libadd.so, 参考了网上一些例子,如果把第五行改为
#cgo LDFLAGS: -L../lib -ladd
执行时会出错
/tmp/go-build3845117109/b001/exe/main: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
exit status 127
但如果设置了环境变量 LD_LIBRARY_PATH=/home/vagrant/testgo/lib 也能让它跑起来
LD_LIBRARY_PATH=/home/vagrant/testgo/lib go run src/main.go
Hello c value: go2021
链接:

  1. 全面总结:Golang 调用 C/C++, 例子式教程
  2. golang 学习 (10): 使用go语言调用c语言的so动态库
  3. GoLang 调用 .so 文件 (go plugin 调用 go 生成的动态库)
永久链接 https://yanbin.blog/go-invoke-c-dylib-linux/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。