Innebygd utvikling med C og GoLang (CGO)

Hei, jeg er ikke en embedded-utvikler. Min kodeerfaring er basert på C#, Java, Go Desktop og serverutvikling. Nylig sto jeg overfor noen utfordringer i Embedded C-programmering på Lobaro og merket - nok en gang - den enorme forskjellen mellom det hellige landet for minnehåndtering og C-pekerlabyrinten der du må tenke deg om to ganger for hvert malloc-kall. C-kode er ofte lite generisk og dermed mindre gjenbrukbar. Det er ikke noe som heter moduler, men en haug med globale ting som har en tendens til å ødelegge andre globale ting. Folk tester sjelden C-kode, og hvis de gjør det, gjør de det på maskinvaren den er skrevet for, noe som gjør det nesten umulig å bruke CI-infrastruktur.

Og så har vi Go. Go er som C, men med løsninger på de fleste problemene som følger med C. Først og fremst en søppelsamler og smart pekerhåndtering. Det er fint, men også grunnen til at du aldri ville kjørt et Go-program på ARM-mikrokontrolleren din, selv om Go kompilerer til nativ ARM-kode. For å nevne noen andre problemområder som ble løst på en fin måte: Stor gjenbrukbarhet som støttes av et fint modulsystem, samtidighet bare fungerer, svært få kjøretidsproblemer på grunn av et strengt statisk typesystem (ikke engang generiske typer), og for alle C-venner har Go Structs som kan bindes til funksjoner. Og så er det de to viktigste funksjonene for denne artikkelen: (1) Testing er innebygd. Det er så enkelt som å sette en Test* funksjon i en *_test.go filen og kjør Gå til test. (2) Du kan kalle C-kode fra Go og omvendt ved hjelp av CGO.

La oss ta en titt på hvordan dette fungerer. Et godt utgangspunkt er CGO-dokumentasjon, den Gå til Wiki og dette Bloggartikkel. Jeg liker å fokusere på emnet jeg slet med selv etter å ha lest de lenkede sidene.

I de følgende avsnittene vil jeg se nærmere på dette:

  • Kaller C-kode fra GO
  • Kaller Go-kode fra C
  • Kompilering av C-kode fra filer
  • Håndtering av funksjonspekere
  • Konvertere typer, tilgangsstrukturer og pekeraritmetikk

Kaller C-kode fra Go

pakke main
/*
// Alt i kommentarene over importen "C" er C-kode og vil bli kompilert med GCC.
// Sørg for at du har GCC installert.
int addInC(int a, int b) {
    returnerer a + b;
}
 */
import "C
import "fmt"
func main() {
       a := 3
       b := 5
       
       c := C.addInC(C.int(a), C.int(b))
       fmt.Println("Legg til i C:", a, "+", b, "=", int(c))
}

Av dette kan vi allerede se mye:

  • importere "C inneholder en spesiell pakke som forteller Go-verktøyet at det skal kompilere med CGO. Kommentarer rett over denne importen kompileres til C ved hjelp av GCC.
  • Vi kan ganske enkelt definere en C-funksjon som addInC og kalle den ved hjelp av C-pakken.
  • C-typer som int, men også strukter du definerer, kan nås via C-pakken. På denne måten kan vi bare konvertere Go int til C.int og tilbake.
    • Konverteringen i Println er ikke nødvendig, men gjør eksemplet mer oversiktlig.

Kaller Go-kode fra C

pakke 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
}

Denne koden fungerer, men krever litt spesiell oppmerksomhet. Først er det den spesielle //eksport kommentar over Go-funksjonen. Dette ber CGO om å eksportere funksjonen til en egen C-fil. Men det fører til følgende begrensning: "hvis programmet ditt bruker noen av //eksport direktiver, kan C-koden i kommentaren bare inneholde deklarasjoner (ekstern int f();), ikke definisjoner (int f() { return 1; }). Du kan bruke //eksport direktiver for å gjøre Go-funksjoner tilgjengelige for C-kode." Se også: CGO-dokumentasjon

Dette er grunnen til at jeg definerte multiplyInGo som statisk inline. Dermed unngår du problemet med dupliserte symboler.

Kompilering av C-kode fra filer

Den gode nyheten er at alle C-filene i modulen din bare er kompilert og kan brukes fra Go-koden. #include "my_c_file.h" for å få tak i erklæringene. De dårlige nyhetene er: Det fungerer ikke i hovedpakken, og C-koden din må ikke ha noen avhengigheter til C-kode utenfor Go-modulmappen. Det fungerer fint å inkludere globalt tilgjengelige libs som #include.

