Embedded ontwikkeling met C en GoLang (CGO)

Hallo, ik ben geen embedded ontwikkelaar. Mijn codeerervaring is gebaseerd op C#, Java, Go Desktop en Server ontwikkeling. Onlangs stond ik bij Lobaro voor wat uitdagingen in Embedded C programmering en merkte - opnieuw - het enorme verschil tussen het heilige land van geheugenbeheer en het C pointer doolhof waar je twee keer moet nadenken over elke malloc call. C-code is vaak niet erg generiek en dus minder herbruikbaar. Er is niet zoiets als modules, maar een hoop globale dingen die de neiging hebben andere globale dingen te breken. Mensen testen zelden C-code, en als ze dat doen, doen ze dat op de hardware waarvoor het geschreven is, waardoor het bijna onmogelijk is om enige CI-infrastructuur te gebruiken.

En dan is er Go. Go is als C, maar met oplossingen voor de meeste problemen die bij C horen. Allereerst een garbage collector en slim pointerbeheer. Dat is mooi, maar ook de reden waarom je nooit een Go-programma op je ARM-microcontroller zou draaien, ook al compileert Go naar native ARM-code. Om enkele andere probleemgebieden te noemen die mooi zijn opgelost: Grote herbruikbaarheid ondersteund door een mooi modulesysteem, Concurrency werkt gewoon, zeer weinig run time problemen door een strikt statisch type systeem (zelfs geen generics), en voor alle vrienden van C heeft het Structs die aan functies gebonden kunnen worden. En dan de twee belangrijkste kenmerken voor dit artikel: (1) Testen is ingebouwd. Het is zo simpel als een Test* functie in een *_test.go bestand en voer ga testen. (2) U kunt C-code aanroepen vanuit Go en omgekeerd met behulp van CGO.

Laten we eens kijken hoe dit werkt. Een goed uitgangspunt is de CGO documentatiede Ga Wiki en dit Blog Artikel. Ik wil me concentreren op het onderwerp waar ik moeite mee had, zelfs na het lezen van de gelinkte pagina's.

In de volgende secties wil ik nader ingaan op:

  • C-code aanroepen vanuit GO
  • Go-code aanroepen vanuit C
  • Compileren van C-code uit bestanden
  • Omgaan met functiepunten
  • Omzetten van types, toegang tot structs en pointeraritmetiek

C-code aanroepen vanuit Go

pakket main
/*
// Alles in commentaar boven de import "C" is C-code en wordt gecompileerd met de GCC.
// Zorg ervoor dat je GCC geïnstalleerd hebt.
int addInC(int a, int b) {
    return a + b;
}
 */
importeer "C
import "fmt"
func main() {
       a := 3
       b := 5
       
       c := C.addInC(C.int(a), C.int(b))
       fmt.Println("Toevoegen in C:", a, "+", b, "=", int(c))
}

Hieruit kunnen we al veel opmaken:

  • import "C levert een speciaal pakket dat de Go-tool vertelt om te compileren met CGO. Commentaar direct boven deze import wordt gecompileerd naar C met de GCC.
  • We kunnen gewoon een C-functie definiëren zoals addInC en die aanroepen met behulp van het C-pakket.
  • C types zoals int maar ook structs die je definieert zijn toegankelijk via het C pakket. Zo kunnen we onze Go int gewoon converteren naar C.int en terug.
    • De conversie binnen Println is niet nodig, maar maakt het voorbeeld duidelijker.

Go-code aanroepen vanuit C

pakket main
/*
static inline int multiplyInGo(int a, int b) {
    return go_multiply(a, b);
}
 */
import "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))
}
//export go_multiply
func go_multiply(a C.int, b C.int) C.int {
       return a * b
}

Deze code werkt, maar heeft wat speciale aandacht nodig. Ten eerste is er de speciale //export commentaar boven de Go-functie. Dit vertelt CGO om de functie te exporteren naar een apart C-bestand. Maar dat leidt tot de volgende beperking: "als uw programma gebruik maakt van enig //export richtlijnen, dan mag de C-code in het commentaar alleen declaraties (externe int f();), niet definities (int() { return 1; }). U kunt gebruik maken van //export richtlijnen om Go-functies toegankelijk te maken voor C-code." Zie ook: CGO documentatie

Dit is de reden waarom ik multiplyInGo als statisch inline. Dit voorkomt het probleem met de dubbele symbolen.

Compileren van C-code uit bestanden

Het goede nieuws is dat alle C-bestanden in uw module gewoon gecompileerd en bruikbaar zijn vanuit Go-code, u kunt ook #include "my_c_file.h" om de verklaringen te krijgen. Het slechte nieuws is: Het werkt niet in je hoofdpakket en je C-code mag geen afhankelijkheden hebben naar C-code buiten de Go-module map. Het opnemen van globaal beschikbare libs zoals #include werkt prima.

Er is één mogelijke oplossing, namelijk het opnemen van C-bestanden uit een andere map. De inhoud van de C-bestanden zal worden geïnlineeerd in de 1TP11Inclusief verklaring en dus gecompileerd. Maar pas op voor ongewenste neveneffecten en zorg ervoor dat dezelfde C-code niet tweemaal wordt opgenomen.

