Programowanie wbudowane z C i GoLang (CGO)

Cześć, nie jestem programistą embedded. Moje doświadczenie w kodowaniu opiera się na C#, Javie, Go Desktop i rozwoju serwerów. Niedawno w Lobaro stanąłem przed kilkoma wyzwaniami w programowaniu Embedded C i zauważyłem - po raz kolejny - ogromną różnicę między świętą krainą zarządzania pamięcią a labiryntem wskaźników C, w którym trzeba dwa razy pomyśleć o każdym wywołaniu malloc. Kod C jest często mało generyczny, a przez to mniej użyteczny. Nie ma czegoś takiego jak moduły, ale kilka globalnych rzeczy, które mają tendencję do łamania innych globalnych rzeczy. Ludzie rzadko testują kod C, a jeśli to robią, robią to na sprzęcie, dla którego został napisany, co prawie uniemożliwia korzystanie z jakiejkolwiek infrastruktury CI.

A potem jest Go. Go jest jak C, ale z rozwiązaniami większości problemów związanych z C. Przede wszystkim Garbage collector i inteligentne zarządzanie wskaźnikami. To miłe, ale także powód, dla którego nigdy nie uruchomisz programu Go na swoim mikrokontrolerze ARM, mimo że Go kompiluje się do natywnego kodu ARM. Aby wspomnieć o kilku innych problematycznych obszarach, które zostały ładnie rozwiązane: świetna reużywalność wspierana przez ładny system modułów, współbieżność po prostu działa, bardzo mało problemów w czasie wykonywania dzięki ścisłemu statycznemu systemowi typów (nawet nie generycznych), a dla wszystkich przyjaciół C ma Strukty, które można powiązać z funkcjami. A potem są dwie najważniejsze cechy tego artykułu: (1) Testowanie jest wbudowane. Jest to tak proste, jak umieszczenie Test* w funkcję *_test.go plik i wykonać przejdź test. (2) Za pomocą CGO można wywoływać kod C z Go i odwrotnie.

Przyjrzyjmy się, jak to działa. Dobrym punktem wyjścia jest Dokumentacja CGOw Go Wiki i to Artykuł na blogu. Lubię skupiać się na temacie, z którym się zmagałem, nawet po przeczytaniu podlinkowanych stron.

W kolejnych sekcjach chciałbym przyjrzeć się temu bliżej:

  • Wywołanie kodu C z GO
  • Wywoływanie kodu Go z języka C
  • Kompilowanie kodu C z plików
  • Radzenie sobie ze wskaźnikami funkcji
  • Konwertowanie typów, struktury dostępu i arytmetyka wskaźnikowa

Wywoływanie kodu C z Go

