Saltar al contenido principal

CGO con estructuras bitfield en C

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 estructura cgo_content de C
  • Convertir la estructura cgo_content a la estructura content 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 estructura cgo_content
  • Convertir la estructura cgo_content a la estructura content 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