Det finnes en mulig løsning, nemlig å inkludere C-filer fra en annen mappe. Innholdet i C-filen vil bli inlinet ved 1TP11Inkludert og dermed kompileres. Men vær oppmerksom på uønskede bivirkninger, og pass på at den samme C-koden ikke inkluderes to ganger.

Håndtering av funksjonspekere

pakke main
/*
int go_multiply(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiply_f multipliserer;
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 innledende funksjonen for å sette opp funksjonspekeren multiplisert med go_multiply implementeringer. Dette krever at du erklærer go_multiply før, siden CGO kompilerer erklæringen i en egen header-fil (_cgo_export.h) som vi ikke kan inkludere før generering. Typedefen er ikke strengt tatt nødvendig, men gjør eksemplet mer oversiktlig. I multiplyWithFp kaller vi ganske enkelt funksjonen som er lagret i funksjonspekeren.

Tildelingen av funksjonspekeren må skje i C. Følgende kode vil ikke fungere som en erstatning for C.init() ring:

C.multiply = C.multiply_f(go_multiply);

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

Konvertere typer, tilgangsstrukturer og pekeraritmetikk

Vi har allerede gjort en typekonvertering mellom int og C.int. Det samme gjelder for alle andre typer. Hvis du har en struct my_struct_t i C kan du bare bruke er med C.my_struct_t i Go. Du kan få tilgang til alle feltene i C-strukturene, og CGO-kompilatoren sjekker til og med typene og oppdager når du får tilgang til felter som ikke finnes.

Det finnes noen typer som er vanskeligere å håndtere. Men CGO kommer med noen hjelpefunksjoner:

// Go-streng til C-streng
// C-strengen allokeres i C-heapen ved hjelp av malloc.
// Det er oppkallerens ansvar å sørge for at den blir
// frigjøre den, for eksempel ved å kalle C.free (sørg for å inkludere stdlib.h
// hvis C.free er nødvendig).
func C.CString(string) *C.char
// Go []byte slice til C-matrise
// C-matrisen allokeres i C-heapen ved hjelp av malloc.
// Det er oppkallerens ansvar å sørge for å frigjøre den.
// frigjøre den, for eksempel ved å kalle C.free (sørg for å 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 eksplisitt lengde til Go-streng
func C.GoStringN(*C.char, C.int) string
// C-data med eksplisitt lengde til Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

De fleste av dem er ganske klare. Jeg ble bare litt forvirret av unsafe.pointer i C.GoBytes. Her er et lite eksempel:

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

Som du ser, kan vi bare konvertere C-pekeren vår til unsafe.pointer. Når du har en usikker peker, f.eks. fra C.CBytes, og ønsker å konvertere den til en C-peker, kan du gjøre det på denne måten:

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

Enhver C void-peker representeres av Go unsafe.Pointer.

Det er svært farlig, men også mulig, å håndtere pekeraritmetikk:

func getPayload(packet *C.packet_t) []byte {
       dataPtr := unsafe.Pointer(pakke.data)
       // La oss anta en header på 2 byte før nyttelasten.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       return payload
}

Konklusjon

Det var det. Vi har sett hvordan du kaller C-kode fra Go og omvendt. Det er enkelt å kompilere noen få C-filer sammen med Go-koden, men det blir vanskeligere når du har større C-kodebaser. Bruk av funksjonspekere i C er en fin måte å erstatte opprinnelige C-implementeringer med Go-implementeringer på. På denne måten kan du ha funksjoner som GetTimestamp i C som er implementert med Go-funksjoner der du kan bruke tid på å lure deg gjennom testene.

For C-strukturer må du skrive litt konverteringskode for å konvertere C-strukturer til Go-strukturer så snart du vil bruke dem i andre deler av Go-programmet. Jeg anbefaler at du holder Go-koden som omslutter C-funksjoner, strengt adskilt og bare overfører Go-typer mellom offentlige Go-funksjoner.

Alle typer unntatt funksjonspekere kan konverteres mellom C og Go, og selv funksjonspekere kan lagres som usikre pekere og sendes frem og tilbake mellom de to verdenene.

Det er fortsatt noen emner som #cgo flagg som er dokumentert i de lenkede ressursene ovenfor, men som ikke er dekket i denne artikkelen.