概述

最近在巩固cgo的基础知识,在网上看到一篇Go和C之间 字符串数组、切片类型转换的文章,让我想到我之前写的一篇在go中遍历C结构体数组的文章,让我有新的方法来解决之前的问题,把C的数组转化为Go的切片,对于文章的方法我直接”拿来主义“。

数组、字符串和切片

我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。
在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。
Go语言和C语言的数组、字符串和切片之间的相互转换可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。

以克隆的方式进行类型转换

CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

func C.CString(string) *C.char              //go字符串转化为char*
func C.CBytes([]byte) unsafe.Pointer        // go 切片转化为指针
func C.GoString(*C.char) string             //C字符串 转化为 go字符串
func C.GoStringN(*C.char, C.int) string     
func C.GoBytes(unsafe.Pointer, C.int) []byte

其中C.CString针对输入的Go字符串,克隆一个C语言格式的字符串;返回的字符串由C语言的malloc函数分配,不使用时需要通过C语言的free函数释放。C.CBytes函数的功能和C.CString类似,用于从输入的Go语言字节切片克隆一个C语言版本的字节数组,同样返回的数组需要在合适的时候释放。C.GoString用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。C.GoStringN是另一个字符数组克隆函数。C.GoBytes用于从C语言数组,克隆一个Go语言字节切片。

克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。

上面粗体部分表示,利用C.CString把go字符串转化为C字符串,内存由C语言的malloc分配,不使用时需要free释放内存,否则会出现内存泄漏。

通过直接访问C语言的内存来进行数据转换

reflect包中有字符串和切片的定义:在reflect包中有字符串和切片的定义:

type StringHeader struct {<!-- -->
    Data uintptr
    Len  int
}

type SliceHeader struct {<!-- -->
    Data uintptr
    Len  int
    Cap  int
}

如果不希望单独分配内存,可以在Go语言中直接访问C语言的内存空间:

/*
#include <string.h>
char arr[10];
char *s = "Hello";
*/
import "C"
import (
    "reflect"
    "unsafe"
)
func main() {<!-- -->
    // 通过 reflect.SliceHeader 转换
    var arr0 []byte
    var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
    arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0]))
    arr0Hdr.Len = 10
    arr0Hdr.Cap = 10

    // 通过切片语法转换
    arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10]

    var s0 string
    var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0))
    s0Hdr.Data = uintptr(unsafe.Pointer(C.s))
    s0Hdr.Len = int(C.strlen(C.s))

    sLen := int(C.strlen(C.s))
        s1 := string((*[31]byte)(unsafe.Pointer(C.s))[:sLen:sLen])
}

因为Go语言的字符串是只读的,用户需要自己保证Go字符串在使用期间,底层对应的C字符串内容不会发生变化、内存不会被提前释放掉。

在CGO中,会为字符串和切片生成和上面结构对应的C语言版本的结构体:

typedef struct {<!-- --> const char *p; GoInt n; } GoString;
typedef struct {<!-- --> void *data; GoInt len; GoInt cap; } GoSlice;

在Go语言中直接访问C语言的内存空间的例子

package main

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

typedef struct {
   char *name;
   int age;
}person;
//一个长度为10的person结构体数组
person pon[10];
void NewPersonArray()
{
   int n = 10;
   //初始化name
   for(int i = 0; i<n;i++){
     pon[i].name = (char*)malloc(sizeof(char)*10);
     pon[i].age = i;
      sprintf(pon[i].name, "name:%d", i);
   }
}

//释放内存
void freePersonArray()
{
   for (int i = 0; i < 10; i ++){
      free(pon[i].name);
   }
}
*/
import "C"
import (
   "fmt"
   "unsafe"
)

func main()  {<!-- -->
   C.NewPersonArray()
   //通过切片语法转换
   arr1 := (*[20]C.person)(unsafe.Pointer(&C.pon[0]))[:10:10]
   for _, v := range arr1 {<!-- -->
      fmt.Printf("p.name: %s, p.age: %d\n",  C.GoString(v.name), int(v.age) )
   }
   C.freePersonArray()
}

通过切片语法转换把C的结构体数组转换为go的数据

 arr1 := (*[20]C.person)(unsafe.Pointer(&C.pon[0]))[:10:10]

注意,如果C.pon是一个数组指针该方法就不适用,在go中无法使用索引C.pon[0]的 方法来访问C数组指针中的数据。比如有一个长度为10的数组指针 *C.pon,则在go中无法通过C.pon[0] 索引的方式来访问数据。如果有一个长度为10的数组[10]C.pon, 则可以使用C.pon[0] 索引的方式来访问数据,不过在go中还需要转化为切片才能访问

输出:

p.name: name:0, p.age: 0
p.name: name:1, p.age: 1
p.name: name:2, p.age: 2
p.name: name:3, p.age: 3
p.name: name:4, p.age: 4
p.name: name:5, p.age: 5
p.name: name:6, p.age: 6
p.name: name:7, p.age: 7
p.name: name:8, p.age: 8
p.name: name:9, p.age: 9

给一个小彩蛋

在go中一个长度为10的char, 通过访问数组首位元素的地址来输出整个数组

package main

/*
#include <stdio.h>
void NewChar(char *s, int n)
{
	sprintf(s, "I'm char");
}
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main()  {<!-- -->
	var c [10]C.char
	C.NewChar(&c[0], C.int(10))
	//通过数组首元素地址输出整个数组
	arr := C.GoString(&c[0])
	fmt.Printf("111111: %s\n",arr)
}

参考文章:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-03-cgo-types.html

--完--