Vestavěný vývoj v jazycích C a GoLang (CGO)

Ahoj, nejsem embedded vývojář. Moje zkušenosti s kódováním jsou založeny na vývoji C#, Javy, Go Desktop a serverů. Nedávno jsem na Lobaro čelil několika výzvám v embedded programování v jazyce C a všiml jsem si - opět - obrovského rozdílu mezi svatou zemí správy paměti a labyrintem ukazatelů v jazyce C, kde si musíte každé volání malloc dvakrát rozmyslet. Kód v jazyce C je často málo obecný, a tudíž méně znovupoužitelný. Neexistuje nic takového jako moduly, ale hromada globálních věcí, které mají tendenci rozbíjet jiné globální věci. Lidé kód v jazyce C testují jen zřídka, a pokud ano, tak na hardwaru, pro který je napsán, což téměř znemožňuje použití jakékoli infrastruktury CI.

A pak je tu Go. Go je jako C, ale má řešení většiny problémů, které se vyskytují v C. Především Garbage collector a inteligentní správu ukazatelů. To je hezké, ale také důvod, proč byste nikdy nespustili program v jazyce Go na mikrořadiči ARM, i když se Go kompiluje do nativního kódu ARM. Abych zmínil některé další problémové oblasti, které se pěkně vyřešily: Skvělá znovupoužitelnost podporovaná pěkným systémem modulů, Concurrency prostě funguje, velmi málo problémů při běhu díky přísnému statickému typovému systému (ani generika) a pro všechny přátele jazyka C má Struktury, které lze vázat na funkce. A pak jsou tu dvě nejdůležitější vlastnosti pro tento článek: (1) Testování je zabudované. Je to tak jednoduché, jako vložení příkazu Test* do funkce *_test.go a spustit přejít na testování. (2) Pomocí CGO můžete volat kód v jazyce C z jazyka Go a naopak.

Podívejme se, jak to funguje. Dobrým výchozím bodem je Dokumentace CGO... Přejít na Wiki a toto Článek na blogu. Rád bych se zaměřil na téma, se kterým jsem se potýkal i po přečtení odkazovaných stránek.

V následujících kapitolách se chci blíže věnovat:

  • Volání kódu C ze systému GO
  • Volání kódu Go z jazyka C
  • Kompilace kódu C ze souborů
  • Práce s ukazateli funkcí
  • Převod typů, přístup ke strukturám a aritmetika ukazatelů

Volání kódu C z jazyka Go

