Obsah
1. Go, Python a problematika práce s řetězci
2. Funkce pro uvolnění řetězce z paměti naprogramovaná v jazyce Go
3. Explicitní správa paměti v Pythonu aneb odvrácená strana integrace s Go
4. Přímé volání céčkovské funkce free z Pythonu
5. Spojování řetězců s memory leakem vs. volání funkce free
6. Předávání polí mezí Go a Pythonem
7. Funkce naprogramovaná v Go akceptující céčkové pole a jeho délku
8. Předání pole z céčka do funkce naprogramované v Go
9. Předání pole z Pythonu do funkce naprogramované v Go
10. Vytvoření pole ze seznamu hodnot
11. Pole s prvky typu double, resp. float64
13. Předávání struktur mezi Go, céčkem a Pythonem
14. Funkce naprogramovaná v Go akceptující parametr typu struktura
15. Zavolání Go funkce z céčka s předáním struktury
16. Zavolání Go funkce z Pythonu s předáním struktury
17. Repositář s demonstračními příklady
1. Go, Python a problematika práce s řetězci
V úvodním článku věnovaném volání funkcí naprogramovaných v jazyce Go z Pythonu jsme si mj. ukázali i funkci, která akceptuje dva řetězce (konkrétně céčkovské řetězce), převede tyto řetězce na datový typ string v Go, řetězce spojí, převede výsledek zpět na céčkovský řetězec a ten vrátí, přesněji řečeno vrátí ukazatel na první znak tohoto nového řetězce. V této funkci je logická chyba, která není odhalena překladačem. Tato chyba spočívá v tom, že se alokovaný céčkovský řetězec neuvolňuje z paměti:
package main import "C" //export concat func concat(text1, text2 *C.char) *C.char { t1 := C.GoString(text1) t2 := C.GoString(text2) result := t1 + t2 return C.CString(result) } func main() {}
Překlad do dynamické knihovny:
$ go build -buildmode=c-shared -o so7.so so7.go
To, že k uvolňování naalokované paměti skutečně nedochází, si můžeme ověřit následujícím skriptem naprogramovaným v jazyku Python. Tento skript nejprve vytvoří dva dlouhé (korektně zakódované) řetězce, které postupně spojuje a vypisuje délku výsledného řetězce. Celý skript může vypadat následovně:
import ctypes import time so7 = ctypes.CDLL("./so7.so") t1 = ("ěščř ЩжΛλ"*10000).encode("utf-8") t2 = ("<foobar>"*10000).encode("utf-8") so7.concat.restype = ctypes.c_char_p for i in range(100000): t = so7.concat(t1, t2) print(len(t)) time.sleep(0.01)
2. Funkce pro uvolnění řetězce z paměti naprogramovaná v jazyce Go
O uvolnění paměti alokované (i když poněkud nenápadně) funkcí C.Cstring se tedy musíme postarat sami. Pro tento účel vytvoříme novou funkci nazvanou například freeString, které se předá céčkový řetězec (tedy ukazatel na první znak v řetězci). Tato funkce zavolá standardní céčkovskou funkci free. Je ovšem nutné podotknout, že je zapotřebí několika triků. První trik spočívá v tom, že se použije strukturovaný komentář:
// #include <stdlib.h>
tento komentář se předá cgo při překladu a dělá přesně to, co je zde napsáno. Tento řádek je nutné použít proto, aby bylo možné zavolat funkci free přes C.free (víme již, že C je pseudobalíček).
Další trik spočívá v konverzi běžného ukazatele v Go na „ne-bezpečný“ céčkový ukazatel, který se předá standardní céčkovské funkci free:
C.free(unsafe.Pointer(s))
Nová podoba zdrojového kódu, který se bude překládat do dynamické knihovny, bude vypadat následovně:
package main // #include <stdlib.h> import "C" import "unsafe" //export concat func concat(text1, text2 *C.char) *C.char { t1 := C.GoString(text1) t2 := C.GoString(text2) result := t1 + t2 return C.CString(result) } //export freeString func freeString(s *C.char) { C.free(unsafe.Pointer(s)) } func main() {}
Překlad do dynamické knihovny již známe:
$ go build -buildmode=c-shared -o so8.so so8.go
3. Explicitní správa paměti v Pythonu aneb odvrácená strana integrace s Go
Samotná existence funkce freeString nám pochopitelně nijak kouzelně nepomůže vyřešit problém s uvolňováním paměti – budeme muset tuto funkci volat explicitně. Je to ukázáno v dalším skriptu, který získá ukazatel vracený funkcí concat, přetypováním a přístupem k atributu value získá řetězec, který vypíše a následně původní blok paměti (což je, jak již víme, céčkový řetězec) uvolní zavoláním funkce freeString:
import ctypes so8 = ctypes.CDLL("./so8.so") t1 = "ěščř ЩжΛλ".encode("utf-8") t2 = "<foobar>".encode("utf-8") so8.concat.restype = ctypes.POINTER(ctypes.c_char) ptr = so8.concat(t1, t2) val = ctypes.cast(ptr, ctypes.c_char_p).value print(val.decode("utf-8")) so8.freeString(ptr)
V úvodní kapitole byl návratový typ funkce concat specifikován takto:
so7.concat.restype = ctypes.c_char_p
Tento datový typ nelze v novém skriptu použít, neboť ctypes se nám v tomto případě snaží pomoci a namísto ukazatele pro nás vytvoří Pythonovský řetězec (a původní ukazatel je ztracen → memory leak). Musíme tedy namísto toho použít:
so8.concat.restype = ctypes.POINTER(ctypes.c_char)
…což je zdánlivě to samé, ovšem tentokrát již ctypes nebude na pozadí provádět žádné další nepředložené operace.
4. Přímé volání céčkovské funkce free z Pythonu
Ve skutečnosti nemusíme v jazyce Go vytvářet funkci freeString ani žádnou podobnou funkci, protože s využitím ctypes je možné standardní céčkovskou funkci free volat přímo z Pythonu. Nejprve ovšem musíme standardní knihovnu (libc) najít a načíst. K tomu nám dopomůže pomocná funkce ctypes.util.find_library:
import ctypes.util libc = ctypes.CDLL(ctypes.util.find_library('c'))
Následně můžeme (pro jistotu) specifikovat, jakého typu je jediný argument této funkce:
libc.free.argtypes = (ctypes.c_void_p,)
Poté je již možné funkci free zavolat a předat jí libovolný platný céčkovský ukazatel (což je obecně odlišné od Pythonovské reference!):
libc.free(ptr)
Upravený skript vypadá následovně:
import ctypes import ctypes.util libc = ctypes.CDLL(ctypes.util.find_library('c')) libc.free.argtypes = (ctypes.c_void_p,) so8 = ctypes.CDLL("./so8.so") t1 = "ěščř ЩжΛλ".encode("utf-8") t2 = "<foobar>".encode("utf-8") so8.concat.restype = ctypes.POINTER(ctypes.c_char) ptr = so8.concat(t1, t2) val = ctypes.cast(ptr, ctypes.c_char_p).value print(val.decode("utf-8")) libc.free(ptr)
5. Spojování řetězců s memory leakem vs. volání funkce free
Jen pro úplnost se podívejme na spojování řetězců s memory leakem (bez uvolnění paměti):
import ctypes import time so8 = ctypes.CDLL("./so8.so") t1 = ("ěščř ЩжΛλ"*10000).encode("utf-8") t2 = ("<foobar>"*10000).encode("utf-8") so8.concat.restype = ctypes.POINTER(ctypes.c_char) for i in range(100000): ptr = so8.concat(t1, t2) val = ctypes.cast(ptr, ctypes.c_char_p).value print(len(val)) time.sleep(0.001)
Se skriptem, který uvolnění paměti korektně provádí:
import ctypes, ctypes.util import time so8 = ctypes.CDLL("./so8.so") libc = ctypes.CDLL(ctypes.util.find_library('c')) libc.free.argtypes = (ctypes.c_void_p,) t1 = ("ěščř ЩжΛλ"*10000).encode("utf-8") t2 = ("<foobar>"*10000).encode("utf-8") so8.concat.restype = ctypes.POINTER(ctypes.c_char) for i in range(100000): ptr = so8.concat(t1, t2) val = ctypes.cast(ptr, ctypes.c_char_p).value print(len(val)) libc.free(ptr) time.sleep(0.001)
6. Předávání polí mezí Go a Pythonem
Ve druhé části dnešního článku se budeme zabývat další důležitou a velmi často dotazovanou problematikou. Konkrétně se jedná o předávání polí mezi programovacím jazykem Go a Pythonem. Zde opět velmi brzy narazíme na rozdíly mezi datovými typy jazyka Go a Pythonu, kde Go podporuje skutečná pole (ty ovšem ve skutečnosti nepoužijeme), kdežto v Pythonu se (pokud se omezíme na základní jazyk, nikoli například na knihovnu Numpy) pracuje spíše se seznamy. Proto je zapotřebí na straně Pythonu používat správné datové typy a explicitně konvertovat seznamy na pole. Ovšem situace je problematická i na straně jazyka Go, protože je nutné předávat i délku pole – data se totiž budou předávat přes céčková pole, což je (opět) ukazatel na první prvek pole, tentokrát ovšem zcela bez informace o počtu prvků (pochopitelně za předpokladu, že pole neobsahuje nějaký prvek sloužící jako „zarážka“).
7. Funkce naprogramovaná v Go akceptující céčkové pole a jeho délku
Podívejme se nyní na to, jakým způsobem by bylo možné v jazyku Go vytvořit funkci, která bude akceptovat céčkovské pole celých čísel a ve druhém parametru i délku pole. Pole se předává přes ukazatel (tedy referencí, nikoli hodnotou):
func sum(values *C.int, length int) int64 { ... ... ...
Jakmile známe ukazatel na pole a jeho délku, můžeme vytvořit řez (slice), s nímž se pracuje mnohem lépe, než s pouhými poli (interně je však pole stále použito):
slice := unsafe.Slice(values, length)
Funkce následně vypočte a vrátí součet všech prvků tohoto pole.
var sum int64 = 0 for _, value := range slice { sum += int64(value) } return sum
Úplný zdrojový kód tohoto demonstračního příkladu bude vypadat takto:
package main import "C" import "unsafe" //export sum func sum(values *C.int, length int) int64 { var sum int64 = 0 slice := unsafe.Slice(values, length) for _, value := range slice { sum += int64(value) } return sum } func main() {}
Překlad do dynamické knihovny již známe:
$ go build -buildmode=c-shared -o so9.so so9.go
Po překladu výše uvedeného kódu do dynamické knihovny získáme mj. i hlavičkový soubor, který bude obsahovat i hlavičku funkce volatelnou z céčka či z například z Pythonu přes ctypes či cffi:
#ifdef __cplusplus extern "C" { #endif extern GoInt64 sum(int* values, GoInt length); #ifdef __cplusplus
Celý vygenerovaný hlavičkový soubor pak vypadá následovně:
/* Code generated by cmd/cgo; DO NOT EDIT. */ /* package command-line-arguments */ #line 1 "cgo-builtin-export-prolog" #include <stddef.h> /* for ptrdiff_t below */ #ifndef GO_CGO_EXPORT_PROLOGUE_H #define GO_CGO_EXPORT_PROLOGUE_H #ifndef GO_CGO_GOSTRING_TYPEDEF typedef struct { const char *p; ptrdiff_t n; } _GoString_; #endif #endif /* Start of preamble from import "C" comments. */ /* End of preamble from import "C" comments. */ /* Start of boilerplate cgo prologue. */ #line 1 "cgo-gcc-export-header-prolog" #ifndef GO_CGO_PROLOGUE_H #define GO_CGO_PROLOGUE_H typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64; typedef GoInt64 GoInt; typedef GoUint64 GoUint; typedef __SIZE_TYPE__ GoUintptr; typedef float GoFloat32; typedef double GoFloat64; typedef float _Complex GoComplex64; typedef double _Complex GoComplex128; /* static assertion to make sure the file is being used on architecture at least with matching size of GoInt. */ typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; #ifndef GO_CGO_GOSTRING_TYPEDEF typedef _GoString_ GoString; #endif typedef void *GoMap; typedef void *GoChan; typedef struct { void *t; void *v; } GoInterface; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; #endif /* End of boilerplate cgo prologue. */ #ifdef __cplusplus extern "C" { #endif extern GoInt64 sum(int* values, GoInt length); #ifdef __cplusplus } #endif
8. Předání pole z céčka do funkce naprogramované v Go
Pro lepší pochopení toho, jak má vlastně vypadat pole předávané do funkce naprogramované v Go si nejprve ukažme, jak by zavolání této funkce bylo realizováno v ANSI C. Konkrétně budeme chtít zavolat tuto funkci:
extern GoInt64 sum(int* values, GoInt length);
Deklarace typů GoInt64 a GoInt je součástí výše uvedeného hlavičkového souboru (použijeme je dále):
typedef long long GoInt64; typedef GoInt64 GoInt;
Způsob otevření dynamické knihovny, získání ukazatele na pojmenovanou funkci (či symbol) a zavolání této funkce jsme si již vysvětlili minule. V následujícím příkladu se ovšem bude několik věcí odlišovat. Především ukazatel na volanou funkci má odlišný typ, a to konkrétně tento:
GoInt64 (*sum)(int* values, GoInt length);
Lišit se bude i způsob volání této funkce poté, co na ni získáme ukazatel:
int ret; int input[] = {1,2,3,4}; printf("address for 'sum' retrieved: %p\n", (void*)sum); puts("Calling 'sum'..."); ret = sum(input, sizeof(input)/sizeof(int)); printf("...called, return value: %d\n", ret);
Úplný zdrojový kód příkladu naprogramovaného v céčku bude vypadat takto:
#include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include "so9.h" int main() { void *library; GoInt64 (*sum)(int* values, GoInt length); /* pokus o otevreni a nacteni sdilene knihovny */ library = dlopen("./so9.so", RTLD_LAZY); if (library != NULL) { printf("dynamic library loaded: %p\n", library); } else { puts("unable to load dynamic library"); return 1; } sum = dlsym(library, "sum"); if (sum != NULL) { int ret; int input[] = {1,2,3,4}; printf("address for 'sum' retrieved: %p\n", (void*)sum); puts("Calling 'sum'..."); ret = sum(input, sizeof(input)/sizeof(int)); printf("...called, return value: %d\n", ret); } else { puts("unable to retrieve address for 'sum'"); } /* pokus o uzavreni sdilene knihovny */ if (library != NULL) { int err = dlclose(library); if (err != 0) { puts("unable to close dynamic library"); return 1; } else { puts("dynamic library closed"); } } return EXIT_SUCCESS; }
Překlad:
$ gcc -ansi -Wall use_so9.c -ldl
Otestování, že funkci z dynamické knihovny lze skutečně zavolat:
dynamic library loaded: 0x56256b1872c0 address for 'sum' retrieved: 0x7f5b37b769b0 Calling 'sum'... ...called, return value: 10 dynamic library closed
9. Předání pole z Pythonu do funkce naprogramované v Go
Nyní již víme, jak má vypadat pole, které se předá funkci naprogramované v Go. Pokud budeme chtít takové pole předat nikoli z céčka ale z Pythonu, budeme muset využít další vlastnosti knihovny ctypes. Konkrétně se bude jednat o definici nového typu „čtyři hodnoty typu int“ a vytvoření hodnoty tohoto typu. To lze provést následovně:
IntArray = ctypes.c_int * 4 array = IntArray(1, 2, 3, 4)
Proměnná array obsahuje referenci na inicializované pole kompatibilní s céčkem, čehož můžeme ihned využít:
print(so9.sum(array, 4))
Celý skript bude vypadat následovně:
import ctypes so9 = ctypes.CDLL("./so9.so") IntArray = ctypes.c_int * 4 array = IntArray(1, 2, 3, 4) so9.sum.restype = ctypes.c_int64 print(so9.sum(array, 4))
Otestování funkčnosti:
$ python3 use_so9A.py 10
10. Vytvoření pole ze seznamu hodnot
Častěji se však setkáme s požadavkem na převedení seznamu hodnot na céčkovské pole. I to je pochopitelně možné, a to způsobem ukázaným v dalším skriptu:
import ctypes so9 = ctypes.CDLL("./so9.so") values = [1, 2, 3, 4, 5] IntArray = ctypes.c_int * len(values) array = IntArray(*values) so9.sum.restype = ctypes.c_int64 print(so9.sum(array, 4))
Otestování funkčnosti:
$ python3 use_so9B.py 10
Ještě si pro jistotu vyzkoušejme, že se skutečně musí jednat o seznam celých čísel:
import ctypes so9 = ctypes.CDLL("./so9.so") values = [1, "Foo", True, 4, None] IntArray = ctypes.c_int * len(values) array = IntArray(*values) so9.sum.restype = ctypes.c_int64 print(so9.sum(array, 4))
Nyní při pokusu o spuštění dostaneme chybové hlášení:
$ python3 use_so9C.py Traceback (most recent call last): File "use_so9C.py", line 8, in <module> array = IntArray(*values) TypeError: an integer is required (got type str)
11. Pole s prvky typu double resp. float64
Pro úplnost si ještě ukažme funkci, která pro vstupní pole hodnot typu double (což v Go odpovídá datovému typu float64) vypočte průměrnou hodnotu. Pokud je však pole prázdné, vrátí se hodnota –1 (tím mj. zabráníme dělení nulou). V jazyce Go bude vytvoření takové funkce snadné:
package main import "C" import "unsafe" //export average func average(values *C.double, length int) float64 { if length == 0 { return -1 } var sum float64 = 0 slice := unsafe.Slice(values, length) for _, value := range slice { sum += float64(value) } return sum / float64(length) } func main() {}
Překlad do dynamické knihovny již známe, takže jen ve stručnosti:
$ go build -buildmode=c-shared -o so10.so so10.go
Hlavička funkce volatelná z C či Pythonu:
#ifdef __cplusplus extern "C" { #endif extern GoFloat64 average(double* values, GoInt length); #ifdef __cplusplus
A vygenerovaný hlavičkový soubor:
/* Code generated by cmd/cgo; DO NOT EDIT. */ /* package command-line-arguments */ #line 1 "cgo-builtin-export-prolog" #include <stddef.h> /* for ptrdiff_t below */ #ifndef GO_CGO_EXPORT_PROLOGUE_H #define GO_CGO_EXPORT_PROLOGUE_H #ifndef GO_CGO_GOSTRING_TYPEDEF typedef struct { const char *p; ptrdiff_t n; } _GoString_; #endif #endif /* Start of preamble from import "C" comments. */ /* End of preamble from import "C" comments. */ /* Start of boilerplate cgo prologue. */ #line 1 "cgo-gcc-export-header-prolog" #ifndef GO_CGO_PROLOGUE_H #define GO_CGO_PROLOGUE_H typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64; typedef GoInt64 GoInt; typedef GoUint64 GoUint; typedef __SIZE_TYPE__ GoUintptr; typedef float GoFloat32; typedef double GoFloat64; typedef float _Complex GoComplex64; typedef double _Complex GoComplex128; /* static assertion to make sure the file is being used on architecture at least with matching size of GoInt. */ typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; #ifndef GO_CGO_GOSTRING_TYPEDEF typedef _GoString_ GoString; #endif typedef void *GoMap; typedef void *GoChan; typedef struct { void *t; void *v; } GoInterface; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; #endif /* End of boilerplate cgo prologue. */ #ifdef __cplusplus extern "C" { #endif extern GoFloat64 average(double* values, GoInt length); #ifdef __cplusplus } #endif
12. Předání pole z Pythonu
V případě, že budeme chtít výše uvedenou funkci average volat z Pythonu častěji, je vhodné si vytvořit pomocnou funkci konstruující céčkové pole z předaných hodnot:
def c_array(values): ArrayType = ctypes.c_double * len(values) return ArrayType(*values)
Použití takové funkce je již triviální:
import ctypes so10 = ctypes.CDLL("./so10.so") def c_array(values): ArrayType = ctypes.c_double * len(values) return ArrayType(*values) so10.average.restype = ctypes.c_double v1 = [] print(so10.average(c_array(v1), len(v1))) v2 = [1] print(so10.average(c_array(v2), len(v2))) v3 = [1, 2] print(so10.average(c_array(v3), len(v3))) v4 = [1, 2, 3, 4] print(so10.average(c_array(v4), len(v4)))
Po spuštění tohoto skriptu dostaneme následující výsledky:
-1.0 1.0 1.5 2.5
13. Předávání struktur mezi Go, céčkem a Pythonem
Třetí část dnešního článku je věnována již relativně dosti složité problematice – konkrétně tomu, jak se předávají struktury (záznamy) mezi kódem psaným v jazyce Go a Pythonem, přičemž „mezikrokem“ jsou v tomto případě céčkovské datové struktury. Jádro problému spočívá v tom, že je nutné znát pravidla pro zarovnání prvků v datových strukturách (align). Nejprve se budeme zabývat tím nejjednodušším případem, konkrétně tím, že prvky struktury budou mít takovou šířku, že nedojde k jejich zarovnání, resp. přesněji řečeno se mezi prvky nevloží žádná výplň (což se týká použitého datového typu double).
14. Funkce naprogramovaná v Go akceptující parametr typu struktura
V případě, že budeme chtít v programovacím jazyce Go vytvořit funkci akceptující jako svůj parametr (céčkovou) strukturu, je nutné tuto strukturu nejdříve popsat přímo v céčku, a to v komentáři:
/* struct Vector { double X; double Y; }; */
S využitím tohoto triku se v pseudobalíčku C objeví nový datový typ pojmenovaný struct_Vector (což je získáno z prvního řádku deklarace. Můžeme tedy vytvořit funkci, která bude strukturu akceptovat jako svůj parametr a vrátí vypočtenou délku vektoru. Taková funkce může vypadat například takto:
//export length func length(v C.struct_Vector) C.double { r := C.double(math.Sqrt(float64(v.X*v.X) + float64(v.Y*v.Y))) fmt.Printf("%f %f %f\n", float64(v.X), float64(v.Y), r) return r }
Celý zdrojový kód pak vypadá následovně:
package main import "math" import "fmt" /* struct Vector { double X; double Y; }; */ import "C" //export length func length(v C.struct_Vector) C.double { r := C.double(math.Sqrt(float64(v.X*v.X) + float64(v.Y*v.Y))) fmt.Printf("%f %f %f\n", float64(v.X), float64(v.Y), r) return r } func main() {}
Po překladu zdrojového kódu do dynamické knihovny:
$ go build -buildmode=c-shared -o so11.so so11.go
…bude důležité zjistit, jaké informace byly zahrnuty do vygenerovaného hlavičkového souboru so11.h. V první řadě zde nalezneme deklaraci struktury typu Vector:
#line 6 "so11.go" struct Vector { double X; double Y; };
A v řadě druhé pak i hlavičku funkce length:
#ifdef __cplusplus extern "C" { #endif extern double length(struct Vector v); #ifdef __cplusplus
Celý hlavičkový soubor vypadá takto:
/* Code generated by cmd/cgo; DO NOT EDIT. */ /* package command-line-arguments */ #line 1 "cgo-builtin-export-prolog" #include <stddef.h> /* for ptrdiff_t below */ #ifndef GO_CGO_EXPORT_PROLOGUE_H #define GO_CGO_EXPORT_PROLOGUE_H #ifndef GO_CGO_GOSTRING_TYPEDEF typedef struct { const char *p; ptrdiff_t n; } _GoString_; #endif #endif /* Start of preamble from import "C" comments. */ #line 6 "so11.go" struct Vector { double X; double Y; }; #line 1 "cgo-generated-wrapper" /* End of preamble from import "C" comments. */ /* Start of boilerplate cgo prologue. */ #line 1 "cgo-gcc-export-header-prolog" #ifndef GO_CGO_PROLOGUE_H #define GO_CGO_PROLOGUE_H typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64; typedef GoInt64 GoInt; typedef GoUint64 GoUint; typedef __SIZE_TYPE__ GoUintptr; typedef float GoFloat32; typedef double GoFloat64; typedef float _Complex GoComplex64; typedef double _Complex GoComplex128; /* static assertion to make sure the file is being used on architecture at least with matching size of GoInt. */ typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; #ifndef GO_CGO_GOSTRING_TYPEDEF typedef _GoString_ GoString; #endif typedef void *GoMap; typedef void *GoChan; typedef struct { void *t; void *v; } GoInterface; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; #endif /* End of boilerplate cgo prologue. */ #ifdef __cplusplus extern "C" { #endif extern double length(struct Vector v); #ifdef __cplusplus } #endif
15. Zavolání Go funkce z céčka s předáním struktury
Před popisem práce se strukturami v Pythonu si opět ukážeme, jak se funkce naprogramovaná v Go zavolá z céčka. Budeme postupovat nám již známým způsobem, tedy otevřením a načtením dynamické knihovny a použitím následujícího ukazatele, ke kterému přiřadíme adresu vstupního bodu do přeložené funkce length:
double (*length)(struct Vector v);
Po získání ukazatele na funkci si jen připravíme strukturu se vstupními daty, funkci zavoláme a zobrazíme výsledek:
struct Vector v; v.X = 1.0; v.Y = 1.0; double ret; printf("address for 'length' retrieved: %p\n", (void*)length); puts("Calling 'length'..."); ret = length(v); printf("...called, vector length: %f\n", ret);
Otestování funkcionality:
dynamic library loaded: 0x55f7dafc12c0 address for 'length' retrieved: 0x7f10c9162b50 Calling 'length'... 1.000000 1.000000 1.414214 ...called, vector length: 1.414214 dynamic library closed
Pro jistotu si ukažme úplný zdrojový kód celého příkladu:
#include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include "so11.h" int main() { void *library; double (*length)(struct Vector v); /* pokus o otevreni a nacteni sdilene knihovny */ library = dlopen("./so11.so", RTLD_LAZY); if (library != NULL) { printf("dynamic library loaded: %p\n", library); } else { puts("unable to load dynamic library"); return 1; } length = dlsym(library, "length"); if (length != NULL) { struct Vector v; v.X = 1.0; v.Y = 1.0; double ret; printf("address for 'length' retrieved: %p\n", (void*)length); puts("Calling 'length'..."); ret = length(v); printf("...called, vector length: %f\n", ret); } else { puts("unable to retrieve address for 'length'"); } /* pokus o uzavreni sdilene knihovny */ if (library != NULL) { int err = dlclose(library); if (err != 0) { puts("unable to close dynamic library"); return 1; } else { puts("dynamic library closed"); } } return EXIT_SUCCESS; }
16. Zavolání Go funkce z Pythonu s předáním struktury
Volání funkce akceptující strukturu z Pythonu lze realizovat poměrně elegantním způsobem. Samotný jazyk Python sice datový typ „struktura“ nepodporuje, ovšem strukturu lze v tomto případě nahradit vhodně navrženou třídou. Tato třída je odvozena od třídy ctypes.Structure a musí mít definovány atributy odpovídající jak jménem, tak i typem svému céčkovému protějšku (a nepřímo tak i protějšku napsaném v Go). V našem konkrétním případě může tato třída vypadat takto:
class Vector(ctypes.Structure): _fields_ = [("X", ctypes.c_double), ("Y", ctypes.c_double)]
Realizace skriptu, který zavolá funkci length naprogramovanou původně v Go, tedy může vypadat následovně:
import ctypes so11 = ctypes.CDLL("./so11.so") so11.length.restype = ctypes.c_double class Vector(ctypes.Structure): _fields_ = [("X", ctypes.c_double), ("Y", ctypes.c_double)] v = Vector(1.0, 1.0) print(so11.length(v))
17. Repositář s demonstračními příklady
Zdrojové kódy všech minule i dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
18. Odkazy na Internetu
- ctypes – A foreign function library for Python
https://docs.python.org/3/library/ctypes.html - Kooperace mezi kódem psaným v Go a C: cgo
https://www.root.cz/clanky/kooperace-mezi-kodem-psanym-v-go-a-c-cgo/ - cgo – Introduction
https://zchee.github.io/golang-wiki/cgo/ - Introduction to cgo (wiki)
https://github.com/golang/go/wiki/cgo - C? Go? Cgo!
https://go.dev/blog/cgo - ctypes return a string from c function
https://newbedev.com/ctypes-return-a-string-from-c-function - cgo: Passing a slice/array pointer between Go/C
https://groups.google.com/g/Golang-Nuts/c/DXCIP6pMMN0 - cgo-struct-array
https://github.com/llgoer/cgo-struct-array - C structs and Pointers
https://www.programiz.com/c-programming/c-structures-pointers - Pass struct and array of structs to C function from Go
https://stackoverflow.com/questions/19910647/pass-struct-and-array-of-structs-to-c-function-from-go - dlopen(3) — Linux manual page
https://man7.org/linux/man-pages/man3/dlopen.3.html - dlclose(3p) — Linux manual page
https://man7.org/linux/man-pages/man3/dlclose.3p.html - dlsym(3) — Linux manual page
https://man7.org/linux/man-pages/man3/dlsym.3.html - How to correctly assign a pointer returned by dlsym into a variable of function pointer type?
https://stackoverflow.com/questions/36384195/how-to-correctly-assign-a-pointer-returned-by-dlsym-into-a-variable-of-function - Faster Python with Go shared objects (the easy way)
https://blog.kchung.co/faster-python-with-go-shared-objects/ - Programovací jazyk Rust: použití FFI pro volání funkcí z nativních knihoven
https://www.root.cz/clanky/programovaci-jazyk-rust-pouziti-ffi-pro-volani-funkci-z-nativnich-knihoven/ - Programovací jazyk Rust: použití FFI pro volání funkcí z nativních knihoven (2. část)
https://www.root.cz/clanky/programovaci-jazyk-rust-pouziti-ffi-pro-volani-funkci-z-nativnich-knihoven-2-cast/ - Programovací jazyk Rust: použití FFI při předávání struktur
https://www.root.cz/clanky/programovaci-jazyk-rust-pouziti-ffi-pri-predavani-struktur/ - GNU C Library: Integers
https://www.gnu.org/software/libc/manual/html_node/Integers.html - Position-independent code
https://cs.wikipedia.org/wiki/Position-independent_code - Creating a shared and static library with the gnu compiler [gcc]
http://www.adp-gmbh.ch/cpp/gcc/create_lib.html - FFI: Foreign Function Interface
https://doc.rust-lang.org/book/ffi.html - Primitive Type pointer
https://doc.rust-lang.org/std/primitive.pointer.html