CGO con estructuras bitfield en C
- ES
- EN
Una de las cosas en las que trabajé en un proyecto previo (en golang) era sobre interactuar con una biblioteca en C la cual usaba estructuras bitfield, así que no solo era interactuar con código en C, sino también manipular bits para asegurarse de que los datos representados no se corrompieran entre las estructuras bitfield de C y las estructuras de go, en ambos sentidos.
Esta entrada es un recordatorio para mi mismo sobre cómo se hizo, en caso de que lo olvide en el futuro (lo cual realmente significa que no sé si lo olvide en 2 ó 4 meses).
La parte en C
Una estructura con campos de bits en C es definida de la siguiente manera:
// el tamaño de la estructura content es de 128 bits, de los cuales
// los primeros 64 se usan entre los campos first-fifth
struct content {
uint64_t first: 4;
uint64_t second: 2;
uint64_t third: 10;
uint64_t fourth: 16;
uint64_t fifth: 32;
uint64_t sixth;
} __attribute__((packed));
Con la finalidad de proveer un significado sobre cómo fue hecho el manejo
de la estructura entre C <-> Go, Definiré 2 operaciones simples: escribir la
estructura struct content
a un archivo y leer la estructura struct content
desde un archivo:
int save_to_file(const char *filename, struct content *c);
int read_from_file(const char *filename, struct content *c);
Hasta este punto, podríamos decir que ésta es nuestra biblioteca (llamémosla
libbitfield
)
La parte en Go
Comenzaré por definir las contrapartes en Go
de la siguiente manera:
// cada campo toma el tipo más pequeño capaz de mantener
// los valores representados en el campo de la estructura en C
type content struct {
first uint8
second uint8
third uint16
fourth uint16
fifth uint32
sixth uint64
}
func saveToFile(filename string, c *content) error {}
func readFromFile(filename string) (*content, error) {}
Hasta este punto todo se mira bien, pero aquí está el truco:
No hay manejo directo de estructuras con campos de bits de C en CGO, así que tenemos que hacer la traducción con una estructura intermedia en C que pueda ser traducida a Go.
Lo esencial
Para que esto funcione, tendremos que definir una estructura intermedia CGO
que sea lo más cercano a los tipos de datos usados en la estructura content
de GO:
// esta estructura en C traducirá los campos de bits en campos de tipo de
// tamaño completo, los cuales pueden ser manejados hacia una estructura en GO
// (y viceversa)
struct cgo_content {
uint8_t first;
uint8_t second;
uint16_t third;
uint16_t fourth;
uint32_t fifth;
uint64_t sixth;
};
Así como las funciones que realizarán las traducciones:
struct cgo_content *to_cgo_content(const struct content *c); // campos de bits -> tamaño completo
struct content *to_content(const struct cgo_content *cgo_c);// tamaño completo -> campos de bits
El flujo
Básicamente tendríamos que separar la parte en GO, la parte del adaptador en C y la parte de campos de bits (la biblioteca bitfield).
El binario en GO se comunicaría via CGO con el código adaptador en C, el cual haría la traducción entre las estructuras de tipos de tamaño completo en C y las de campos de bits (y viceversa).
┌───────────────────┐
│ Binario en GO │
│ │
│ │
│ │
│ │ GO
│ │ }
│ │ }
│ GCO │ }
│ │ }
└───┬───────────────┘ }
│ ▲ } Esta parte es el
▼ │ } pegamento que tenemos
┌─────────────┴─────┐ } que implementar para
│ │ } comunicar una parte con
│ Adaptador │ } la otra
│ en C │ }
└───┬───────────────┘ }
│ ▲ }
▼ │
┌──────────────┴────┐
│ │
│ Biblioteca │ libitfield
│ Campos de bits │
│ │
└───────────────────┘
Una vista detallada
El flujo para guardar a archivo sería el siguiente:
- Convertir el nombre del archivo (cadena en go) a una cadena en C (
C.CString
) - Convertir la estructura
content
de go a la estructuracgo_content
de C - Convertir la estructura
cgo_content
a la estructuracontent
de C (campos de bits) - Llamar a la función en C
save_to_file
Mientras el flujo para leer desde archivo sería el siguiente:
- Convertir el nombre del archivo (cadena en go) a una cadena en C (
C.CString
) - Definir un apuntador a una estructura
content
de C (campos de bits) - Llamar a la función en C
read_from_file
- Convertir la estructura
content
de C (campos de bits) a la estructuracgo_content
- Convertir la estructura
cgo_content
a la estructuracontent
de go
Ejecutando todo completo
Para poder llamar a la biblioteca desde GO, necesitaríamos ligar contra la misma
y definir la variable de entorno LD_LIBRARY_PATH
(asumiendo que la biblioteca
no se encuentra propiamente instalada).
Al compilarlo, necesitamos definir las variables CFLAGS
y LDFLAGS
, para
que se incluyan las funciones con las que CGO interactuaría. Para este ejemplo
podemos agregar las mismas como instrucciones en forma de comentarios dentro
del código en GO, de manera que CGO las interprete sin tener que definir las
variables de entorno cada vez que compilamos el código:
package main
// #cgo CFLAGS: -I${SRCDIR}/libbitfield
// #cgo LDFLAGS: -L${SRCDIR}/libbitfield -lbitfield
// #include "cgo_bitfield.h"
import "C"
// ...
// más código a continuación
En el recorte de código anterior, SRCDIR
se ha replazado de acuerdo a:
[…] la ruta absoluta al directorio que contiene el código fuente.
Más detalles en la documentación de CGO (en inglés).
Ahora podemos simplemente ejecutar go build
y verificar que el binario
resultante estaría ligado contra nuestra biblioteca libbitfield
:
# nótese que libbitfield no está instalada
$ ldd bitfield
linux-vdso.so.1 (0x00007fff9c3db000)
libbitfield.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fd59b309000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd59b4ff000)
# pero agregar LD_LIBRARY_PATH nos permite encontrarla
$ LD_LIBRARY_PATH=./libbitfield ldd bitfield
linux-vdso.so.1 (0x00007fff9c3db000)
libbitfield.so => ./libbitfield.so (0x00007fdcfa83f000)
libc.so.6 => /lib64/libc.so.6 (0x00007fd59b309000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd59b4ff000)
Ofreciendo un código suficientemente pequeño para ejercitar este ejercicio, terminamos con una función main como la siguiente
func main() {
filename := "file_from_go.bin"
c := &content{
first: 0x4,
second: 0x0,
third: 0x1BD,
fourth: 0x276E,
fifth: 0x61502074,
sixth: 0x323420202163696E,
}
fmt.Printf("Guardando %#v en el archivo %v\n", c, filename)
if err := saveToFile(filename, c); err != nil {
fmt.Printf("error al guardar al archivo: %v\n", err)
}
new_c, err := readFromFile(filename)
if err != nil {
fmt.Printf("error al leer el archivo: %v\n", err)
return
}
fmt.Printf("Valores leídos desde la estructura content de golang: %#v\n", new_c)
}
Lo cual mostraría la siguiente salida al ejecutarse:
$ LD_LIBRARY_PATH=./libbitfield ./bitfield
Guardando &main.content{first:0x4, second:0x0, third:0x1bd, fourth:0x276e, fifth:0x61502074, sixth:0x323420202163696e} en el archivo file_from_go.bin
C (save_to_file): guardando estructura al archivo file_from_go.bin
C (save_to_file): 0x4 0x0 0x1BD 0x276E 0x61502074 0x2163696E
C (read_from_file): leyendo estructura desde el archivo file_from_go.bin
C (read_from_file): 0x4 0x0 0x1BD 0x276E 0x61502074 0x2163696E
Valores leídos desde la estructura content de golang: &main.content{first:0x4, second:0x0, third:0x1bd, fourth:0x276e, fifth:0x61502074, sixth:0x323420202163696e}
La salida con los prefijos C (save_to_file/read_from_file)
son solo mensajes
de depuración desde la biblioteca bitfield en C
, para visualizar que los valores
son correctos
Finalmente (y por diversión), vamos a observar el contenido del archivo file_from_go.bin
:
$ hexdump -C file_from_go.bin
00000000 44 6f 6e 27 74 20 50 61 6e 69 63 21 20 20 34 32 |Don't Panic! 42|
00000010
Los valores raros definidos en la estructura content
no eran tan aleatorios después de todo ;-)
Y eso es todo. Para una vista detallada y un código navegable, vea el siguiente repositorio