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 bij Lobaro stond ik voor een aantal uitdagingen in Embedded C programmeren 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 om 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 CI infrastructuur te gebruiken.

En dan is er nog Go. Go is net als C, maar met oplossingen voor de meeste problemen die bij C horen. Allereerst een Garbage collector en slim Pointer management. 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 kunnen worden gebonden aan functies. En dan zijn er nog de twee belangrijkste eigenschappen 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) Je kunt C-code aanroepen vanuit Go en vice versa met CGO.

Laten we eens kijken hoe dit werkt. Een goed startpunt is de CGO documentatiede Ga Wiki en dit Blog Artikel. Ik concentreer me graag op het onderwerp waar ik moeite mee heb, zelfs na het lezen van de gekoppelde pagina's.

In de volgende paragrafen wil ik hier dieper op ingaan:

  • C-code aanroepen vanuit GO
  • Go-code aanroepen vanuit C
  • C-code compileren vanuit bestanden
  • Omgaan met functiepunten
  • Typen converteren, toegang tot structs en pointeraritmetica

C-code aanroepen vanuit Go

pakket hoofd
/*
// Alles in commentaar boven de import "C" is C-code en wordt gecompileerd met GCC.
// Zorg ervoor dat GCC geïnstalleerd is.
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 eenvoudigweg een C-functie definiëren zoals addInC en deze aanroepen met behulp van het C-pakket.
  • C-types zoals int maar ook structs die je definieert zijn toegankelijk via het C-pakket. Op deze manier kunnen we gewoon onze Go int converteren naar C.int en terug.
    • De conversie binnen Println is niet nodig, maar maakt het voorbeeld duidelijker.

Go-code aanroepen vanuit C

pakket hoofd
/*
static inline int multiplyInGo(int a, int b) {
    return go_multiply(a, b);
}
 */
importeer "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 //exporteren 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 je programma gebruik maakt van een //exporteren richtlijnen, dan mag de C-code in het commentaar alleen declaraties (externe int f();), niet definities (int f() { return 1; }). U kunt //exporteren richtlijnen om Go-functies toegankelijk te maken voor C-code." Zie ook: CGO documentatie

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

C-code compileren vanuit bestanden

Het goede nieuws is dat alle C-bestanden in je module gewoon gecompileerd en bruikbaar zijn vanuit Go-code, je 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 om C-bestanden uit een andere map op te nemen. De inhoud van de C-bestanden zal worden geïnlineeerd in de 1TP11Inbegrepen statement en dus gecompileerd. Maar wees je bewust van ongewenste neveneffecten en zorg ervoor dat dezelfde C-code niet twee keer wordt opgenomen.

Omgaan met functiepunten

pakket hoofd
/*
int go_multiply(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiply_f vermenigvuldigen;
statisch 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("vermenigvuldigInGo:", 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 te vermenigvuldigen met de gaan_vermenigvuldigen implementaties. Dit vereist het declareren van gaan_vermenigvuldigen voorheen, omdat CGO de declaratie compileert in een apart headerbestand (_cgo_export.h) die we niet vóór het genereren kunnen opnemen. De typedef is niet strikt noodzakelijk, maar maakt het voorbeeld duidelijker. In vermenigvuldigenmetFp roepen we gewoon de functie aan die is opgeslagen in de functieaanwijzer.

De toewijzing van de functiepointer 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 "kan go_multiply (type func(C.int, C.int) C.int) niet converteren naar type C.multiply_f".

Typen converteren, toegang tot structs en pointeraritmetica

We hebben al wat typeconversie gedaan tussen int en C.int. Hetzelfde werkt voor alle andere types. Als je een struct mijn_structuur_t in C kun je gewoon gebruiken met C.mijn_structuur_t in Go. Je hebt toegang tot alle velden van de C structs en de CGO compiler zal zelfs de types controleren en detecteren 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 // wordt
// vrij te maken, bijvoorbeeld door C.free aan te roepen (zorg ervoor dat u stdlib.h
// op te nemen als C.free nodig is).
func C.CString(string) *C.char
// Ga []byte slice naar C array
// De C-array wordt gealloceerd in de C-heap met malloc.
// Het is de verantwoordelijkheid van de beller om ervoor te // zorgen dat het
// vrij te maken, bijvoorbeeld door C.free aan te roepen (zorg ervoor dat u 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, lengte C.uint8_t) []byte {
       return C.GoBytes(unsafe.Pointer(data), C.int(length))
}

Zoals je kunt zien, kunnen we onze C pointer gewoon converteren naar unsafe.pointer. Als je een onveilige pointer hebt, bijvoorbeeld van C.CBytes en deze wilt converteren naar 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 weergegeven door Go unsafe.pointer.

Omgaan met Pointer arithmetic 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))
       payload terugsturen
}

Conclusie

Dat was het. We hebben gezien hoe je C-code kunt aanroepen vanuit Go en vice versa. Het is makkelijk om een paar C bestanden samen met je Go code te compileren, 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 naar Go structs, zodra je ze wilt gebruiken in andere delen van je Go-programma. Ik raad aan om de Go-code die C-functies omhult strikt te scheiden en alleen Go-types door te geven tussen openbare Go-functies.

Alle typen behalve functie-aanwijzers kunnen worden geconverteerd tussen C en Go en zelfs functie-aanwijzers kunnen worden opgeslagen als unsafe.pointer en tussen beide werelden worden doorgegeven.

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