golang调用c库,cgo(一)

基本使用

有时候我们需要使用golang去调用一些c的类库,因为使用golang重复实现一遍比较耗时,一些成熟的功能直接调用更好。当然前提是要先安装该c库。CGO可以直接用C的代码,或者C的静态库,或者动态库,当然C++也是可以的。

golang中的CGO特性,能够创建调用C代码的Go包。

package main

import "C"

func main() {

}

然后在编译的时候,需要指定CGO_ENABLED=1,当然,默认值就是1,因此不用指定。

> go env |grep CGO
GCCGO="gccgo"
CGO_ENABLED="1"
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"

默认的C编译器是 gcc。

一旦关闭就会影响 CGO 编译。需要特别留意,交叉编译时会默认关闭 CGO。

package main

/*

#include <stdio.h>
#include <stdlib.h>

*/
import "C"

func main() {
	s := C.CString("hello world.")
	C.puts(s)
    C.free(unsafe.Pointer(s))
}
> go run main.go
hello world.

注意,注释要紧挨着import "C",否则就是普通的注释,而不是指令。

C.puts(s)是调用stdio.h中的puts()函数,但是,需要先将go字符串转换成c字符串,所以C.CString()是由伪包C提供的函数。

cgo将当前包引用的C语言符号都放到了虚拟的C包中,同时当前包依赖的其它Go语言包内部可能也通过cgo引入了相似的虚拟C包,但是不同的Go语言包引入的虚拟的C包之间的类型是不能通用的。这个约束对于要自己构造一些cgo辅助函数时有可能会造成一点的影响。

import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。

// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"
基本数据类型

golang 和 C 的基本数值类型转换对照表如下:

C 语言类型CGO 类型Go语言类型
charC.charbyte
singed charC.scharint8
unsigned charC.ucharuint8
shortC.shortint16
unsigned shortC.ushortuint16
intC.intint32
unsigned intC.uintuint32
longC.longint32
unsigned longC.ulonguint32
long long intC.longlongint64
unsigned long long intC.ulonglonguint64
floatC.floatfloat32
doubleC.doublefloat64
size_tC.size_tuint

注意 C 中的整形比如 int 在标准中是没有定义具体字长的,但一般默认认为是 4 字节,对应 CGO 类型中 C.int 则明确定义了字长是 4 ,但 golang 中的 int 字长则是 8 ,因此对应的 golang 类型不是 int 而是 int32 。为了避免误用,C 代码最好使用 C99 标准的数值类型,对应的转换关系如下:

C语言类型CGO类型Go语言类型
int8_tC.int8_tint8
uint8_tC.uint8_tuint8
int16_tC.int16_tint16
uint16_tC.uint16_tuint16
int32_tC.int32_tint32
uint32_tC.uint32_tuint32
int64_tC.int64_tint64
uint64_tC.uint64_tuint64

C类型是使用在C代码中的,CGO类型和GO类型都是用在GO代码中的,CGO类型是在GO代码中GO类型转换成C类型然后传递给C函数使用。然后在GO代码中C函数的返回值也是CGO类型的。

package main

/*

#include <stdint.h>
static int32_t add(int32_t a, int32_t b) {
	return a + b;
}

*/
import "C"
import "fmt"

func main() {
	var a, b int32 = 1, 2
	var c int32 = int32(C.add(C.int32_t(a), C.int32_t(b)))
	fmt.Println(c)
}
字符串

c语言没有字符串类型,使用char数组代替,于是CGO提供了用于转换 Go 和 C 类型的字符串的函数,都是通过复制数据来实现。

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

注意,如果程序常驻内存的话需要使用C.free(unsafe.Pointer(s))来释放由C.CString()创建的内存。

package main

/*

#include <stdio.h>
#include <stdlib.h>

static void SayHello(const char* s) {
	puts(s);
}
*/
import "C"
import "unsafe"

func main() {
	s := C.CString("hello world.")
	C.SayHello(s)
	C.free(unsafe.Pointer(s))
}

我们也可以将SayHello函数放到当前目录下的一个C语言源文件中。因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的static修饰符,因为static函数只能在声明它的文件中可见,其他文件不能引用该函数。hello.c

#include <stdio.h>

void SayHello(const char* s) {
	puts(s);
}

然后在CGO部分先声明SayHello函数

package main

/*
#include "hello.c"
*/
import "C"

func main() {
	C.SayHello(C.CString("hello world."))
}

或者模块化

hello.h

void SayHello(const char* s);

hello.c

#include "hello.h"
#include <stdio.h>

void SayHello(const char* s) {
	puts(s);
}

也可以使用c++来实现hello.h接口

#include <iostream>

extern "C" {
	#include "hello.h"
}

void SayHello(const char* s) {
	std::cout << s;
}

或者golang实现

hello.h

void SayHello(char* s);

main.go

//export SayHello
func SayHello(s *C.char) {
	fmt.Print(C.GoString(s))
}

CGO不仅仅用于Go语言中调用C语言函数,还可以用于导出Go语言函数给C语言函数调用。我们通过CGO的//export SayHello指令将Go语言实现的函数SayHello导出为C语言函数。为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。

或者简化一下

package main

//void SayHello(char* s);
import "C"

import (
	"fmt"
)

func main() {
	C.SayHello(C.CString("Hello, World\n"))
}

//export SayHello
func SayHello(s *C.char) {
	fmt.Print(C.GoString(s))
}

在Go1.10中CGO新增加了一个_GoString_预定义的C语言类型,用来表示Go语言字符串。下面是改进后的代码:

package main

//void SayHello(_GoString_ s);
import "C"

import (
	"fmt"
)

