go 语言高级特性--CGO的使用

982 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情

介绍

cgo 是 go 语言里面的一个特性,属于 go 的高级用法,我们可以使用 cgo 来实现 go 语言调用 c 语言程序。

  1. 简单示例:
package main

/*
#include <stdio.h>

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

func main() {
    hi := C.CString("Hello World!")
    C.Hello(hi)
    defer C.free(unsafe.Pointer(hi)) // 释放分配在C中的内存,否则会造成内存泄露
}
  • import "C" 语句启用 CGO 特性
  • 引入 C 语言的 <stdio.h> 头文件
  • CGO 包的 C.CString 函数将 Go 语言字符串转为 C 语言字符串
  • C.CString 创建的 C 语言字符串如果没有被正确释放,会导致内存泄漏,另外程序退出后操作系统也会自动回收程序的所有资源。
  1. 工具链

要使用 CGO 特性,需要安装 C/C++ 构建工具链,在 macOS 和 Linux 下是要安装 GCC,在 windows 下是需要安装 MinGW 工具。同时需要保证环境变量 CGO_ENABLED 被设置为 1,这表示 CGO 是被启用的状态。在本地构建时 CGO_ENABLED 默认是启用的,当交叉构建时 CGO 默认是禁止的。比如要交叉构建 ARM 环境运行的 Go 程序,需要手工设置好 C/C++ 交叉构建的工具链,同时开启 CGO_ENABLED 环境变量。然后通过 import "C" 语句启用 CGO 特性。

动态库路径

  1. 编译目标代码时指定的动态库搜索路径(指的是用-wl,rpath或-R选项而不是-L);

example: gcc -Wl,-rpath,/home/arc/test,-rpath,/lib/,-rpath,/usr/lib/,-rpath,/usr/local/lib test.c

  1. 环境变量 LD_LIBRARY_PATH ****指定的动态库搜索路径;(例如:export LD_LIBRARY_PATH=/root/test/env/lib
  2. 配置文件/etc/ld.so.conf中指定的动态库搜索路径;(更改/etc/ld.so.conf文件后记得一定要执行命令:ldconfig,该命令会将/etc/ld.so.conf文件中所有路径下的库载入内存中)
  3. 默认的动态库搜索路径/lib;
  4. 默认的动态库搜索路径/usr/lib。

类型转换

// Go 字符串转换为 C 字符串
// C 字符串使用 malloc 来分配到 C 堆里。
// 调用方需要把它释放掉,比如调用 C.free (如果需要 C.free,确保程序包含 stdlib.h)
func C.CString(string) *C.char

// 这里同样需要使用C.free释放内存
func C.CBytes([]byte) unsafe.Pointer
 
// C 字符串转换为 Go 字符串
func C.GoString(*C.char) string
 
// 有明确长度的 C 数据转换为 Go 字符串
func C.GoStringN(*C.char, C.int) string
 
// 有明确长度的 C 数据转换为 Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

官方提供的函数计算字符串的长度获取字符串的地址

size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);

参数类型是_GoString_ s,第一个方法返回Go字符串的长度,第二个方法返回指向这个字符串的char*指针,下面为示例代码 :

package main
/*
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 这两个函数要声明在序言中
size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);
void PrintGoStringInfo(char* s, size_t count)
{
	// 注意,s尾部没有"\0", 在C中直接当字符串处理会出错
	char* buf = malloc((count + 1) * sizeof(char)); //
	memset(buf, 0, count + 1);
	memcpy(buf, s, count);
	printf("%s\n", buf);
	printf("sizeof goString: %ld\n", count);
	free(buf);
}
*/
import "C"

func main() {
	str := "hello world"
	C.PrintGoStringInfo(C._GoStringPtr(str), C._GoStringLen(str))
}
  • 需要注意的是 _GoStringPtr 返回的 char* 尾部是不包含的\0 的,在 C 中直接当字符串处理会出错
  • 这两个函数仅可在序言中使用,不能在其他的C文件中使用,C代码绝不能通过 _GoStringPtr 返回的指针修改其指向的内容

注意:这两个函数可以很方便的将Go string转换为C的char*,如果使用 _GoStringPtr 传入一个临时的 string 到C中,在C中应拷贝一份副本到C内存中,尤其是在一些异步调用的过程中,从官方关于cgo的文档看来,这两个函数似乎并不保证传入的临时Go string类型不会被gc回收

GO 语言中访问 C 语言的 struct ,union,enum,对应关系:

