cgo实践三:swig介绍

1,460 阅读6分钟

swig 对于写 C/C++ 的同学来说并不陌生,它是一个开发工具,用以把当前的C/C++项目和其他高层次语言连接起来,直白的说就是:可以生成供其他语言调用的api 接口代码。那么对于Go的项目来说,就是他可以直接为C/C++公共项目封装生成对应的cgo代码,作为Go应用开发的同学不用再手动封装cgo代码(别人给你写好了),从而降低了接入方的成本。

而且swig不单是针对Go语言来设计的,而是适配了众多的高级语言,如PythonJavaPHPJavascript 等等,都可以生成对应语言所调用的接口,方便应用层快速接入。

本文主要是介绍swigGo语言集成,如果你对其他语言也感兴趣,不妨去查阅一下官方文档

在实际应用中,如果是大型C++项目,使用swig会如虎添翼。但如果是一个小型的C项目,使用swig反而会把事情复杂化,所以最好视具体情况评估。

安装 swig

linux 安装直接下载源码编译即可:www.swig.org/download.ht…yum 也提供swig 包,但是默认版本比较老旧,macOS 可以直接使用 brew install swig 进行安装。如果你还需要选择其它平台,可以查看官方文档:swig install

wget http://prdownloads.sourceforge.net/swig/swig-{versoin}.tar.gz && tar -zxvf
cd swig-*
./configure
make
sudo make install

使用 swig 流程

使用swig也不是很复杂,主要分为两个步:

  1. 定义 swig 接口文件(swig interface file);
  2. 使用 swig 命令行生成对应语言的代码;
  3. 集成使用。

swig接口文件

在使用swig时,我们需要提供一个接口定义文件(SWIG interface file),文件后缀往往是 .i 或者 .swig,这个文件用来告诉swig具体的操作逻辑,一个常规的接口文件大致内容如下:

/* File : example.i */
%module example

%{
/* Put headers and other declarations here */
extern double My_variable;
extern int    fact(int);
extern int    my_mod(int n, int m);
%}

extern double My_variable;
extern int    fact(int);
extern int    my_mod(int n, int m);

/*...*/ 代表的是注释部分。

%modulego中表示对应的 package 包名。最终对应生成的cgo文件会转变为:

// example.go
package example

%{...%} 代表额外要插入的头文件声明变量或函数声明(如果有),对应cgo文件的如下部分区域,这和直接编写cgo文件没有区别:

/*
extern void _wrap_My_variable_set_example_dffde00251d303fd(double arg1);
extern double _wrap_My_variable_get_example_dffde00251d303fd(void);
*/
import "C"

import "unsafe"
import _ "runtime/cgo"

我们拿example.i声明的My_variable变量来说,你会发觉,最终生成的代码并没有直接是原生的My_variable名称,而是提供了包裹的俩GetterSetter 函数。swig 默认会对我们需要暴露的数据类型做一层包装,而且这些代码都是通过命令行工具结合 example.i 接口文件动态生成的,所以你不能直接修改这些代码。

之所以要包裹一层,是因为swig需要做额外的数据管理,比如内存分配,内存释放等。使用方不用关心这些变量的内存管理问题,swig来包装生成对应的内存管理函数方法。因为对于go这种自动内存管理,和C 这种手动管理内存,通信数据的内存管理是不对等的,就像是前两节手撸cgo代码提到的char * 等指针类型数据内存释放注意问题。

example.i剩下部分,则表示的是暴露给go直接使用的函数方法,swig会负责把如下三行生成对应的go函数:

...
extern double My_variable;
extern int    fact(int);
extern int    my_mod(int n, int m);

最终的example.go会生成类似如下函数方法:

// 变量
func SetMy_variable(arg1 float64) {
    _swig_i_0 := arg1
    C._wrap_My_variable_set_example_dffde00251d303fd(C.double(_swig_i_0))
}

