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.