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 struktury, 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 również użyć #include "my_c_file.h"
aby uzyskać deklaracje. Złe wiadomoś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 powiodło się "nie można przekonwertować go_multiply (typ func(C.int, C.int) C.int) na typ 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 (należy pamiętać 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 zaaranżowanie, aby została ona // 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. Tylko trochę się pogubiłem 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 niebezpieczne wskaźniki 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.