Sulautettu kehitys C:llä ja GoLangilla (CGO)

Hei, en ole sulautettu kehittäjä. Koodauskokemukseni perustuu C#-, Java-, Go Desktop- ja palvelinkehitykseen. Hiljattain Lobaro:ssä kohtasin joitakin haasteita sulautetun C-ohjelmoinnin parissa ja huomasin - jälleen kerran - valtavan eron muistinhallinnan pyhän maan ja C:n osoitinlabyrintin välillä, jossa jokaista malloc-kutsua pitää miettiä kahdesti. C-koodi ei useinkaan ole kovin yleistä ja siten vähemmän uudelleenkäytettävää. Ei ole olemassa moduuleja, vaan joukko globaaleja asioita, jotka yleensä rikkovat muita globaaleja asioita. C-koodia testataan harvoin, ja jos testataankin, se tehdään laitteistolla, jolle se on kirjoitettu, mikä tekee CI-infrastruktuurin käytön lähes mahdottomaksi.

Ja sitten on vielä Go. Go on kuin C, mutta siinä on ratkaisut useimpiin C:n mukanaan tuomiin ongelmiin. Ensinnäkin roskienkerääjä ja älykäs osoittimien hallinta. Se on mukavaa, mutta myös syy siihen, miksi et koskaan ajaisi Go:n ohjelmaa ARM-mikrokontrollerilla, vaikka Go kääntää natiivin ARM-koodin. Mainitakseni joitakin muita ongelma-alueita, jotka on ratkaistu hienosti: hieno moduulijärjestelmä tukee suurta uudelleenkäytettävyyttä, samanaikaisuus vain toimii, erittäin vähän ajoaikaisia ongelmia tiukan staattisen tyyppijärjestelmän ansiosta (ei edes geneerisiä), ja kaikille C:n ystäville on tarjolla Structs-rakenteita, jotka voidaan sitoa funktioihin. Ja sitten on vielä kaksi tämän artikkelin kannalta tärkeintä ominaisuutta: (1) Testaus on sisäänrakennettu. Se on yhtä yksinkertaista kuin laittaa Testi* funktio *_test.go tiedosto ja suorita mene testiin. (2) Voit kutsua C-koodia Go:sta ja päinvastoin CGO:n avulla.

Katsotaanpa, miten tämä toimii. Hyvä lähtökohta on CGO-asiakirjat... Go Wiki ja tämä Blogiartikkeli. Haluan keskittyä aiheeseen, jonka kanssa kamppailin myös linkitettyjen sivujen lukemisen jälkeen.

Seuraavissa jaksoissa haluan tarkastella lähemmin:

  • C-koodin kutsuminen GO:sta
  • Go-koodin kutsuminen C:stä
  • C-koodin kääntäminen tiedostoista
  • Toiminto-osoittimien käsittely
  • Muunna tyypit, käytä rakennetta ja osoitinaritmetiikkaa.

C-koodin kutsuminen Go:sta

paketti main
/*
// Kaikki "C"-tuonnin yläpuolella olevissa kommenteissa on C-koodia ja se käännetään GCC:llä.
// Varmista, että sinulla on GCC asennettuna.
int addInC(int a, int b) {
    return a + b;
}
 */
import "C
import "fmt"
func main() {
       a := 3
       b := 5
       
       c := C.addInC(C.int(a), C.int(b))
       fmt.Println("Lisää C:ssä:", a, "+", b, "=", int(c))
}

Tästä voimme jo nähdä paljon:

  • tuonti "C tarjoaa erityisen paketin, joka käskee Go-työkalua kääntämään CGO:n kanssa. Suoraan tämän tuonnin yläpuolella olevat kommentit käännetään C:ksi GCC:llä.
  • Voimme yksinkertaisesti määritellä C-funktion, kuten addInC, ja kutsua sitä C-paketin avulla.
  • C-tyyppejä, kuten int, mutta myös määrittelemiäsi rakennelmia voidaan käyttää C-paketin kautta. Näin voimme vain muuntaa Go int -tyyppimme C.int:ksi ja takaisin.
    • Muunnos Printlnin sisällä ei ole välttämätön, mutta tekee esimerkistä selkeämmän.

Go-koodin kutsuminen C:stä

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

Tämä koodi toimii, mutta vaatii erityistä huomiota. Ensinnäkin on erityinen //export kommentti Go-funktion yläpuolella. Tämä käskee CGO:ta viemään funktion erilliseen C-tiedostoon. Mutta tämä johtaa seuraavaan rajoitukseen: "jos ohjelmasi käyttää mitä tahansa //export direktiivejä, niin kommentissa oleva C-koodi saa sisältää vain julistuksia (ulkoinen int f();), ei määritelmiä (int f() { return 1; }). Voit käyttää //export direktiivejä, jotta Go-funktiot ovat C-koodin käytettävissä." Katso myös: CGO-asiakirjat

Tästä syystä määrittelin multiplyInGo kuten static inline. Näin vältytään kaksoissymboleista aiheutuvilta ongelmilta.

C-koodin kääntäminen tiedostoista

Hyvä uutinen on, että kaikki moduulisi C-tiedostot on vain käännetty ja käyttökelpoisia Go-koodista, voit myös käyttää #include "my_c_file.h" saadaksesi ilmoitukset. Huonot uutiset ovat: C-koodillasi ei saa olla riippuvuuksia C-koodiin Go-moduulikansion ulkopuolella. Globaalisti saatavilla olevien libien, kuten #include, sisällyttäminen toimii hyvin.