示例

  1. go 字符串类型 与 c++ char* 互转使用
#ifndef SM4_EX_H
#define SM4_EX_H

typedef unsigned char u_char;

extern int g_loadCout;

//
#ifdef __cplusplus
extern "C" {
#endif
//

/*
 * 对数据进行 sm4 加密
 * _out     解密的数据
 * _outLen  输入时,为_out缓冲区的长度。输出时,为_out中返回的数据的长度
 * _plaint      明文,被加密的数据
 * _plaintLen   _plaint数据的长度
 * _key 加密密钥,长度固定为 32 byte
 */
void encryptData(char *_out, int *_outLen, const char *_plaint, int _plaintLen, char *_key, int _keyLen);

//encryptData() 函数的,解密操作
void decryptData(char *_out, int *_outLen, const char *_cipher, int _cipherLen, char *_key, int _keyLen);

//
#ifdef __cplusplus
}
#endif
//

#endif // SM4_EX_H
package main

/*
#cgo CFLAGS: -I./
#cgo LDFLAGS: -L./ -lsm4 -lstdc++ -lcurl //-lsm4 引入对应C程序的动态库文件
#include "sm4.h" //引入对应C程序的头文件
#include <stdlib.h>
#include <stdio.h>
*/
import "C"
import (
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"unsafe"
)

func main() {
	txt := "123"
	in := C.CString(txt)
	defer C.free(unsafe.Pointer(in)) // 释放分配在C中的内存,否则会造成内存泄露
	inlen := C.int(len(txt))

	keytxt := "123456789"
	b, _ := hex.DecodeString(keytxt)
	keylen := C.int(len(keytxt))
	key := (*C.char)(unsafe.Pointer(&b[0]))

	len1 := len(txt) + 100
	outlen := C.int(len1)
	outtxt := make([]byte, len1)
	out := ((*C.char)(unsafe.Pointer(&outtxt[0])))

	C.encryptData(out, &outlen, in, inlen, key, keylen/2)
	fmt.Printf("明文:<%s>长度<%d>;加密后密文:<%v>长度<%d>\n", txt, int(inlen), C.GoStringN(out, outlen), int(outlen))

	encoded := base64.StdEncoding.EncodeToString([]byte(C.GoStringN(out, outlen)))
	fmt.Printf("加密后密文base64:<%s>\n", encoded)

	d := hex.EncodeToString([]byte(C.GoStringN(out, outlen)))
	fmt.Printf("加密后密文转16进制<%s>\n:", d)

	//----------------解密-----------------------------
	fmt.Println("------------------")

	entxt, _ := base64.StdEncoding.DecodeString(encoded) //密文base64解码
	entxt2 := C.CString(string(entxt))
	defer C.free(unsafe.Pointer(entxt2)) // 释放分配在C中的内存,否则会造成内存泄露
	entxtlen := C.int(len(string(entxt)))

	len2 := len(string(entxt)) + 100
	out2len := C.int(len2)
	outtxt2 := make([]byte, len2)
	out2 := ((*C.char)(unsafe.Pointer(&outtxt2[0])))

	C.decryptData(out2, &out2len, entxt2, entxtlen, key, keylen/2)

	fmt.Println("输入密文长度:", int(entxtlen), entxtlen)
	fmt.Printf("解密后明文:<%s>,长度<%d>\n\n", C.GoStringN(out2, out2len), int(out2len))
}
  1. go 字符串与 c++ unsigned char 互转
void sm4_ecb(const unsigned char *in,
             unsigned char *out,
             const unsigned char *keyValue,
             int length,
             int enc);
package main

/*
#cgo CFLAGS: -I./
#cgo LDFLAGS: -L./ -lsm4
#include "sm4.h"
*/
import "C"

import (
	"fmt"
	"unsafe"
)

func main() {
	in := []byte("hello world!")
	key := []byte("1234567890abcdef")
	var out []byte

	C.sm4_ecb((*C.uchar)(unsafe.Pointer(&in[0])),
		(*C.uchar)(unsafe.Pointer(&out)),
		(*C.uchar)(unsafe.Pointer(&key[0])),
		(C.int)(len(in)),
		(C.int)(0),
	)
	fmt.Println("out:", out)
}
  1. 释放 C 中的内存
package print
// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

func Print(s string) {
	cs := C.CString(s)
	defer C.free(unsafe.Pointer(cs))  // 释放分配在C中的内存,否则会造成内存泄露
	C.fputs(cs, (*C.FILE)(C.stdout))
}