Sviluppo embedded con C e GoLang (CGO)
Salve, non sono uno sviluppatore embedded. La mia esperienza di programmazione si basa su C#, Java, Go Desktop e sviluppo di server. Recentemente all'Lobaro ho affrontato alcune sfide nella programmazione C embedded e ho notato - ancora una volta - l'enorme differenza tra la terra santa della gestione della memoria e il labirinto dei puntatori del C, dove bisogna pensare due volte a ogni chiamata malloc. Il codice C è spesso poco generico e quindi meno riutilizzabile. Non esistono moduli, ma un mucchio di cose globali che tendono a rompere altre cose globali. Le persone raramente testano il codice C e se lo fanno, lo fanno sull'hardware per cui è stato scritto, rendendo quasi impossibile l'uso di qualsiasi infrastruttura di CI.
E poi c'è Go. Go è come il C, ma con soluzioni alla maggior parte dei problemi che si presentano con il C. Prima di tutto un garbage collector e una gestione intelligente dei puntatori. Questo è bello, ma è anche il motivo per cui non eseguirete mai un programma Go sul vostro microcontrollore ARM, anche se Go si compila in codice ARM nativo. Per citare altre aree problematiche che sono state ben risolte: grande riutilizzabilità supportata da un bel sistema di moduli, la concorrenza funziona, pochissimi problemi di run time grazie a un rigoroso sistema di tipi statici (nemmeno generici) e per tutti gli amici del C ci sono le strutture che possono essere legate alle funzioni. E poi ci sono le due caratteristiche più importanti per questo articolo: (1) I test sono integrati. È semplice come inserire un elemento Test*
in una funzione *_test.go
ed eseguire vai al test
. (2) È possibile richiamare codice C da Go e viceversa utilizzando CGO.
Vediamo come funziona. Un buon punto di partenza è il file Documentazione CGO, il Vai a Wiki e questo Articolo del blog. Mi piace concentrarmi sull'argomento per il quale ho avuto difficoltà anche dopo aver letto le pagine collegate.
Nelle sezioni che seguono, vorrei dare un'occhiata più da vicino:
- Chiamare il codice C da GO
- Chiamare il codice Go dal C
- Compilazione di codice C da file
- Gestire i puntatori di funzione
- Conversione dei tipi, accesso alle strutture e aritmetica dei puntatori
Chiamare codice C da Go
pacchetto main /* // Tutto ciò che è contenuto nei commenti sopra l'importazione "C" è codice C e sarà compilato con GCC. // Assicuratevi di avere installato GCC. int addInC(int a, int b) { restituisce a + b; } */ importare "C importare "fmt" func main() { a := 3 b := 5 c := C.addInC(C.int(a), C.int(b)) fmt.Println("Aggiungi in C:", a, "+", b, "=", int(c)) }
Da questo possiamo già capire molto:
importare "C
fornisce un pacchetto speciale che indica allo strumento Go di compilare con CGO. I commenti direttamente al di sopra di questa importazione sono compilati in C utilizzando il GCC.- Si può semplicemente definire una funzione C come addInC e chiamarla utilizzando il pacchetto C.
- I tipi C, come int, ma anche le strutture definite dall'utente, sono accessibili tramite il pacchetto C. In questo modo possiamo semplicemente convertire il nostro Go int in C.int e viceversa.
- La conversione all'interno di Println non è necessaria, ma rende l'esempio più chiaro.
Chiamare il codice Go dal C
pacchetto main /* statico inline int moltiplicaInGo(int a, int b) { restituisce go_multiply(a, b); } */ importare "C importare ( "fmt" ) func main() { a := 3 b := 5 c := C.moltiplicaInGo(C.int(a), C.int(b)) fmt.Println("moltiplicaInGo:", a, "*", b, "=", int(c)) } //esportazione go_multiply func go_multiply(a C.int, b C.int) C.int { restituisce a * b }
Questo codice funziona, ma richiede un'attenzione particolare. Innanzitutto c'è lo speciale //esportazione
sopra la funzione Go. Ciò indica a CGO di esportare la funzione in un file C separato. Ma questo porta alla seguente restrizione: "se il vostro programma utilizza un qualsiasi file //esportazione
allora il codice C nel commento può includere solo dichiarazioni (int esterno f();
), non le definizioni (int f() { restituisce 1; }
). È possibile utilizzare //esportazione
per rendere le funzioni di Go accessibili al codice C". Vedi anche: Documentazione CGO
Questo è il motivo per cui ho definito moltiplicareInGo
come statico in linea
. In questo modo si evita il problema dei simboli duplicati.
Compilazione di codice C da file
La buona notizia è che tutti i file C del modulo sono semplicemente compilati e utilizzabili dal codice Go, si può anche usare #include "my_c_file.h"
per ottenere le dichiarazioni. Le cattive notizie sono: Non funziona nel pacchetto principale e il codice C non deve avere dipendenze da codice C al di fuori della cartella del modulo Go. L'inclusione di librerie disponibili a livello globale, come #include, funziona bene.
Esiste una soluzione possibile, che consiste nell'includere i file C da un'altra cartella. Il contenuto del file C verrà inserito nel file 1TP11Comprende
e quindi compilato. Ma fate attenzione agli effetti collaterali indesiderati e assicuratevi che lo stesso codice C non venga incluso due volte.
Gestire i puntatori di funzione
pacchetto main /* int go_multiply(int a, int b); typedef int (*multiply_f)(int a, int b); moltiplica_f; statico inline init() { moltiplica = go_multiply; } static inline int multiplyWithFp(int a, int b) { restituisce multiply(a, b); } */ importare "C importare ( "fmt" ) func main() { a := 3 b := 5 C.init(); c := C.multiplyWithFp(C.int(a), C.int(b)) fmt.Println("moltiplicaInGo:", a, "+", b, "=", int(c)) } //esportazione go_multiply func go_multiply(a C.int, b C.int) C.int { restituisce a * b }
Il codice qui sopra ha un elemento init
per impostare il puntatore alla funzione moltiplicata per la funzione go_multiply
implementazioni. Ciò richiede di dichiarare go_multiply
prima, poiché CGO compila la dichiarazione in un file di intestazione separato (_cgo_export.h
) che non possiamo includere prima della generazione. Il typedef non è strettamente necessario, ma rende l'esempio più chiaro. In moltiplicareConFp
chiamiamo semplicemente la funzione memorizzata nel puntatore a funzione.
L'assegnazione del puntatore alla funzione deve avvenire in C. Il codice seguente non funzionerà come sostituzione del codice C.init()
chiamata:
C.multiply = C.multiply_f(go_multiply);
Fallisce con "Impossibile convertire go_multiply (tipo func(C.int, C.int) C.int) nel tipo C.multiply_f".
Conversione dei tipi, accesso alle strutture e aritmetica dei puntatori
Abbiamo già effettuato una conversione di tipo tra int e C.int. Lo stesso vale per tutti gli altri tipi. Se si ha una struct my_struct_t
in C si può usare semplicemente con C.my_struct_t
in Go. È possibile accedere a tutti i campi delle strutture C e il compilatore CGO controllerà persino i tipi e rileverà quando si accede a campi non esistenti.
Ci sono alcuni tipi che sono più difficili da gestire. Ma CGO è dotato di alcune funzioni di aiuto:
// Da stringa Go a stringa C // La stringa C viene allocata nell'heap C utilizzando malloc. // È responsabilità del chiamante provvedere a liberarla, ad esempio chiamando C.free (assicurarsi di includere stdlib.h). // liberarla, ad esempio chiamando C.free (assicurarsi di includere stdlib.h // se C.free è necessario). func C.CString(stringa) *C.char // Passa la fetta di []byte all'array C // L'array C viene allocato nell'heap C utilizzando malloc. // È responsabilità del chiamante provvedere a liberarlo. // libero, ad esempio chiamando C.free (assicurarsi di includere stdlib.h // se C.free è necessario). func C.CBytes([]byte) unsafe.Pointer // Da stringa C a stringa Go func C.GoString(*C.char) stringa // Dati C con lunghezza esplicita in stringa Go func C.GoStringN(*C.char, C.int) stringa // Dati C con lunghezza esplicita in Go []byte func C.GoBytes(unsafe.Pointer, C.int) []byte
La maggior parte di essi è abbastanza chiara. Sono solo un po' confuso con il puntatore unsafe.pointer in C.GoBytes. Ecco un piccolo esempio:
func go_handleData(data *C.uint8_t, length C.uint8_t) []byte { return C.GoBytes(unsafe.Pointer(data), C.int(length)) }
Come si può vedere, possiamo semplicemente convertire il nostro puntatore C in unsafe.pointer. Quando si ottiene un puntatore non sicuro, ad esempio da C.CBytes, e lo si vuole convertire in un puntatore C, si può procedere in questo modo:
goByteSlice := []byte {1, 2, 3} goUnsafePointer := C.CBytes(goByteSlice) cPointer := (*C.uint8_t)(goUnsafePointer)
Qualsiasi puntatore nullo C è rappresentato da Go unsafe.Pointer.
Trattare l'aritmetica dei puntatori è molto pericoloso, ma anche possibile:
func getPayload(packet *C.packet_t) []byte { dataPtr := unsafe.Pointer(packet.data) // Assumiamo un'intestazione di 2 byte prima del payload. payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2)) restituire payload }
Conclusione
Tutto qui. Abbiamo visto come chiamare il codice C da Go e viceversa. È facile compilare alcuni file C insieme al codice Go, ma diventa complicato quando si hanno basi di codice C più grandi. L'uso dei puntatori di funzione in C è un ottimo modo per sostituire le implementazioni native in C con quelle in Go. In questo modo si possono avere funzioni come GetTimestamp
in C che sono implementati con funzioni Go in cui è possibile falsificare il tempo durante i test.
Per le strutture C è necessario scrivere del codice di conversione per convertire le strutture C in strutture Go, non appena si desidera utilizzarle in altre parti del programma Go. Si consiglia di separare rigorosamente il codice Go che avvolge le funzioni C e di passare solo i tipi Go tra le funzioni Go pubbliche.
Tutti i tipi, tranne i puntatori a funzione, possono essere convertiti tra C e Go e anche i puntatori a funzione possono essere memorizzati come puntatori non sicuri e passati avanti e indietro tra i due mondi.
Ci sono ancora alcuni argomenti come #cgo
che sono documentati nelle risorse collegate sopra, ma non sono trattati in questo articolo.