Obsah
1. Propojení Go s Pythonem s využitím cgo a ctypes
2. Překlad zdrojového kódu Go do dynamické knihovny
4. Malá odbočka: načtení dynamické knihovny a zavolání funkce hello z jazyka C
5. Úplný zdrojový kód programu v C, který volá funkce naprogramované v Go
6. Načtení dynamické knihovny a zavolání funkce hello z jazyka Python
7. Vliv funkce main v knihovně vytvořené v jazyce Go
8. Funkce s argumenty a návratovou hodnotou
10. Použití specifického datového typu Go – int64
11. Explicitní specifikace návratového typu funkce z dynamické knihovny
12. Funkce akceptující parametr obsahující řetězec
13. Otestování funkce akceptující řetězec
14. Funkce naprogramovaná v Go akceptující céčkový řetězec
15. Otestování funkce akceptující céčkový řetězec
16. Funkce naprogramovaná v Go vracející řetězec
19. Repositář s demonstračními příklady
1. Propojení Go s Pythonem s využitím cgo a ctypes
Poměrně často se v praxi (v mém případě konkrétně při vývoji mikroslužeb) můžeme setkat s požadavkem na propojení programového kódu psaného v Pythonu s kódem, který je napsaný v jazyce Go. Tento problém je možné řešit různými způsoby, například tak, že budou existovat dva procesy (každý psaný v jiném jazyce), které spolu budou nějakým způsobem komunikovat (přes sockety, pojmenované roury, POSIXové fronty, REST API atd. atd.), program psaný v Go bude spouštěn z Pythonu jako aplikace ovládaná z příkazového řádku atd. Ovšem ve chvíli, kdy je například z výkonnostních důvodů vyžadována těsnější integrace (časté volání funkcí naprogramovaných v Go z Pythonu), nemusí výše uvedené způsoby postačovat. Jedno z možných řešení, které se vlastně samo nabízí, spočívá v použití dvou technologií – cgo na straně programovacího jazyka Go a ctypes na straně Pythonu.
Při použití kombinace cgo s ctypes je kód napsaný v jazyce Go přeložen do nativní dynamické knihovny (tedy konkrétně do souboru s koncovkou „.so“ na Linuxu a „.dll“ ve Windows). Aplikace psaná v Pythonu tuto dynamickou knihovnu načte a přes balíček ctypes umožní volání funkcí naprogramovaných v Go. Zpočátku se může zdát, že se jedná o bezproblémové řešení, ovšem na cestě k výslednému produktu je nutné zdolat poměrně mnoho překážek. Některé jsou relativně snadné (například ctypes lze nahradit za cffi, pokud to vývojáři více vyhovuje), další již komplikovanější. Tyto problémy spočívají v tom, že se střetávají dva rozdílné typové systémy. Navíc obě technologie předpokládají, že jedna z komunikujících stran je psaná v céčku – a to znamená, že se mezi dva programovací jazyky s automatickou správnou paměti vložilo rozhraní předpokládající manuální správu paměti se všemi z toho plynoucími důsledky.
2. Překlad zdrojového kódu Go do dynamické knihovny
Kooperaci mezi Pythonem a Go si otestujeme na tom nejtypičtějším příkladu; pochopitelně se bude jednat o program typu „Hello, world“. Přitom vlastní výpis této zprávy bude implementován v programovacím jazyku Go, konkrétně ve funkci nazvané hello. Tuto funkci později zavoláme z Pythonu.
Vzhledem k tomu, že má být funkce hello de facto volatelná z céčka (resp. nepřímo přes ctypes), je nutné před její hlavičkou uvést speciální komentář, kterým se specifikuje, pod jakým jménem má být funkce z céčka viditelná:
//export hello func hello() { ... ... ...
Navíc, i když to prozatím nebude nutné, provedeme import speciálního balíčku „C“ Tento balíček můžeme importovat (syntaxe je shodná s běžnými balíčky), a to dokonce bez toho, aby byl vůbec ve zdrojovém kódu reálně použit. To znamená, že následující zdrojový kód je zcela korektní a může být bez chyb přeložen překladačem jazyka Go:
package main import "C" import "fmt" //export hello func hello() { fmt.Println("Hello, world!") } func main() {}
My ovšem namísto vytvoření spustitelného souboru provedeme překlad do dynamické knihovny, a to konkrétně příkazem:
$ go build -buildmode=c-shared -o so1.so so1.go
kde „so1.go“ je jméno zdrojového souboru a „so1.so“ název výsledné dynamické knihovny.
3. Výsledky překladu
Výsledkem překladu je v první řadě hlavičkový soubor nazvaný „so1.h“. Ten je určen například pro to, aby bylo možné volat funkce napsané v Go z programovacího jazyka C popř. C++. V našem případě, kdy budeme chtít volat funkce z Go z Pythonu, není tento soubor přímo využíván použitými nástroji, nicméně i přesto je velmi užitečný, protože kromě dalších informací obsahuje i hlavičku naší funkce (a v dalších příkladech i přesné datové typy argumentů i návratové hodnoty):
#ifdef __cplusplus extern "C" { #endif extern void hello(); #ifdef __cplusplus }
Celý obsah tohoto souboru 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 void hello(); #ifdef __cplusplus } #endif
Dále vznikl soubor „so1.so“, což je ona dynamická knihovna, kterou dále využijeme. Ta je poměrně velká, protože obsahuje celý runtime jazyka Go (včetně správce paměti) a navíc i balíček „fmt“. Závislosti této knihovny jsou jen základní:
$ ldd so1.so
linux-vdso.so.1 (0x00007fffb178c000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6aec0ed000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6aebefb000) /lib64/ld-linux-x86-64.so.2 (0x00007f6aec2cc000)
Ostatně se můžeme sami přesvědčit o tom, zda dynamická knihovna skutečně obsahuje i funkci hello:
$ nm so1.so | grep hello
00000000000c6480 T _cgoexp_c12381a1b464_hello 00000000000fc9c0 d _cgoexp_c12381a1b464_hello.stkobj 00000000000c64f0 T hello
4. Malá odbočka: načtení dynamické knihovny a zavolání funkce hello z jazyka C
Dynamickou knihovnu vytvořenou v jazyce Go je možné načíst do programu napsaného v klasickém céčku. Samotné načtení knihovny, které probíhá v čase běhu programu (tedy v runtime), může vypadat například následovně:
void *library; /* pokus o otevreni a nacteni sdilene knihovny */ library = dlopen("./so1.so", RTLD_LAZY); if (library != NULL) { printf("dynamic library loaded: %p\n", library); } else { puts("unable to load dynamic library"); return 1; }
Pokud k načtení knihovny došlo (tj. soubor s dynamickou knihovnou byl nalezen a má korektní formát), můžeme se pokusit v něm nalézt symbol odpovídající funkci hello:
void *library; hello = dlsym(library, "hello"); /* kontrola na NULL, klasika ... */
V případě, že překládáte s přepínačem -pedantic, je nutné řádek s voláním dlsym upravit, aby byl překladač spokojený s přetypováním ukazatelů:
void (*hello)(); *(void **) (&hello) = dlsym(library, "hello");
Následně je již možné funkci, na kterou jsme získali ukazatel, zavolat:
hello();
A uzavřít dynamickou knihovnu:
if (library != NULL) { int err = dlclose(library); if (err != 0) { puts("unable to close dynamic library"); return 1; } else { puts("dynamic library closed"); } }
Příklad získaný po překladu a spuštění programu:
$ ./a.out dynamic library loaded: 0x5608a19f22c0 address for 'hello' retrieved: 0x7f40606684f0 Calling 'hello'... Hello, world! ...called dynamic library closed
5. Úplný zdrojový kód programu v C, který volá funkce naprogramované v Go
Úplný zdrojový kód programu napsaného v ANSI C, který po svém spuštění načte dynamickou knihovnu a zavolá v ní uloženou funkci hello, může vypadat následovně. Vidíme, že se ve zdrojovém kódu objevuje poměrně velké množství obslužného kódu, kontrol na chyby, které mohou nastat atd. Navíc je interní mechanismus získávání symbolů, překlad adres apod. relativně komplikovaný. Všechny tyto operace jsou u statických knihoven prováděny v době překladu:
#include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include "so1.h" int main() { void *library; void (*hello)(); /* pokus o otevreni a nacteni sdilene knihovny */ library = dlopen("./so1.so", RTLD_LAZY); if (library != NULL) { printf("dynamic library loaded: %p\n", library); } else { puts("unable to load dynamic library"); return 1; } hello = dlsym(library, "hello"); /* pro preklad s --pedantic *(void **) (&hello) = dlsym(library, "hello"); */ if (hello != NULL) { printf("address for 'hello' retrieved: %p\n", (void*)hello); puts("Calling 'hello'..."); hello(); puts("...called"); } else { puts("unable to retrieve address for 'hello'"); } /* 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; }
6. Načtení dynamické knihovny a zavolání funkce hello z jazyka Python
Nyní se vraťme k ústřednímu tématu dnešního článku, tedy k tomu, jakým způsobem je možné dynamickou knihovnu získanou překladem kódu v Go načíst a použít ve skriptu naprogramovaném v Pythonu. K tomuto účelu můžeme použít standardní balíček ctypes, který dynamickou knihovnu načte a automaticky z ní získá symboly volatelných funkcí. Navíc nám umožní relativně jednoduchým způsobem explicitně definovat typy argumentů a typy návratových hodnot volaných funkcí (což si ukážeme dále).
Nejjednodušší skript, který tuto operaci provádí, může vypadat následovně:
import ctypes so1 = ctypes.CDLL("so1.so") so1.hello()
Tento skript předpokládá, že dynamická knihovna „so1.so“ bude nalezena v systémových adresářích popř. v adresářích uložených v proměnné prostředí LD_LIBRARY_PATH:
$ python3 use_so1A.py Traceback (most recent call last): File "use_so1A.py", line 3, in <module> so1 = ctypes.CDLL("so1.so") File "/usr/lib/python3.8/ctypes/__init__.py", line 373, in __init__ self._handle = _dlopen(self._name, mode) OSError: so1.so: cannot open shared object file: No such file or directory $ export LD_LIBRARY_PATH=. $ python3 use_so1A.py Hello, world!
Alternativně je možné uvést cestu ke knihovně přímo v Pythonovském skriptu:
import ctypes so1 = ctypes.CDLL("./so1.so") so1.hello()
S výsledkem:
$ python3 use_so1B.py Hello, world!
7. Vliv funkcí main a init v knihovně vytvořené v jazyce Go
Nyní nepatrně upravíme zdrojový kód naprogramovaný v jazyce Go. Přidáme do něj funkci init a taktéž tělo funkce main:
package main import "C" import "fmt" //export hello func hello() { fmt.Println("Hello, world!") } func init() { fmt.Println("init") } func main() { hello() }
V případě, že tento program přeložíme a spustíme běžným způsobem, bude se chovat jako normální aplikace psaná v Go – init se zavolá automaticky a poté řízení programu vstoupí do funkce main, ze které můžeme volat funkci hello, která není v tomto případě komentářem //export hello nijak dotčena:
$ go run so2.go init Hello, world!
Ovšem nás zajímá kooperace s Pythonem, proto překlad provedeme i následujícím způsobem:
$ go build -buildmode=c-shared -o so2.so so2.go
Dynamickou knihovnu načteme a zavoláme v ní uloženou funkci hello:
import ctypes so2 = ctypes.CDLL("./so2.so") so2.hello()
Ze zobrazeného výsledku je patrné, že se automaticky zavolala i funkce init:
$ python3 use_so2B.py init Hello, world!
8. Funkce s argumenty a návratovou hodnotou
Volání funkce bez argumentů a bez návratové hodnoty je triviální, jak jsme ostatně mohli vidět v předchozích příkladech. Proto si vyzkoušíme, jak bude vypadat volání funkce s několika argumenty a s návratovou hodnotou z Pythonu. To je již složitější operace, neboť se zde střetávají tři typové systémy – systém jazyka Go, systém céčka a konečně systém Pythonu. Nicméně se vraťme k funkci, kterou budeme testovat. Bude se jednat o součet dvou hodnot typu int:
package main import "C" //export add func add(x, y int) int { return x + y } func main() {}
Po překladu do dynamické knihovny:
$ go build -buildmode=c-shared -o so3.so so3.go
…se v hlavičkovém souboru so3.h objeví i hlavička funkce hello:
#ifdef __cplusplus extern "C" { #endif extern GoInt add(GoInt x, GoInt y); #ifdef __cplusplus }
Celý hlavičkový soubor 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 GoInt add(GoInt x, GoInt y); #ifdef __cplusplus } #endif
9. Volání funkce z Pythonu
Novou nativní funkci add si otestujeme. Nejprve s „rozumnými“ hodnotami:
import ctypes so3 = ctypes.CDLL("./so3.so") a = 1 b = 2 c = so3.add(a, b) print(c)
Výsledkem bude podle očekávání hodnota 3.
Dále se funkci pokusíme předat hodnoty typu float:
import ctypes so3 = ctypes.CDLL("./so3.so") a = 1.2 b = 3.4 c = so3.add(a, b) print(c)
Což nebude příliš úspěšné:
$ python3 use_so3B.py Traceback (most recent call last): File "use_so3B.py", line 8, in <module> c = so3.add(a, b) ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1
A nakonec s hodnotami typu long:
import ctypes so3 = ctypes.CDLL("./so3.so") a = 1 b = 10000000000000000 c = so3.add(a, b) print(c)
Zde dojde v některém místě výpočtu (prozatím nevíme kde) k přetečení:
1874919425
10. Použití specifického datového typu Go – int64
Abychom zjistili, kde přesně k přetečení výsledků dochází, upravíme nejdříve funkci add tak, aby na jakékoli architektuře akceptovala hodnoty typu int64 a aby i výsledek byl totožného typu. Navíc se budou vypisovat informace jak o předaných parametrech, tak i o výsledku. Tato úprava je triviální:
package main import "C" import "fmt" //export add func add(x, y int64) int64 { result := x + y fmt.Printf("Called add(%d, %d) with result %d\n", x, y, result) return result } func main() {}
Po překladu do dynamické knihovny se vytvoří i hlavičkový soubor, z něhož je patrné, že se skutečně použil odlišný typ:
extern "C" { #endif extern GoInt64 add(GoInt64 x, GoInt64 y); #ifdef __cplusplus
Úplný hlavičkový soubor, kde je i sekvence odvozených typů long long → GoInt64 → Goint, 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 add(GoInt64 x, GoInt64 y); #ifdef __cplusplus } #endif
Chování takto vzniklé dynamické knihovny si znovu otestujeme:
import ctypes so4 = ctypes.CDLL("./so4.so") a = 1 b = 2 c = so4.add(a, b) print(c)
Výsledek:
Called add(1, 2) with result 3 3
import ctypes so4 = ctypes.CDLL("./so4.so") a = 2**31-1 b = 1 c = so4.add(a, b) print(c)
Výsledek:
Called add(2147483647, 1) with result 2147483648 -2147483648
Na tomto výsledku je patrné, že část psaná v Go počítá výsledky správně (v 64bitovém rozsahu), ovšem na cestě do Pythonu se provede převod na int64.
11. Explicitní specifikace návratového typu funkce z dynamické knihovny
Problém s přetečením nastává z toho důvodu, že ctypes očekává, že funkce budou implicitně vracet hodnotu typu int. To však můžeme změnit, a to následovně (viz zvýrazněnou část kódu):
import ctypes so4 = ctypes.CDLL("./so4.so") a = 2**31-1 b = 1 so4.add.restype = ctypes.c_int64 c = so4.add(a, b) print(c)
S nyní již korektním a konzistentním výsledkem:
Called add(2147483647, 1) with result 2147483648 2147483648
12. Funkce akceptující parametr obsahující řetězec
Způsob předávání dalších číselných typů (kromě komplexních čísel) je stejně jednoduchý (nebo problematický – záleží na úhlu pohledu), jako práce s parametry a návratovými hodnotami typu int resp. int64. Pojďme si však ukázat, jak lze zajistit komunikaci mezi funkcí napsanou v Go a skriptem v Pythonu za situace, kdy je nutné Go funkci předat řetězec. Začneme tím nejjednodušším možným případem – funkcí, které se řetězec předá jako parametr a která tento řetězec vypíše a vrátí jeho délku:
package main import "C" import "fmt" //export hello func hello(name string) int { fmt.Printf("Hello %s\n", name) return len(name) } func main() {}
Překlad do dynamické knihovny již známe:
$ go build -buildmode=c-shared -o so5.so so5.go
Zajímavá je hlavička této funkce (z pohledu C a ctypes):
#ifdef __cplusplus extern "C" { #endif extern GoInt hello(GoString name); #ifdef __cplusplus
13. Otestování funkce akceptující řetězec
Funkci akceptující parametr typu string (v kontextu jazyka Go) si můžeme zkusit zavolat ze skriptu naprogramovaného v Pythonu:
import ctypes so5 = ctypes.CDLL("./so5.so") l = so5.hello("World!") print(l)
Toto zcela nevinně vypadající volání způsobí pád aplikace, a to konkrétně pád v nativní (Go) části, tedy v dynamické knihovně:
runtime.throw({0x7f969c3375a8, 0x0}) /opt/go/src/runtime/panic.go:1198 +0x71 fp=0xc000052978 sp=0xc000052948 pc=0x7f969c2e4871 runtime.(*mcache).allocLarge(0x0, 0x7f969c450000, 0x0, 0x1) /opt/go/src/runtime/mcache.go:229 +0x22e fp=0xc0000529d8 sp=0xc000052978 pc=0x7f969c2c83ae runtime.mallocgc(0x7f969c450000, 0x0, 0x0) /opt/go/src/runtime/malloc.go:1082 +0x5c5 fp=0xc000052a58 sp=0xc0000529d8 pc=0x7f969c2c0405 runtime.growslice(0x203000, {0xc0000160f8, 0x400, 0x60}, 0x6c) /opt/go/src/runtime/slice.go:261 +0x4ac fp=0xc000052ac0 sp=0xc000052a58 pc=0x7f969c2f996c fmt.(*buffer).writeString(...) /opt/go/src/fmt/print.go:82 fmt.(*fmt).padString(0x203000, {0x7f969c466550, 0x203000}) /opt/go/src/fmt/format.go:110 +0x21c fp=0xc000052b38 sp=0xc000052ac0 pc=0x7f969c32be3c fmt.(*fmt).fmtS(0x0, {0x7f969c466550, 0x0}) /opt/go/src/fmt/format.go:359 +0x35 fp=0xc000052b68 sp=0xc000052b38 pc=0x7f969c32cad5 fmt.(*pp).fmtString(0x100000000000000, {0x7f969c466550, 0x203000}, 0x0) /opt/go/src/fmt/print.go:446 +0xc5 fp=0xc000052bb8 sp=0xc000052b68 pc=0x7f969c32f145 fmt.(*pp).printArg(0xc0001005b0, {0x7f969c35c100, 0xc000056210}, 0x73) /opt/go/src/fmt/print.go:694 +0x60c fp=0xc000052c58 sp=0xc000052bb8 pc=0x7f969c3310cc fmt.(*pp).doPrintf(0xc0001005b0, {0x7f969c3369f4, 0x9}, {0xc000052df0, 0x7f969c2ba2a5, 0xc000052da0}) /opt/go/src/fmt/print.go:1026 +0x288 fp=0xc000052d50 sp=0xc000052c58 pc=0x7f969c333a08 fmt.Fprintf({0x7f969c36b080, 0xc000010018}, {0x7f969c3369f4, 0x9}, {0xc000052df0, 0x1, 0x1}) /opt/go/src/fmt/print.go:204 +0x75 fp=0xc000052db0 sp=0xc000052d50 pc=0x7f969c32e175 fmt.Printf(...) /opt/go/src/fmt/print.go:213 main.hello({0x7f969c466550, 0x1}) /tmp/ramdisk/so5.go:9 +0x69 fp=0xc000052e10 sp=0xc000052db0 pc=0x7f969c3349e9 _cgoexp_f2f6a0aec10a_hello(0x7fffb65fe2d0) _cgo_gotypes.go:38 +0x28 fp=0xc000052e30 sp=0xc000052e10 pc=0x7f969c334a68 runtime.cgocallbackg1(0x7f969c334a40, 0x0, 0x0) /opt/go/src/runtime/cgocall.go:306 +0x29a fp=0xc000052f00 sp=0xc000052e30 pc=0x7f969c2b801a runtime.cgocallbackg(0x0, 0x0, 0x0) /opt/go/src/runtime/cgocall.go:232 +0x109 fp=0xc000052f90 sp=0xc000052f00 pc=0x7f969c2b7ce9 runtime.cgocallbackg(0x7f969c334a40, 0x7fffb65fe2d0, 0x0) <autogenerated>:1 +0x31 fp=0xc000052fb8 sp=0xc000052f90 pc=0x7f969c311f11 runtime.cgocallback(0x0, 0x0, 0x0) /opt/go/src/runtime/asm_amd64.s:915 +0xb3 fp=0xc000052fe0 sp=0xc000052fb8 pc=0x7f969c30fb33 runtime.goexit() /opt/go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc000052fe8 sp=0xc000052fe0 pc=0x7f969c30fd61 Aborted (core dumped)
14. Funkce naprogramovaná v Go akceptující céčkový řetězec
Jednu z možných cest zajištění předávání řetězce z Pythonu do funkce naprogramované v Go představuje použití céčkovských řetězců, jejichž vlastnosti jsou od řetězců v Go či Pythonu dosti odlišné:
- Jedná se o sekvenci bajtů ukončených hodnotou 0
- Z pohledu programátora se jedná o ukazatel na první znak řetězce
- Tato sekvence bajtů je měnitelná
- O alokaci a dealokaci se musí postarat programátor
V jazyku Go můžeme napsat funkci, která akceptuje céčkovský řetězec:
func hello(name *C.char) int { ... ... ...
Ovšem aby s ním bylo možné pracovat jako s řetězcem jazyka Go, je nutné provést explicitní převod:
goName := C.GoString(name)
Demonstrační příklad s funkcí naprogramovanou v Go tedy můžeme upravit takto:
package main import "C" import "fmt" //export hello func hello(name *C.char) int { goName := C.GoString(name) fmt.Printf("Hello %s\n", goName) return len(goName) } func main() {}
Překlad do dynamické knihovny již známe:
$ go build -buildmode=c-shared -o so6.so so6.go
Z hlavičkového souboru je patrné, že jsme skutečně dosáhli kýženého cíle:
#ifdef __cplusplus extern "C" { #endif extern GoInt hello(char* name); #ifdef __cplusplus
15. Otestování funkce akceptující céčkový řetězec
Pojďme si tedy nově upravenou funkci otestovat. Nejdříve se jí pokusíme předat standardní řetězec Pythonu. Interní reprezentace řetězců je v Pythonu sice odlišná od céčkových řetězců, ovšem tento rozdíl bude řešit přímo ctypes:
import ctypes so6 = ctypes.CDLL("./so6.so") l = so6.hello("World!") print(l)
Tento první pokus nebude v Pythonu 3 úspěšný, neboť se provádí převod na wchar_t*, tedy předává se ukazatel na řetězec zkonvertovaný na pole „širokých“ znaků. Hned první široký znak, tedy „W“, bude ve druhém bajtu obsahovat nulu, která (z pohledu Go) řetězec ukončí, takže se vypíše právě ono dvojité wé:
$ python3 use_so6A.py Hello W 1
Zkusme tedy převod řetězce na typ bytes, který je taktéž knihovnou ctypes podporován. Určíme, že se má řetězec přetransformovat do kódování UTF-8, kde již bude nulový bajt skutečně umístěn pouze na konci řetězce:
import ctypes so6 = ctypes.CDLL("./so6.so") l = so6.hello("World!".encode("utf-8")) print(l)
Nyní se již obsah řetězce zobrazí korektně, stejně jako jeho délka:
$ python3 use_so6B.py Hello World! 6
Dále je vhodné si otestovat, jak bude předán řetězec používající znaky mimo ASCII:
import ctypes so6 = ctypes.CDLL("./so6.so") l = so6.hello("ěščř ЩжΛλ".encode("utf-8")) print(l)
I zde je vše v pořádku:
$ python3 use_so6C.py Hello ěščř ЩжΛλ 17
16. Funkce naprogramovaná v Go vracející řetězec
V jazyce Go je možné vytvořit funkci, která bude řetězec nejenom akceptovat jako parametr, ale i vracet. Kvůli komunikaci s Pythonem se ovšem nebude vracet typ string, ale ukazatel na C.char – tedy interně klasický céčkový řetězec:
//export concat func concat(text1, text2 *C.char) *C.char { ... ... ...
V případě, že v jazyce Go máme hodnotu typu string, je možné tuto hodnotu převést na céčkový řetězec zavoláním konverzní funkce C.Cstring:
text := t1 + "foo" return C.CString(text)
Úplný zdrojový kód takto vytvořené funkce bude vypadat následovně:
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() {}
17. Memory leak v Go?
Ostatně si to můžeme sami vyzkoušet. Nejprve zdánlivě funkční příklady.
Spojení řetězců bez konečného převodu z bytes na řetězec:
import ctypes so7 = ctypes.CDLL("./so7.so") t1 = "ěščř ЩжΛλ".encode("utf-8") t2 = "<foobar>".encode("utf-8") so7.concat.restype = ctypes.c_char_p t = so7.concat(t1, t2) print(t)
Výsledek:
b'\xc4\x9b\xc5\xa1\xc4\x8d\xc5\x99 \xd0\xa9\xd0\xb6\xce\x9b\xce\xbb<foobar>'
Převod výsledku zpět na řetězec:
import ctypes so7 = ctypes.CDLL("./so7.so") t1 = "ěščř ЩжΛλ".encode("utf-8") t2 = "<foobar>".encode("utf-8") so7.concat.restype = ctypes.c_char_p t = so7.concat(t1, t2) print(t.decode("utf-8"))
Výsledek:
ěščř ЩжΛλ<foobar>
A konečně se pokuste spustit tento skript a přes top/htop sledovat spotřebu paměti procesu (před začátkem swapování je dobré proces ukončit):
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)
18. Obsah navazujícího článku
Pro plnohodnotnou integraci programovacího jazyka Go s Pythonem je ve skutečnosti nutné umět předávat i hodnoty dalších typů – pole, řezy, struktury (záznamy) atd. (lze dokonce předávat a následně volat i funkce). S touto již poměrně složitější problematikou se seznámíme příště. Opět využijeme možností ctype, ovšem zmíníme se i o cffi, SWIG a dalších podobně koncipovaných projektech.
19. Repositář s demonstračními příklady
Zdrojové kódy všech 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:
20. 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/ - C? Go? Cgo!
https://go.dev/blog/cgo - 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