Inbäddad utveckling med C och GoLang (CGO)

Hej, jag är inte en inbäddad utvecklare. Min kodningserfarenhet är baserad på C#, Java, Go Desktop och serverutveckling. Nyligen på Lobaro stod jag inför några utmaningar i Embedded C-programmering och märkte - än en gång - den enorma skillnaden mellan minneshanteringens heliga land och C-pekarlabyrinten där du måste tänka två gånger om varje malloc-anrop. C-kod är ofta inte särskilt generisk och därmed mindre återanvändbar. Det finns inget som heter moduler, utan en massa globala saker som tenderar att förstöra andra globala saker. Människor testar sällan C-kod, och om de gör det, gör de det på den hårdvara som den är skriven för, vilket gör det nästan omöjligt att använda någon CI-infrastruktur.

Och så finns det Go. Go är som C men med lösningar på de flesta problem som följer med C. Först och främst en skräpsamlare och smart pekarhantering. Det är trevligt, men också anledningen till att du aldrig skulle köra ett Go-program på din ARM-mikrokontroller, även om Go kompilerar till inbyggd ARM-kod. För att nämna några andra problemområden som löstes på ett bra sätt: Bra återanvändbarhet som stöds av ett bra modulsystem, samtidighet fungerar bara, mycket få körtidsproblem på grund av ett strikt statiskt typsystem (inte ens generics), och för alla C-vänner har det strukturer som kan bindas till funktioner. Och sedan finns det de två viktigaste funktionerna för den här artikeln: (1) Testning är inbyggd. Det är så enkelt som att sätta en Test*. funktion till en *_test.go fil och kör Gå test. (2) Du kan anropa C-kod från Go och vice versa med CGO.

Låt oss ta en titt på hur detta fungerar. En bra utgångspunkt är CGO-dokumentation, den Gå till Wiki och detta Bloggartikel. Jag gillar att fokusera på det ämne som jag kämpade med även efter att ha läst de länkade sidorna.

I de följande avsnitten vill jag titta närmare på:

  • Anropa C-kod från GO
  • Anropa Go-kod från C
  • Kompilering av C-kod från filer
  • Hantering av funktionspekare
  • Konvertera typer, accessstrukturer och pekararitmetik

Anropa C-kod från Go

paket main
/*
// Allt i kommentarer ovanför importen "C" är C-kod och kommer att kompileras med GCC.
// Se till att du har en GCC installerad.
int addInC(int a, int b) {
    returnera a + b;
}
 */
importera "C"
importera "fmt"
func main() {
       a := 3
       b := 5
       
       c := C.addInC(C.int(a), C.int(b))
       fmt.Println("Lägg till i C:", a, "+", b, "=", int(c))
}

Från detta kan vi redan se mycket:

  • importera "C" innehåller ett speciellt paket som talar om för Go-verktyget att det ska kompilera med CGO. Kommentarer direkt ovanför denna import är kompilerade till C med hjälp av GCC.
  • Vi kan helt enkelt definiera en C-funktion som addInC och anropa den med hjälp av C-paketet.
  • C-typer som int men även structs som du definierar kan nås via C-paketet. På så sätt kan vi bara konvertera vår Go int till C.int och tillbaka.
    • Omvandlingen i Println är inte nödvändig men gör exemplet tydligare.

Anropa Go-kod från C

paket main
/*
statisk inline int multiplyInGo(int a, int b) {
    return go_multiply(a, b);
}
 */
Importera "C"
importera (
       "fmt"
)
func main() {
       a := 3
       b := 5
       
       c := C.multiplyInGo(C.int(a), C.int(b))
       fmt.Println("multiplyInGo:", a, "*", b, "=", int(c))
}
//exportera go_multiply
func go_multiply(a C.int, b C.int) C.int {
       returnera a * b
}

Denna kod fungerar men kräver lite särskild uppmärksamhet. Först har vi den speciella //export kommentar ovanför Go-funktionen. Den säger åt CGO att exportera funktionen till en separat C-fil. Men det leder till följande begränsning: "Om ditt program använder någon //export direktiv, då får C-koden i kommentaren endast innehålla deklarationer (extern int f();), inte definitioner (int f() { return 1; }). Du kan använda //export direktiv för att göra Go-funktioner tillgängliga för C-kod." Se även: CGO-dokumentation

Detta är anledningen till att jag definierade multipliceraInGo som statisk inline. På så sätt undviks problemet med dubbla symboler.

Kompilering av C-kod från filer

Den goda nyheten är att alla C-filer i din modul bara är kompilerade och användbara från Go-kod, du kan också använda #include "my_c_file.h" för att få deklarationerna. De dåliga nyheterna är: Det fungerar inte i ditt huvudpaket och din C-kod får inte ha några beroenden till C-kod utanför Go-modulmappen. Att inkludera globalt tillgängliga libs som #include fungerar bra.