On olemassa yksi mahdollinen kiertotie, joka on C-tiedostojen sisällyttäminen toisesta kansiosta. C-tiedoston sisältö sisällytetään riviin osoitteessa 1TP11Sisältää lausunto ja siten koottu. Ole kuitenkin tietoinen ei-toivotuista sivuvaikutuksista ja varmista, että samaa C-koodia ei sisällytetä kahdesti.

Toiminto-osoittimien käsittely

paketti main
/*
int go_multiply(int a, int b);
typedef int (*multiply_f)(int a, int b);
multiply_f multiply;
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
}

Yllä olevassa koodissa on init funktio asettaa funktion osoittimen moninkertaisesti funktioon go_multiply toteutukset. Tämä edellyttää, että ilmoitetaan go_multiply aiemmin, koska CGO kääntää ilmoituksen erilliseen otsikkotiedostoon (_cgo_export.h), joita emme voi sisällyttää aiempaan sukupolveen. Typedef ei ole varsinaisesti välttämätön, mutta tekee esimerkistä selkeämmän. Osoitteessa multiplyWithFp me yksinkertaisesti kutsumme funktio-osoittimeen tallennettua funktiota.

Funktion osoittimen osoittamisen on tapahduttava C:ssä. Seuraava koodi ei korvaa funktiota C.init() soittaa:

C.multiply = C.multiply_f(go_multiply);

Se epäonnistuu seuraavalla tavalla: "cannot convert go_multiply (type func(C.int, C.int) C.int) to type C.multiply_f".

Muunna tyypit, käytä rakennetta ja osoitinaritmetiikkaa.

Teimme jo jonkin verran tyyppimuunnoksia int:n ja C.int:n välillä. Sama pätee kaikkiin muihin tyyppeihin. Jos sinulla on struct my_struct_t C:ssä voit vain käyttää on kanssa C.my_struct_t in Go. Voit käyttää kaikkia C-rakenteiden kenttiä, ja CGO-kääntäjä jopa tarkistaa tyypit ja havaitsee, jos käytät kenttiä, joita ei ole olemassa.

Joitakin tyyppejä on vaikeampi käsitellä. Mutta CGO:n mukana tulee joitakin aputoimintoja:

// Go merkkijono C-merkkijonoksi
// C-merkkijono allokoidaan C-kasaan käyttäen mallocia.
// Kutsujan vastuulla on huolehtia siitä, että se on
// vapauttamisesta esimerkiksi kutsumalla C.free (muista sisällyttää tiedostoon stdlib.h).
// jos C.free-tiedostoa tarvitaan).
func C.CString(string) *C.char
// Siirry []tavun viipaleen C-matriisiin.
// C-muotoinen array allokoidaan C:n heapissa käyttäen mallocia.
// Kutsujan vastuulla on huolehtia siitä, että se on
// vapauttamisesta esimerkiksi kutsumalla C.free (muista sisällyttää stdlib.h-tiedosto).
// jos C.free-tiedostoa tarvitaan).
func C.CBytes([]byte) unsafe.Pointer
// C merkkijono Go merkkijonoksi
func C.GoString(*C.char) string
// C-tieto eksplisiittisellä pituudella Go-säikeeksi
func C.GoStringN(*C.char, C.int) string
// C-tieto, jonka pituus on selvä Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

Useimmat niistä ovat melko selkeitä. Minua vain hieman hämmensi C.GoBytesin unsafe.pointer. Tässä on pieni esimerkki:

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

Kuten näet, voimme vain muuntaa C-osoittimen unsafe.pointeriksi. Kun saat turvattoman osoittimen esim. C.CBytesista ja haluat muuntaa sen C-osoittimeksi, voit tehdä sen näin:

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

Mikä tahansa C void-osoitin edustaa Go unsafe.Pointer.

Osoitinaritmetiikan käsittely on hyvin vaarallista mutta myös mahdollista:

func getPayload(packet *C.packet_t) []byte {
       dataPtr := unsafe.Pointer(packet.data)
       // Oletetaan 2 tavun otsikko ennen hyötykuormaa.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       return payload
}

Päätelmä

Siinä kaikki. Olemme nähneet, miten C-koodia kutsutaan Go-koodista ja päinvastoin. On helppoa kääntää muutama C-tiedosto yhdessä Go-koodin kanssa, mutta se on hankalaa, kun sinulla on isompi C-koodipohja. Funktion osoittimien käyttäminen C:ssä on hyvä tapa korvata natiivit C-toteutukset Go-toteutuksilla. Näin voit käyttää funktioita kuten GetTimestamp C:ssä, jotka on toteutettu Go:n funktioilla, joilla voit huijata aikaa testien aikana.

C-rakenteiden osalta sinun on kirjoitettava muunnoskoodi, jolla C-rakenteet muunnetaan Go-rakenteiksi heti, kun haluat käyttää niitä Go:n ohjelman muissa osissa. Suosittelen erottelemaan tiukasti C-funktioita ympäröivän Go-koodin ja siirtämään Go-tyyppejä vain julkisten Go-funktioiden välillä.

Kaikki tyypit paitsi funktio-osoittimet voidaan muuntaa C:n ja Gon välillä, ja jopa funktio-osoittimet voidaan tallentaa vaarallisina osoittimina ja siirtää takaisin ja väkisin molempien maailmojen välillä.

On vielä joitakin aiheita, kuten #cgo liput, jotka on dokumentoitu yllä olevissa linkitetyissä lähteissä, mutta joita ei käsitellä tässä artikkelissa.