Indlejret udvikling med C og GoLang (CGO)

Hej, jeg er ikke embedded-udvikler. Min kodeerfaring er baseret på C#, Java, Go Desktop og serverudvikling. For nylig hos Lobaro stod jeg over for nogle udfordringer i embedded C-programmering og bemærkede - endnu en gang - den enorme forskel mellem hukommelsesstyringens hellige land og C-pointerlabyrinten, hvor man skal tænke sig om to gange ved hvert malloc-kald. C-kode er ofte ikke særlig generisk og dermed mindre genanvendelig. Der er ikke noget, der hedder moduler, men en masse globale ting, som har en tendens til at ødelægge andre globale ting. Folk tester sjældent C-kode, og hvis de gør, gør de det på den hardware, den er skrevet til, hvilket gør det næsten umuligt at bruge nogen CI-infrastruktur.

Og så er der Go. Go er ligesom C, men med løsninger på de fleste problemer, der følger med C. Først og fremmest en garbage collector og smart pointer management. Det er rart, men også grunden til, at du aldrig ville køre et Go-program på din ARM-mikrocontroller, selvom Go kompilerer til native ARM-kode. For at nævne nogle andre problemområder, der blev løst fint: Stor genanvendelighed understøttet af et fint modulsystem, samtidighed fungerer bare, meget få køretidsproblemer på grund af et strengt statisk typesystem (ikke engang generics), og for alle venner af C har det strukturer, der kan bindes til funktioner. Og så er der de to vigtigste funktioner for denne artikel: (1) Test er indbygget. Det er så simpelt som at sætte en Test* funktion i en *_test.go fil og udfør Gå til test. (2) Du kan kalde C-kode fra Go og vice versa ved hjælp af CGO.

Lad os se på, hvordan det fungerer. Et godt udgangspunkt er CGO-dokumentation, den Gå til Wiki og dette Blogartikel. Jeg kan godt lide at fokusere på det emne, jeg kæmpede med, selv efter at have læst de linkede sider.

I de følgende afsnit vil jeg se nærmere på det:

  • Opkald til C-kode fra GO
  • Opkald til Go-kode fra C
  • Kompilering af C-kode fra filer
  • Håndtering af funktionspointere
  • Konvertering af typer, adgang til strukturer og pointeraritmetik

Kald af C-kode fra Go

pakke main
/*
// Alt i kommentarer over importen "C" er C-kode og vil blive kompileret med GCC.
// Sørg for, at du har GCC installeret.
int addInC(int a, int b) {
    returner a + b;
}
 */
import "C
import "fmt"
func main() {
       a := 3
       b := 5
       
       c := C.addInC(C.int(a), C.int(b))
       fmt.Println("Tilføj i C:", a, "+", b, "=", int(c))
}

Ud fra dette kan vi allerede se en masse:

  • import "C indeholder en særlig pakke, der fortæller Go-værktøjet, at det skal kompilere med CGO. Kommentarer direkte over denne import er kompileret til C ved hjælp af GCC.
  • Vi kan simpelthen definere en C-funktion som addInC og kalde den ved hjælp af C-pakken.
  • C-typer som int, men også structs, som du definerer, kan tilgås via C-pakken. På den måde kan vi bare konvertere vores Go int til C.int og tilbage igen.
    • Konverteringen i Println er ikke nødvendig, men gør eksemplet mere overskueligt.

Opkald til Go-kode fra C