balíček main
/*
// Vše v komentářích nad importem "C" je kód v jazyce C a bude zkompilován pomocí GCC.
// Ujistěte se, že máte nainstalované GCC.
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("Add in C:", a, "+", b, "=", int(c))
}

Z toho už je vidět mnohé:

  • import "C" poskytuje speciální paket, který nástroji Go sdělí, že má kompilovat pomocí CGO. Komentáře přímo nad tímto importem se zkompilují do jazyka C pomocí GCC.
  • Můžeme jednoduše definovat funkci C, například addInC, a zavolat ji pomocí balíčku C.
  • K typům jazyka C, jako je int, ale také ke strukturám, které definujete, lze přistupovat prostřednictvím balíčku C. Tímto způsobem můžeme jednoduše převést náš Go int na C.int a zpět.
    • Konverze uvnitř Println není nutná, ale příklad je díky ní přehlednější.

Volání kódu Go z jazyka C

balíček 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
}

Tento kód funguje, ale je třeba mu věnovat zvláštní pozornost. Nejprve je zde speciální //export komentář nad funkcí Go. Ten říká CGO, aby funkci exportoval do samostatného souboru C. To však vede k následujícímu omezení: "pokud váš program používá libovolnou //export pak může kód C v komentáři obsahovat pouze deklarace (externí int f();), nikoli definice (int f() { return 1; }). Můžete použít //export direktivy pro zpřístupnění funkcí jazyka Go kódu jazyka C." Viz také: Dokumentace CGO

To je důvod, proč jsem definoval multiplyInGo jako statické inline. Tím se vyhnete problému s duplicitními symboly.

Kompilace kódu C ze souborů

Dobrou zprávou je, že všechny soubory C ve vašem modulu jsou právě zkompilovány a použitelné z kódu Go, můžete také použít #include "my_c_file.h" získat prohlášení. Špatné zprávy jsou: V hlavním balíčku to nefunguje a váš kód v jazyce C nesmí mít žádné závislosti na kódu v jazyce C mimo složku modulu Go. Zahrnutí globálně dostupných knihoven, jako je #include, funguje dobře.

Existuje jedna možnost, jak to obejít, a to zahrnout soubory C z jiné složky. Obsah souboru C bude inlineován v adresáři 1TP11Včetně a takto zkompilovaný. Dejte si však pozor na nežádoucí vedlejší účinky a ujistěte se, že stejný kód v jazyce C není obsažen dvakrát.

Práce s ukazateli funkcí

balíček 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
}

Výše uvedený kód má init funkce pro nastavení násobení ukazatele funkce na funkci go_multiply implementace. To vyžaduje deklarovat go_multiply předtím, protože CGO zkompiluje deklaraci do samostatného hlavičkového souboru (_cgo_export.h), které nemůžeme zahrnout před generováním. Typedef není nezbytně nutný, ale příklad je díky němu přehlednější. Na adrese multiplyWithFp jednoduše zavoláme funkci uloženou v ukazateli funkce.

Přiřazení ukazatele na funkci musí proběhnout v jazyce C. Následující kód nebude fungovat jako náhrada za C.init() volání:

C.multiply = C.multiply_f(go_multiply);

Selže s hláškou "nelze převést go_multiply (typ func(C.int, C.int) C.int) na typ C.multiply_f"

Převod typů, přístup ke strukturám a aritmetika ukazatelů

Již jsme provedli konverzi typů int a C.int. Totéž platí pro všechny ostatní typy. Pokud máte strukturu my_struct_t v jazyce C stačí použít C.my_struct_t ve hře Go. Můžete přistupovat ke všem polím struktur C a kompilátor CGO dokonce kontroluje typy a zjistí, kdy přistupujete k neexistujícím polím.

S některými typy je obtížnější pracovat. CGO však obsahuje některé pomocné funkce:

// Řetězec Go na řetězec C
// Řetězec C je alokován na haldě C pomocí malloc.
// Je odpovědností volajícího, aby zajistil, že bude
// uvolnění, například voláním C.free (nezapomeňte zahrnout stdlib.h
// pokud je potřeba C.free).
func C.CString(string) *C.char
// Přejít []byte slice na pole C
// Pole C je alokováno na haldě C pomocí malloc.
// Je odpovědností volajícího, aby zajistil, že bude
// uvolnění, například voláním C.free (nezapomeňte zahrnout stdlib.h
// pokud je C.free potřeba).
func C.CBytes([]byte) unsafe.pointer
// C řetězec na Go řetězec
func C.GoString(*C.char) string
// C data s explicitní délkou na Go řetězec
func C.GoStringN(*C.char, C.int) string
// C data s explicitní délkou do Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

Většina z nich je poměrně jasná. Jen mě trochu zmátl unsafe.pointer v C.GoBytes. Zde je malý příklad:

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

Jak vidíte, stačí převést náš ukazatel C na unsafe.pointer. Když jste získali unsafe ukazatel např. z C.CBytes a chcete ho převést na C ukazatel, můžete to udělat takto:

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

Jakýkoli ukazatel void v jazyce C je reprezentován příkazem Go unsafe.pointer.

Práce s aritmetikou ukazatele je velmi nebezpečná, ale také možná:

func getPayload(packet *C.packet_t) []byte {
       dataPtr := unsafe.Pointer(packet.data)
       // Předpokládejme, že před payloadem je 2bajtová hlavička.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       return payload
}

Závěr

To je vše. Viděli jsme, jak volat kód v jazyce C z jazyka Go a naopak. Je snadné zkompilovat několik souborů v jazyce C spolu s kódem v jazyce Go, ale stane se to složitější, když máte větší základnu kódu v jazyce C. Použití ukazatelů funkcí v jazyce C je skvělým způsobem, jak nahradit nativní implementaci jazyka C implementací v jazyce Go. Tímto způsobem můžete mít funkce jako např. GetTimestamp v jazyce C, které jsou implementovány pomocí funkcí Go, kde můžete během testů falšovat čas.

Pro struktury v jazyce C budete muset napsat konverzní kód, který převede struktury v jazyce C na struktury v jazyce Go, jakmile je budete chtít použít v jiných částech programu v jazyce Go. Doporučuji striktně oddělit kód Go, který obaluje funkce C, a mezi veřejnými funkcemi Go předávat pouze typy Go.

Všechny typy kromě ukazatelů funkcí lze převádět mezi jazyky C a Go a dokonce i ukazatele funkcí lze ukládat jako unsafe.pointer a předávat je mezi oběma světy.

Stále existují některá témata, jako např. #cgo příznaků, které jsou zdokumentovány ve výše uvedených zdrojích, ale nejsou popsány v tomto článku.