Omgaan met functiepunten

pakket main
/*
int go_multiply(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiply_f vermenigvuldigen;
static inline init() {
    multiply = go_multiply;
}
static inline int multiplyWithFp(int a, int b) {
    return multiply(a, b);
}
 */
import "C
import (
       "fmt"
)
func main() {
       a := 3
       b := 5
       C.init();
       c := C.multiplyWithFp(C.int(a), C.int(b))
       fmt.Println("multiplyInGo:", a, "+", b, "=", int(c))
}
//export go_multiply
func go_multiply(a C.int, b C.int) C.int {
       return a * b
}

De bovenstaande code heeft een init functie om de functieaanwijzer multiply naar de ga_vermenigvuldigen implementaties. Dit vereist het declareren van ga_vermenigvuldigen eerder, aangezien CGO de verklaring compileert in een apart headerbestand (_cgo_export.h) die we niet vóór de generatie kunnen opnemen. De typedef is niet strikt noodzakelijk, maar maakt het voorbeeld duidelijker. In multiplyWithFp roepen we gewoon de functie aan die is opgeslagen in de functie-aanwijzer.

De toewijzing van de functie-aanwijzer moet in C gebeuren. De volgende code zal niet werken als vervanging voor de C.init() bellen:

C.multiply = C.multiply_f(go_multiply);

Het mislukt met "cannot convert go_multiply (type func(C.int, C.int) C.int) to type C.multiply_f".

Omzetten van types, toegang tot structs en pointeraritmetiek

We hebben al wat type-conversie gedaan tussen int en C.int. Hetzelfde werkt voor alle andere types. Als u een struct my_struct_t in C kun je gewoon gebruiken met C.my_struct_t in Go. Je hebt toegang tot alle velden van de C structs en de CGO compiler controleert zelfs de types en detecteert wanneer je toegang hebt tot niet bestaande velden.

Er zijn enkele types die moeilijker te hanteren zijn. Maar CGO heeft enkele hulpfuncties:

// Ga string naar C string
// De C-string wordt in de C-heap gealloceerd met malloc.
Het is de verantwoordelijkheid van de beller om ervoor te zorgen dat deze // vrij komt.
// vrij te maken, bijvoorbeeld door C.free aan te roepen (zorg ervoor dat stdlib.h
// als C.free nodig is).
func C.CString(string) *C.char
// Ga []byte slice naar C array
// De C-array wordt in de C-heap gealloceerd met malloc.
Het is de verantwoordelijkheid van de beller om ervoor te zorgen dat het // vrij komt.
// vrij te maken, bijvoorbeeld door C.free aan te roepen (zorg ervoor stdlib.h op te nemen
// als C.free nodig is).
func C.CBytes([]byte) onveilig.Pointer
// C-string naar Go-string
func C.GoString(*C.char) string
// C data met expliciete lengte naar Go string
func C.GoStringN(*C.char, C.int) string
// C-gegevens met expliciete lengte naar Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

De meeste zijn vrij duidelijk. Ik was alleen een beetje in de war met de unsafe.pointer in C.GoBytes. Hier is een klein voorbeeld:

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

Zoals je ziet kunnen we onze C pointer gewoon converteren naar unsafe.pointer. Wanneer je een onveilige pointer hebt, bijvoorbeeld van C.CBytes en die wilt omzetten in een C pointer kun je dat als volgt doen:

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

Elke C void pointer wordt voorgesteld door Go unsafe.Pointer.

Omgaan met Pointer rekenen is erg gevaarlijk maar ook mogelijk:

func getPayload(packet *C.packet_t) []byte {
       dataPtr := unsafe.Pointer(packet.data)
       // Laten we uitgaan van een 2 byte header voor de payload.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       retour payload
}

Conclusie

Dat was het. We hebben gezien hoe je C-code aanroept vanuit Go en omgekeerd. Het is gemakkelijk om een paar C bestanden mee te compileren met je Go code, maar het wordt lastig als je grotere C code bases hebt. Het gebruik van functie-aanwijzers in C is een geweldige manier om native C-implementaties te vervangen door Go-implementaties. Op deze manier kun je functies als GetTimestamp in C die zijn geïmplementeerd met Go-functies waar je tijd kunt vervalsen tijdens tests.

Voor C structs zul je wat conversiecode moeten schrijven om de C structs om te zetten in Go structs, zodra je ze wilt gebruiken in andere delen van je Go-programma. Ik raad aan de Go-code die C-functies omhult strikt te scheiden en alleen Go-types door te geven tussen openbare Go-functies.

Alle types behalve functie-aanwijzers kunnen worden geconverteerd tussen C en Go, en zelfs functie-aanwijzers kunnen worden opgeslagen als onveilige aanwijzers en tussen beide werelden worden doorgegeven.

Er zijn nog enkele onderwerpen zoals #cgo vlaggen die zijn gedocumenteerd in de hierboven gelinkte bronnen, maar die niet in dit artikel worden behandeld.