pakke main
/*
statisk 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
}

Denne kode virker, men kræver lidt særlig opmærksomhed. Først er der den særlige //eksport kommentar over Go-funktionen. Den fortæller CGO, at den skal eksportere funktionen til en separat C-fil. Men det fører til følgende begrænsning: "Hvis dit program bruger en //eksport direktiver, så må C-koden i kommentaren kun indeholde deklarationer (ekstern int f();), ikke definitioner (int f() { return 1; }). Du kan bruge //eksport direktiver til at gøre Go-funktioner tilgængelige for C-kode." Se også her: CGO-dokumentation

Det er grunden til, at jeg definerede MultiplicerIndGå som statisk inline. På den måde undgår man problemet med de dobbelte symboler.

Kompilering af C-kode fra filer

Den gode nyhed er, at alle C-filer i dit modul bare er kompileret og kan bruges fra Go-kode, du kan også bruge #include "my_c_file.h" for at få erklæringerne. De dårlige nyheder er: Det virker ikke i din hovedpakke, og din C-kode må ikke have nogen afhængigheder til C-kode uden for Go-modulmappen. At inkludere globalt tilgængelige libs som #include fungerer fint.

Der er en mulig løsning, og det er at inkludere C-filer fra en anden mappe. C-filens indhold vil blive inlinet ved 1TP11Inkluderer statement og dermed kompileret. Men vær opmærksom på uønskede sideeffekter, og sørg for, at den samme C-kode ikke inkluderes to gange.

Håndtering af funktionspointere

pakke main
/*
int go_multiply(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiply_f multiplicerer;
statisk inline init() {
    multiply = go_multiply;
}
statisk 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
}

Koden ovenfor har en start funktion til at opsætte funktionspointeren multipliceret til go_multiply implementeringer. Dette kræver, at man erklærer go_multiply før, da CGO kompilerer deklarationen i en separat header-fil (_cgo_export.h), som vi ikke kan inkludere før genereringen. Typedefen er ikke strengt nødvendig, men gør eksemplet mere klart. I multiplicerMedFp kalder vi simpelthen den funktion, der er gemt i funktionspointeren.

Tildelingen af funktionspointeren skal ske i C. Følgende kode vil ikke fungere som erstatning for C.init() ring:

C.multiply = C.multiply_f(go_multiply);

Den fejler med "cannot convert go_multiply (type func(C.int, C.int) C.int) to type C.multiply_f".

Konvertering af typer, adgang til strukturer og pointeraritmetik

Vi har allerede lavet en typekonvertering mellem int og C.int. Det samme gælder for alle andre typer. Hvis du har en struct my_struct_t i C kan du bare bruge is med C.my_struct_t i Go. Du kan tilgå alle felter i C-strukturerne, og CGO-compileren vil endda tjekke typerne og opdage, når du tilgår felter, der ikke eksisterer.

Der er nogle typer, som er sværere at håndtere. Men CGO kommer med nogle hjælpefunktioner:

// Go-streng til C-streng
// C-strengen allokeres i C-heap'en ved hjælp af malloc.
// Det er opkalderens ansvar at sørge for, at den bliver
// fri, f.eks. ved at kalde C.free (sørg for at inkludere stdlib.h
// hvis C.free er nødvendig).
func C.CString(string) *C.char
// Go []byte slice to C array
// C-arrayet allokeres i C-heap'en ved hjælp af malloc.
// Det er opkalderens ansvar at sørge for, at det bliver
// frigives, f.eks. ved at kalde C.free (sørg for at inkludere stdlib.h
// hvis C.free er nødvendig).
func C.CBytes([]byte) unsafe.Pointer
// C-streng til Go-streng
func C.GoString(*C.char) string
// C-data med eksplicit længde til Go-streng
func C.GoStringN(*C.char, C.int) string
// C-data med eksplicit længde til Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

De fleste af dem er ret klare. Jeg blev bare lidt forvirret over unsafe.pointer i C.GoBytes. Her er et lille eksempel:

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 bare konvertere vores C-pointer til unsafe.pointer. Når du har en usikker pointer, f.eks. fra C.CBytes, og vil konvertere den til en C-pointer, kan du gøre det på denne måde:

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

Enhver C void pointer er repræsenteret af Go unsafe.Pointer.

At håndtere pointer-aritmetik er meget farligt, men også muligt:

func getPayload(packet *C.packet_t) []byte {
       dataPtr := unsafe.Pointer(packet.data)
       // Lad os antage, at der er en 2 byte header før payload.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       return payload
}

Konklusion

Det var så det. Vi har set, hvordan man kalder C-kode fra Go og vice versa. Det er nemt at kompilere et par C-filer sammen med din Go-kode, men det bliver vanskeligt, når du har større C-kodebaser. Brug af funktionspointere i C er en god måde at erstatte native C-implementeringer med Go-implementeringer. På den måde kan du have funktioner som GetTimestamp i C, som er implementeret med Go-funktioner, hvor du kan spare tid under tests.

For C-structs skal du skrive noget konverteringskode for at konvertere C-structs til Go-structs, så snart du vil bruge dem i andre dele af dit Go-program. Jeg anbefaler, at man strengt adskiller den Go-kode, der indpakker C-funktioner, og kun sender Go-typer mellem offentlige Go-funktioner.

Alle typer undtagen funktionspointere kan konverteres mellem C og Go, og selv funktionspointere kan gemmes som usikre pointere og sendes frem og tilbage mellem de to verdener.

Der er stadig nogle emner som #cgo flag, der er dokumenteret i de linkede ressourcer ovenfor, men som ikke er dækket i denne artikel.