Desarrollo integrado con C y GoLang (CGO)

Hola, no soy un desarrollador embebido. Mi experiencia en codificación se basa en C#, Java, Go Desktop y desarrollo de servidores. Recientemente en Lobaro me enfrenté a algunos retos en la programación de C embebido y me di cuenta - una vez más - de la enorme diferencia entre la tierra santa de la gestión de memoria y el laberinto de punteros de C donde tienes que pensar dos veces cada llamada malloc. El código C suele ser poco genérico y, por tanto, menos reutilizable. No existen los módulos, sino un montón de cosas globales que tienden a romper otras cosas globales. La gente rara vez prueba el código C, y si lo hacen, lo hacen en el hardware para el que está escrito, por lo que es casi imposible utilizar cualquier infraestructura de CI.

Y luego está Go. Go es como C pero con soluciones a la mayoría de los problemas que vienen con C. En primer lugar un recolector de basura y gestión inteligente de punteros. Eso está bien, pero también es la razón por la que nunca ejecutarías un programa Go en tu microcontrolador ARM, a pesar de que Go compila a código nativo ARM. Para mencionar algunas otras áreas problemáticas que se resolvieron muy bien: Gran reutilización con el apoyo de un buen sistema de módulos, la concurrencia simplemente funciona, muy pocos problemas en tiempo de ejecución debido a un estricto sistema de tipos estáticos (ni siquiera genéricos), y para todos los amigos de C tiene estructuras que se pueden vincular a las funciones. Y luego están las dos características más importantes para este artículo: (1) Las pruebas están integradas. Es tan simple como poner un Prueba* en una función *_test.go y ejecute ir a la prueba. (2) Puedes llamar a código C desde Go y viceversa usando CGO.

Veamos cómo funciona. Un buen punto de partida es el Documentación de la CGOEl Ir a Wiki y esto Blog Artículo. Me gusta centrarme en el tema con el que tuve problemas incluso después de leer las páginas enlazadas.

En los siguientes apartados quiero profundizar en el tema:

  • Llamada a código C desde GO
  • Llamada al código Go desde C
  • Compilación de código C a partir de archivos
  • Manejo de punteros de función
  • Conversión de tipos, acceso a structs y aritmética de punteros

Llamada a código C desde Go

paquete main
/*
// Todo lo que está en los comentarios por encima del import "C" es código C y será compilado con el GCC.
// Asegúrate de tener instalado GCC.
int addInC(int a, int b) {
    return a + b;
}
 */
import "C
import "fmt"
func main() {
       a := 3
       b := 5
       
       c := C.addInC(C.int(a), C.int(b))
       fmt.Println("Añadir en C:", a, "+", b, "=", int(c))
}

De esto ya podemos deducir muchas cosas:

  • importar "C proporciona un paquete especial que le dirá a la herramienta Go que compile con CGO. Los comentarios directamente por encima de esta importación se compilan a C utilizando el GCC.
  • Podemos simplemente definir una función C como addInC y llamarla utilizando el paquete C.
  • Se puede acceder a los tipos C como int pero también a los structs que definas a través del paquete C. De esta manera podemos convertir nuestro Go int a C.int y viceversa.
    • La conversión dentro de Println no es necesaria pero hace el ejemplo más claro.

Llamada al código Go desde C

paquete main
/*
static inline int multiplyInGo(int a, int b) {
    return go_multiply(a, b);
}
 */
importar "C
import (
       "fmt
)
func main() {
       a := 3
       b := 5
       
       c := C.multiplyInGo(C.int(a), C.int(b))
       fmt.Println("multiplyInGo:", a, "*", b, "=", int(c))
}
//exportar go_multiplicar
func go_multiply(a C.int, b C.int) C.int {
       return a * b
}

Este código funciona, pero necesita un poco de atención especial. En primer lugar está el código especial //exportar sobre la función Go. Esto le dice a CGO que exporte la función a un archivo C separado. Pero eso lleva a la siguiente restricción: "si su programa utiliza cualquier //exportar el código C del comentario sólo puede incluir declaraciones (int externo f();), no definiciones (int f() { return 1; }). Puede utilizar //exportar para que las funciones Go sean accesibles al código C". Véase también: Documentación de la CGO

Esta es la razón por la que definí multiplyInGo como estático en línea. Esto evita el problema de los símbolos duplicados.

Compilación de código C a partir de archivos

La buena noticia es que todos los archivos C en su módulo son sólo compilado y utilizable a partir de código Go, también puede utilizar #include "mi_fichero_c.h" para obtener las declaraciones. Las malas noticias son: No funciona en tu paquete principal y tu código C no debe tener dependencias con código C fuera de la carpeta de módulos Go. Incluir librerías disponibles globalmente como #include funciona bien.

Existe una posible solución: incluir archivos C de otra carpeta. El contenido del archivo C se incluirá en la carpeta 1TP11Incluye y así se compila. Pero tenga cuidado con los efectos secundarios no deseados y asegúrese de que el mismo código C no se incluye dos veces.