func GetMy_variable() (_swig_ret float64) {
    var swig_r float64
    swig_r = (float64)(C._wrap_My_variable_get_example_dffde00251d303fd())
    return swig_r
}

// 函数
func Fact(arg1 int) (_swig_ret int) {
    var swig_r int
    _swig_i_0 := arg1
    swig_r = (int)(C._wrap_fact_example_dffde00251d303fd(C.swig_intgo(_swig_i_0)))
    return swig_r
}

// ...

swig默认会为每个变量声明一组GetterSetter函数来操作这组数据,如果你希望数据是readonly的,那么你需要添加额外的指令 %immutable, 此指令只会生成Getter 函数。它对于定义在%{ %}中和之外的变量都适用。

/* Some read-only variables */

%immutable;

%inline %{
extern double My_variable;
%}

extern double My_variable;

swig 操作命令

swig 工具为每个集成的高级语言都有对于的命令行参数,拿Go来说必要的参数时:

-go
-cgo
-intgosize

如果我们希望把上述example.i生成对应的cgo文件,那么执行一下命令即可:

swig -go -cgo -intgosize 64 example.i

最终会生成如下的文件:

example/
├── example.go
├── example.i
└── example_wrap.c

example.go 即是最终需要在Go语言集成的代码文件,而配套的example_wrap.c 则是需要在你的公共库一起编译的文件,二者缺一不可。

上述参数中 intgosize 需要告诉生成工具当前go编译整型字节长度是64位架构的还是32位的,这点比较特殊,究其原因,我们也大致能猜到,Go语言中尤其是64位操作系统中int所占字节和C语言所占字节数是不一样的,需要额外适配,使用原生cgo编码也会遇到样的问题。

其他参数说明

参数说明
-cgo生成文件将作为go tool cgo输入使用,go1.5版本之后可用,未来会考虑内置为默认参数
-intgosize 设置Go int 类型长度,s取值位 3264
-package 设置导出文件包名,类似%module
-use-shlib生成共享库so,再需要把go项目导出为共享库是才有用
-soname 共享库名称
-gccgo针对选择gccgo编译时,默认只生成基于gc编译器文件
-go-pkgpath 针对选择gccgo编译器时,设置的pkgpath
-go-prefix 针对选择gccgo编译器时,设置路径前缀。如果-go-pkgpath存在,则此变量忽略

不过,在实践中,我们发觉不使用-cgo也是可行的,因为一般我们直接会使用go build直接构建,它内部会自动识别cgo代码而自动选择cgo工具来处理。另外对于编译Go代码我们一般都是使用内置go compiler默认编译器,所以相关的gccgo编译器其实使用场景不多。

集成使用

有了对swig基础了解后,针对上述的演示项目,假设我们有一个C接口函数需要暴露给Go使用:

char* getName()

那么我们只需要在C库实现这个函数功能,例如:

char* getName(){
    return "hello the swig";
}

把这个文件命名为example.c, 并且和example.i 都放在 example/路径下,然后声明 example.i 接口:

%module example

%{
extern char* getName();
%}

/*暴露给Go使用的函数*/
extern char* getName();

请注意上面的代码,暴露给Go函数一定要和%{...%}中声明的函数配套,缺一不可。因为最终生成的文件实质就是在调用%{...%}代码块中声明的函数。

cgo-swig-01.png

从上图我们可以看出(红色箭头)表示最终生成的代码,而真正的调用过程也变成了绿色箭头所示的步骤。

然后运行:

swig -go -intgosize 64 example/example.i

最终的目录文件结构如下:

example
├── example.c
├── example.go
├── example.i
└── example_wrap.c

之后便是直接在 Go代码直接使用这个包目录即可。

package main

import (
   "cgo/example"
   "fmt"
)

func main() {
   fmt.Println(example.GetName()) 
}

最终输出如下结果:

# go run main.go
hello the swig

相关资料

  1. swig 调用cgo代码示例:github.com/swig/swig/t…
  2. swig 调用cgo文档:Running SWIG with Go.