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.