Manejo de punteros de función

paquete main
/*
int ir_multiplicar(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiplicar_f multiplicar;
static inline init() {
    multiplicar = go_multiplicar;
}
static inline int multiplicarConFp(int a, int b) {
    return multiplicar(a, b);
}
 */
importar "C
import (
       "fmt
)
func main() {
       a := 3
       b := 5
       C.init();
       c := C.multiplicarConFp(C.int(a), C.int(b))
       fmt.Println("multiplyInGo:", a, "+", b, "=", int(c))
}
//exportar go_multiplicar
func go_multiply(a C.int, b C.int) C.int {
       return a * b
}

El código anterior tiene un init para configurar el puntero de la función multiplicar a la go_multiply implementaciones. Para ello es necesario declarar go_multiply ya que CGO compila la declaración en un fichero de cabecera separado (_cgo_export.h) que no podemos incluir antes de la generación. El typedef no es estrictamente necesario pero hace el ejemplo más claro. En multiplicarConFp simplemente llamamos a la función almacenada en el puntero de función.

La asignación del puntero a la función debe realizarse en C. El siguiente código no funcionará como reemplazo de la función C.init() llamar:

C.multiply = C.multiply_f(go_multiply);

Falla con "cannot convert go_multiply (type func(C.int, C.int) C.int) to type C.multiply_f".

Conversión de tipos, acceso a structs y aritmética de punteros

Ya hicimos alguna conversión de tipos entre int y C.int. Lo mismo funciona para todos los demás tipos. Si tienes un struct mi_estructura_t en C se puede utilizar con C.mi_estructura_t en Go. Puedes acceder a todos los campos de los structs de C y el compilador CGO incluso comprobará los tipos y detectará cuando accedes a campos no existentes.

Hay algunos tipos que son más difíciles de manejar. Pero CGO viene con algunas funciones de ayuda:

// De cadena Go a cadena C
// La cadena C se asigna en el heap de C usando malloc.
// Es responsabilidad de la persona que llama a la cadena liberarla
// liberar, por ejemplo llamando a C.free (asegúrese de incluir stdlib.h
// si se necesita C.free).
func C.CString(string) *C.char
// Pasa []byte slice a array C
// El array C se asigna en el heap de C usando malloc.
// Es responsabilidad de la persona que llama al array el liberarlo
// liberar, por ejemplo llamando a C.free (asegúrese de incluir stdlib.h
// si se necesita C.free).
func C.CBytes([]byte) unsafe.Pointer
// Cadena C a cadena Go
func C.GoString(*C.char) cadena
// Datos C con longitud explícita a cadena Go
func C.GoStringN(*C.char, C.int) cadena
// Datos C con longitud explícita a Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

La mayoría de ellos son bastante claros. Sólo me confundí un poco con el unsafe.pointer en C.GoBytes. He aquí un pequeño ejemplo:

func go_handleData(datos *C.uint8_t, longitud C.uint8_t) []byte {
       return C.GoBytes(unsafe.Pointer(data), C.int(length))
}

Como puedes ver, podemos convertir nuestro puntero C a unsafe.pointer. Cuando tienes un puntero inseguro, por ejemplo de C.CBytes y quieres convertirlo en un puntero C puedes hacerlo así:

goByteSlice := []byte {1, 2, 3}
goUnsafePointer := C.CBytes(goByteSlice)
cPointer := (*C.uint8_t)(goUnsafePointer)

Cualquier puntero void de C está representado por Go unsafe.Pointer.

Tratar con aritmética de punteros es muy peligroso pero también posible:

func getPayload(paquete *C.paquete_t) []byte {
       dataPtr := unsafe.Pointer(paquete.datos)
       // Supongamos una cabecera de 2 bytes antes del payload.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       return payload
}

Conclusión

Ya está. Hemos visto cómo llamar código C desde Go y viceversa. Es fácil compilar unos pocos archivos C junto con tu código Go pero se vuelve complicado cuando tienes bases de código C más grandes. El uso de punteros de función en C es una gran manera de reemplazar implementaciones nativas de C con implementaciones de Go. De esta forma puedes tener funciones como GetTimestamp en C que se implementan con funciones Go en las que se puede falsear el tiempo durante las pruebas.

Para los structs C necesitarás escribir algún código de conversión para convertir los structs C a structs Go, tan pronto como quieras usarlos en otras partes de tu programa Go. Recomiendo separar estrictamente el código Go que envuelve funciones C y sólo pasar tipos Go entre funciones Go públicas.

Todos los tipos excepto los punteros de función pueden ser convertidos entre C y Go e incluso los punteros de función pueden ser almacenados como punteros inseguros y pasados de ida y vuelta entre ambos mundos.

Aún quedan algunos temas como #cgo que se documentan en los recursos enlazados anteriormente pero que no se tratan en este artículo.