pakiet main
/*
// Wszystko w komentarzach powyżej importu "C" jest kodem C i będzie kompilowane przez GCC.
// Upewnij się, że masz zainstalowane 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("Dodaj w C:", a, "+", b, "=", int(c))
}

Na tej podstawie możemy już wiele zobaczyć:

  • import "C" zawiera specjalny pakiet, który nakazuje narzędziu Go kompilację z CGO. Komentarze bezpośrednio powyżej tego importu są kompilowane do C przy użyciu GCC.
  • Możemy po prostu zdefiniować funkcję C, taką jak addInC i wywołać ją przy użyciu pakietu C.
  • Typy C, takie jak int, ale także struktury, które definiujesz, mogą być dostępne za pośrednictwem pakietu C. W ten sposób możemy po prostu przekonwertować nasz Go int na C.int i z powrotem.
    • Konwersja wewnątrz Println nie jest konieczna, ale sprawia, że przykład jest bardziej przejrzysty.

Wywoływanie kodu Go z języka C

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

Ten kod działa, ale wymaga szczególnej uwagi. Po pierwsze jest specjalny //eksport nad funkcją Go. Powoduje to, że CGO eksportuje funkcję do oddzielnego pliku C. Prowadzi to jednak do następującego ograniczenia: "jeśli twój program używa dowolnego //eksport wówczas kod C w komentarzu może zawierać tylko deklaracje (external int f();), a nie definicje (int f() { return 1; }). Można użyć //eksport aby funkcje Go były dostępne dla kodu C". Zobacz także: Dokumentacja CGO

To jest powód, dla którego zdefiniowałem multiplyInGo jak statyczny inline. Pozwala to uniknąć problemu ze zduplikowanymi symbolami.

Kompilowanie kodu C z plików

Dobrą wiadomością jest to, że wszystkie pliki C w twoim module są po prostu kompilowane i używane z kodu Go, możesz także użyć #include "my_c_file.h" aby uzyskać deklaracje. Złe wieści są następujące: Nie działa w głównym pakiecie, a kod C nie może mieć żadnych zależności z kodem C poza folderem modułów Go. Włączenie globalnie dostępnych bibliotek, takich jak #include, działa dobrze.

Istnieje jedno możliwe obejście, które polega na dołączeniu plików C z innego folderu. Zawartość pliku C zostanie wstawiona do pliku 1TP11W zestawie i w ten sposób skompilowany. Należy jednak pamiętać o niepożądanych efektach ubocznych i upewnić się, że ten sam kod C nie jest dołączany dwukrotnie.

Radzenie sobie ze wskaźnikami funkcji

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

Powyższy kod ma atrybut początek aby ustawić wskaźnik funkcji mnożący się do go_multiply implementacje. Wymaga to zadeklarowania go_multiply wcześniej, ponieważ CGO kompiluje deklarację do oddzielnego pliku nagłówkowego (_cgo_export.h), którego nie możemy uwzględnić przed wygenerowaniem. Typedef nie jest konieczny, ale sprawia, że przykład jest bardziej przejrzysty. W multiplyWithFp po prostu wywołujemy funkcję przechowywaną we wskaźniku funkcji.

Przypisanie wskaźnika funkcji musi nastąpić w języku C. Poniższy kod nie będzie działał jako zamiennik dla funkcji C.init() połączenie:

C.multiply = C.multiply_f(go_multiply);

Nie powiedzie się z komunikatem "cannot convert go_multiply (type func(C.int, C.int) C.int) to type C.multiply_f".

Konwertowanie typów, struktury dostępu i arytmetyka wskaźnikowa

Dokonaliśmy już konwersji typów pomiędzy int i C.int. To samo działa dla wszystkich innych typów. Jeśli masz struct my_struct_t w C można po prostu użyć z C.my_struct_t w Go. Możesz uzyskać dostęp do wszystkich pól struktur C, a kompilator CGO sprawdzi nawet typy i wykryje, kiedy uzyskujesz dostęp do nieistniejących pól.

Niektóre typy są trudniejsze w obsłudze. CGO posiada jednak kilka funkcji pomocniczych:

// Łańcuch Go do łańcucha C
// Ciąg C jest alokowany na stercie C za pomocą malloc.
// Obowiązkiem wywołującego jest zapewnienie, że zostanie on
// zwolnienie go, na przykład poprzez wywołanie C.free (pamiętaj o dołączeniu stdlib.h
// jeśli C.free jest potrzebne).
func C.CString(string) *C.char
// Przejście []byte slice do tablicy C
// Tablica C jest alokowana na stercie C za pomocą malloc.
// Obowiązkiem wywołującego jest zorganizowanie jej
// jej zwolnienie, np. poprzez wywołanie C.free (należy pamiętać o dołączeniu stdlib.h
// jeśli C.free jest potrzebne).
func C.CBytes([]byte) unsafe.pointer
// Ciąg znaków C na ciąg znaków Go
func C.GoString(*C.char) string
// Dane C o jawnej długości na ciąg Go
func C.GoStringN(*C.char, C.int) string
// Dane C z jawną długością do Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

Większość z nich jest całkiem jasna. Jestem tylko trochę zdezorientowany z unsafe.pointer w C.GoBytes. Oto mały przykład:

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

Jak widać, możemy po prostu przekonwertować nasz wskaźnik C na unsafe.pointer. Jeśli otrzymaliśmy niebezpieczny wskaźnik, np. z C.CBytes i chcemy przekonwertować go na wskaźnik C, możemy to zrobić w następujący sposób:

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

Każdy wskaźnik pustej przestrzeni C jest reprezentowany przez Go unsafe.pointer.

Radzenie sobie z arytmetyką wskaźnikową jest bardzo niebezpieczne, ale również możliwe:

func getPayload(packet *C.packet_t) []byte {
       dataPtr := unsafe.Pointer(packet.data)
       // Załóżmy 2-bajtowy nagłówek przed ładunkiem.
       payload := C.GoBytes(unsafe.Pointer(uintptr(dataPtr)+2), C.int(packet.dataLength-2))
       return payload
}

Wnioski

To wszystko. Zobaczyliśmy, jak wywoływać kod C z Go i odwrotnie. Łatwo jest skompilować kilka plików C wraz z kodem Go, ale staje się to trudne, gdy masz większe bazy kodu C. Używanie wskaźników funkcji w C to świetny sposób na zastąpienie natywnych implementacji C implementacjami Go. W ten sposób można mieć funkcje takie jak GetTimestamp w C, które są zaimplementowane w funkcjach Go, gdzie można zaoszczędzić czas podczas testów.

W przypadku struktur C będziesz musiał napisać kod konwersji, aby przekonwertować struktury C na struktury Go, gdy tylko zechcesz użyć ich w innych częściach programu Go. Zalecam ścisłe oddzielenie kodu Go, który opakowuje funkcje C i przekazywanie typów Go tylko między publicznymi funkcjami Go.

Wszystkie typy oprócz wskaźników funkcji mogą być konwertowane między C i Go, a nawet wskaźniki funkcji mogą być przechowywane jako unsafe.pointer i przekazywane z powrotem do obu światów.

Nadal istnieją pewne tematy, takie jak #cgo flagi, które są udokumentowane w powiązanych zasobach powyżej, ale nie zostały omówione w tym artykule.