Det finns en möjlig lösning, nämligen att inkludera C-filer från en annan mapp. C-filens innehåll kommer att infogas i 1TP11Inklusive och därmed kompileras. Men var uppmärksam på oönskade bieffekter och se till att samma C-kod inte inkluderas två gånger.

Hantering av funktionspekare

paket main
/*
int go_multiply(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiply_f multiplicera;
statisk inline init() {
    multiply = go_multiply;
}
statisk inline int multiplyWithFp(int a, int b) {
    returnera multiply(a, b);
}
 */
importera "C"
importera (
       "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))
}
//exportera go_multiply
func go_multiply(a C.int, b C.int) C.int {
       returnera a * b
}

Koden ovan har en Init funktion för att ställa in funktionspekaren multiplicera till gå_multiplicera implementeringar. Detta kräver att man deklarerar gå_multiplicera tidigare, eftersom CGO kompilerar deklarationen i en separat headerfil (_cgo_export.h) som vi inte kan inkludera före generering. Typdefinitionen är inte strikt nödvändig men gör exemplet tydligare. I multipliceraMedFp anropar vi helt enkelt den funktion som finns lagrad i funktionspekaren.

Tilldelningen av funktionspekaren måste ske i C. Följande kod fungerar inte som ersättning för C.init() ring:

C.multiply = C.multiply_f(go_multiply);

Det misslyckas med "kan inte konvertera go_multiply (typ func(C.int, C.int) C.int) till typ C.multiply_f"

Konvertera typer, accessstrukturer och pekararitmetik

Vi har redan gjort en typkonvertering mellan int och C.int. Samma sak gäller för alla andra typer. Om du har en struct my_struct_t i C kan du bara använda is med C.my_struct_t i Go. Du kan komma åt alla fält i C-strukturerna och CGO-kompilatorn kommer även att kontrollera typerna och upptäcka när du kommer åt icke-existerande fält.

Det finns vissa typer som är svårare att hantera. Men CGO kommer med några hjälpfunktioner:

// Go-sträng till C-sträng
// C-strängen allokeras i C-heapen med hjälp av malloc.
// Det är uppringarens ansvar att se till att den
// frigörs, t.ex. genom att anropa C.free (se till att inkludera stdlib.h
// om C.free behövs).
func C.CString(sträng) *C.char
// Go []byte-slice till C-array
// C-arrayen allokeras i C-heapen med hjälp av malloc.
// Det är anroparens ansvar att se till att den blir
// frigöras, t.ex. genom att anropa C.free (se till att inkludera stdlib.h
// om C.free behövs).
func C.CBytes([]byte) osäker.pekare
// C-sträng till Go-sträng
func C.GoString(*C.char) sträng
// C-data med explicit längd till Go-sträng
func C.GoStringN(*C.char, C.int) sträng
// C-data med explicit längd till Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

De flesta av dem är ganska tydliga. Jag blev bara lite förvirrad av unsafe.pointer i C.GoBytes. Här är ett litet exempel:

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

Som du kan se kan vi bara konvertera vår C-pekare till unsafe.pointer. När du har fått en osäker pekare, t.ex. från C.CBytes och vill konvertera den till en C-pekare, kan du göra så här:

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

Alla C void-pekare representeras av Go unsafe.pointer.

Att hantera pekararitmetik är mycket farligt men också möjligt:

func getPayload(paket *C.paket_t) []byte {
       dataPtr := unsafe.Pointer(paket.data)
       // Låt oss anta att det finns ett 2 byte stort sidhuvud före nyttolasten.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       returnera nyttolast
}

Slutsats

Det var det. Vi har sett hur man anropar C-kod från Go och vice versa. Det är enkelt att kompilera några C-filer tillsammans med din Go-kod, men det blir knepigt när du har större C-kodbaser. Att använda funktionspekare i C är ett bra sätt att ersätta inbyggda C-implementeringar med Go-implementeringar. På så sätt kan du ha funktioner som Hämta tidsstämpel i C som är implementerade med Go-funktioner där du kan fuska tid under tester.

För C-strukter måste du skriva konverteringskod för att konvertera C-strukter till Go-strukter, så snart du vill använda dem i andra delar av ditt Go-program. Jag rekommenderar att du strikt separerar Go-koden som omsluter C-funktioner och endast skickar Go-typer mellan publika Go-funktioner.

Alla typer utom funktionspekare kan konverteras mellan C och Go och även funktionspekare kan lagras som unsafe.pointer och skickas fram och tillbaka mellan de båda världarna.

Det finns fortfarande några ämnen som #cgo flaggor som dokumenteras i de länkade resurserna ovan men som inte omfattas av denna artikel.