func main() {
	C.SayHello("Hello, World")
}

//export SayHello
func SayHello(s string) {
	fmt.Print(s)
}

切片

golang 中切片用起来有点像 C 中的数组,但实际的内存模型还是有点区别的。C 中的数组就是一段连续的内存,数组的值实际上就是这段内存的首地址。golang 切片还包含cap和len。

由于底层内存模型的差异,不能直接将 golang 切片的指针传给 C 函数调用,而是需要将存储切片数据的内部缓冲区的首地址及切片长度取出传递,然后在C代码里面重新构建数组:

package main

/*
#include <stdint.h>

static void fill_255(char* buf, int32_t len) {
	int32_t i;
	for (i = 0; i < len; i++) {
		buf[i] = 255;
	}
}
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
	b := make([]byte, 5)
	fmt.Println(b) // [0 0 0 0 0]
	C.fill_255((*C.char)(unsafe.Pointer(&b[0])), C.int32_t(len(b)))
	fmt.Println(b) // [255 255 255 255 255]
}
C函数的返回值

对于有返回值的C函数,我们可以正常获取返回值。

/*
static int div(int a, int b) {
	return a/b;
}
*/
import "C"
import "fmt"

func main() {
	v := C.div(6, 3)
	fmt.Println(v) // 2
}

上面的div函数实现了一个整数除法的运算,然后通过返回值返回除法的结果。

不过如果对于除数为0的情形并没有做特殊处理。如果希望在除数为0的时候返回一个错误,其余时候返回正常的结果。因为C语言不支持返回多个结果,因此<errno.h>标准库提供了一个errno宏用于返回错误状态。我们可以近似地将errno看着一个线程安全的全局变量,可以用于记录最近一次错误的状态码。

改进后的div函数实现如下:

/*
#include <errno.h>

static int div(int a, int b) {
	if(b == 0) {
		errno = EINVAL;
		return 0;
	}
	return a/b;
}
*/
import "C"
import "fmt"

func main() {
	v0, err0 := C.div(2, 1)
	fmt.Println(v0, err0) // 2 <nil>

	v1, err1 := C.div(1, 0)
	fmt.Println(v1, err1) // 0 The device does not recognize the command.
}

CGO也针对<errno.h>标准库的errno宏做的特殊支持:在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno错误状态。

void函数的返回值

C语言函数还有一种没有返回值类型的函数,用void表示返回值类型。一般情况下,我们无法获取void类型函数的返回值,因为没有返回值可以获取。前面的例子中提到,cgo对errno做了特殊处理,可以通过第二个返回值来获取C语言的错误状态。对于void类型函数,这个特性依然有效。

//static void noreturn() {}
import "C"
import "fmt"

func main() {
	_, err := C.noreturn()
	fmt.Println(err)
}

我们也可以尝试获取第一个返回值,它对应的是C语言的void对应的Go语言类型:

//static void noreturn() {}
import "C"
import "fmt"

func main() {
	v, _ := C.noreturn()
	fmt.Printf("%#v", v) // main._Ctype_void{}
}

关于cgo的内部实现机制可以阅读 https://www.cntofu.com/book/73/ch2-cgo/ch2-05-internal.md

链接静态库和动态库 https://www.cntofu.com/book/73/ch2-cgo/ch2-09-static-shared-lib.md

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Windows 系统下,使用 Golang 调用 C 语言开源 libxslt 可以通过以下步骤完成: 1. 安装 libxslt 在 Windows 上,可以通过 Cygwin 或 MinGW 等工具链来安装 libxslt 。 2. 编写 C 语言代码 编写 C 语言代码,实现 libxslt 的功能,并将其编译为动态链接(.dll)文件。 例如,以下是一个使用 libxslt 将 XML 文件转换为 HTML 文件的 C 语言代码: ```c #include <stdio.h> #include <string.h> #include <libxslt/xslt.h> #include <libxslt/transform.h> int transform(char *xml, char *xsl, char *output) { xmlDocPtr doc, res; xsltStylesheetPtr sty; const char *params[1] = { NULL }; int ret; xmlInitParser(); LIBXML_TEST_VERSION doc = xmlReadMemory(xml, strlen(xml), "noname.xml", NULL, 0); sty = xsltParseStylesheetFile((const xmlChar *)xsl); res = xsltApplyStylesheet(sty, doc, params); xmlSaveFormatFile(output, res, 1); xsltFreeStylesheet(sty); xmlFreeDoc(doc); xmlFreeDoc(res); xmlCleanupParser(); return ret; } ``` 将上述代码编译为动态链接,可使用以下命令: ``` gcc -shared -o libxslt.dll -I/path/to/libxslt/include -L/path/to/libxslt/lib -lxslt -lz -lm xslt.c ``` 其中,`/path/to/libxslt` 为 libxslt 的安装目录。 3. 使用 CGO 调用 C 函数 在 Golang 中,使用 CGO 可以调用 C 函数。在调用 C 函数前,需要将 C 函数声明为外部函数。 例如,以下是一个使用 CGO 调用上述 C 函数的 Golang 代码: ```go package main // #cgo LDFLAGS: -L./ -lxslt // int transform(char *xml, char *xsl, char *output); import "C" import "fmt" func main() { xml := "<xml>...</xml>" xsl := "<xsl>...</xsl>" output := "output.html" ret := C.transform(C.CString(xml), C.CString(xsl), C.CString(output)) fmt.Println(ret) } ``` 在编译 Golang 代码时,需要指定 libxslt 的链接标志 `-L` 和 `lxslt`。 以上就是在 Windows 系统下,使用 Golang 调用 C 语言开源 libxslt 的步骤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值