Hlavní navigace

Propojení Go s Pythonem s využitím cgo a ctypes

11. 1. 2022
Doba čtení: 28 minut

Sdílet

 Autor: Go lang
Poměrně často se můžeme setkat s požadavkem na propojení kódu psaného v Pythonu a v Go. Pro tento účel použijeme dvě technologie, které se samy nabízejí: cgo a ctypes. Ovšem uvidíme, že kvůli nim budeme muset „klesnout“ až na úroveň céčka.

Obsah

1. Propojení Go s Pythonem s využitím cgo a ctypes

2. Překlad zdrojového kódu Go do dynamické knihovny

3. Výsledky překladu

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

9. Volání funkce z Pythonu

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

17. Memory leak v Go?

18. Obsah navazujícího článku

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

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 cgoctypes 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.

Poznámka: v dalších článcích si ukážeme i poněkud elegantnější řešení, ovšem dnes popisovaná kombinace cgo + ctypes nám umožní pochopit, jak vlastně může kooperace mezi Go a Pythonem probíhat a že se ani zdaleka nejedná o zcela bezproblémovou záležitost.

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() {}
Poznámka: pro jednoduchost je funkce hello deklarována v balíčku main a tím pádem máme v kódu i funkci main. To má své výhody, protože main jeden zdrojový kód může být použit jak pro překlad aplikace, tak i knihovny.

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;
}
Poznámka: viz man 3 dlopen.

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");
    }
}
Poznámka: viz man 3 dlclose.

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
Poznámka: ve skutečnosti není míchání zpráv produkovaných céčkovými funkcemi ze stdio a zpráv vytvářených přes Go funkce fmt.Print* úplně jednoduché kvůli odlišným bufferům. Může se tedy stát, že zprávy nebudou na výstupu zobrazeny přesně v tom pořadí, v jakém jsou zavolány příslušné výstupní funkce (puts, putchar, printf, fmt.Print*).

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() {}
Poznámka: původní typ int totiž na 32bitových architekturách může mít pouze 32bitový rozsah.

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() {}
Poznámka: vidíme, že tato funkce akceptuje parametr typu string, což je skutečně „řetězec“, ovšem řetězec z pohledu programovacího jazyka Go. Z pohledu Pythonu či dokonce céčka se o „řetězec“ nejedná, což nám za chvíli způsobí problémy – funkce je tedy napsána korektně, překladač nenahlásí žádné problémy, ovšem dojde k pádu v runtime.

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
Poznámka: už tato hlavička by nás měla varovat, že ne vše bude pracovat korektně.

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)
Poznámka: z výpisu chybového hlášení to sice není zřejmé, ovšem nastal typický problém – jak v Go, tak i v Pythonu můžeme pracovat s datovým typem „řetězec“, což je ovšem interně zcela jiný typ, než je tomu v céčku. A při použití cgo a ctypes je společným jazykem právě C (což nám již může naznačovat, že jsme možná nezvolili ty nejlepší technologie).

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é:

  1. Jedná se o sekvenci bajtů ukončených hodnotou 0
  2. Z pohledu programátora se jedná o ukazatel na první znak řetězce
  3. Tato sekvence bajtů je měnitelná
  4. 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
Poznámka: funkce len v Go vrátí délku v bajtech.

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?

Poznámka: překlad výše uvedené funkce do nativní knihovny proběhne bez nejmenších problémů, takže by se mohlo zdát, že je vše v pořádku. Není tomu tak, protože zde opět dochází ke styku dvou odlišných světů – světa jazyka Go s automatickým správcem paměti a světa céčka s malloc a free. Problém v našem případě nastává v tom, že zatímco hodnoty typu string jsou automaticky uvolněny z paměti, kdy je potřeba (proměnné t1, t2 a result), v případě *C.char k uvolnění paměti nedojde – vytvořili jsme tedy krásný memory leak!

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):

Hacking tip

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:

# Příklad/soubor Stručný popis Cesta
1 so1.go definice funkce hello v jazyce Go https://github.com/tisnik/go-root/blob/master/article85/so1.go
2 so1.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article85/so1.h
3 use_so1.c volání funkce hello z céčka https://github.com/tisnik/go-root/blob/master/article85/use_so1.c
4 use_so1A.py volání nativní funkce hello z Pythonu https://github.com/tisnik/go-root/blob/master/article85/u­se_so1A.py
5 use_so1B.py volání nativní funkce hello z Pythonu https://github.com/tisnik/go-root/blob/master/article85/u­se_so1B.py
       
6 so2.go zavolání funkce hello z funkce main https://github.com/tisnik/go-root/blob/master/article85/so2.go
7 so2.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article85/so2.h
8 use_so2A.py volání nativní funkce hello z Pythonu https://github.com/tisnik/go-root/blob/master/article85/u­se_so2A.py
9 use_so2B.py volání nativní funkce hello z Pythonu https://github.com/tisnik/go-root/blob/master/article85/u­se_so2B.py
       
10 so3.go definice funkce add v jazyce Go https://github.com/tisnik/go-root/blob/master/article85/so3.go
11 so3.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article85/so3.h
12 use_so3A.py součet dvou celých čísel https://github.com/tisnik/go-root/blob/master/article85/u­se_so3A.py
13 use_so3B.py pokus o součet dvou čísel s plovoucí řádovou čárkou https://github.com/tisnik/go-root/blob/master/article85/u­se_so3B.py
14 use_so3C.py přetečení výsledku https://github.com/tisnik/go-root/blob/master/article85/u­se_so3C.py
       
15 so4.go funkce add pro datový typ int64 https://github.com/tisnik/go-root/blob/master/article85/so4.go
16 so4.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article85/so4.h
17 use_so4A.py součet dvou hodnot bez přetečení https://github.com/tisnik/go-root/blob/master/article85/u­se_so4A.py
18 use_so4B.py součet dvou hodnot s přetečením https://github.com/tisnik/go-root/blob/master/article85/u­se_so4B.py
19 use_so4C.py explicitní určení návratového typu funkce add https://github.com/tisnik/go-root/blob/master/article85/u­se_so4C.py
       
20 so5.go funkce akceptující parametr obsahující řetězec https://github.com/tisnik/go-root/blob/master/article85/so5.go
21 so5.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article85/so5.h
22 use_so5A.py pokus o volání funkce akceptující řetězec https://github.com/tisnik/go-root/blob/master/article85/u­se_so5A.py
       
23 so6.go funkce akceptující korektní céčkový řetězec https://github.com/tisnik/go-root/blob/master/article85/so6.go
24 so6.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article85/so6.h
25 use_so6A.py zavolání funkce naprogramované v Go s předáním Pythonovského řetězce https://github.com/tisnik/go-root/blob/master/article85/u­se_so6A.py
26 use_so6B.py zavolání funkce naprogramované v Go s předáním pole bajtů https://github.com/tisnik/go-root/blob/master/article85/u­se_so6B.py
27 use_so6C.py otestování s řetězcem obsahujícím znaky Unicode https://github.com/tisnik/go-root/blob/master/article85/u­se_so6C.py
       
28 so7.go funkce spojující dva céčkové řetězce https://github.com/tisnik/go-root/blob/master/article85/so7.go
29 so7.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article85/so7.h
30 use_so7A.py zavolání funkce naprogramované v Go https://github.com/tisnik/go-root/blob/master/article85/u­se_so7A.py
31 use_so7B.py zavolání funkce naprogramované v Go, převod výsledku na řetězec https://github.com/tisnik/go-root/blob/master/article85/u­se_so7B.py
32 use_so7C.py ukázka memory leaku v Go funkci https://github.com/tisnik/go-root/blob/master/article85/u­se_so7C.py

20. Odkazy na Internetu

  1. ctypes – A foreign function library for Python
    https://docs.python.org/3/li­brary/ctypes.html
  2. 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/
  3. cgo – Introduction
    https://zchee.github.io/golang-wiki/cgo/
  4. C? Go? Cgo!
    https://go.dev/blog/cgo
  5. dlopen(3) — Linux manual page
    https://man7.org/linux/man-pages/man3/dlopen.3.html
  6. dlclose(3p) — Linux manual page
    https://man7.org/linux/man-pages/man3/dlclose.3p.html
  7. dlsym(3) — Linux manual page
    https://man7.org/linux/man-pages/man3/dlsym.3.html
  8. How to correctly assign a pointer returned by dlsym into a variable of function pointer type?
    https://stackoverflow.com/qu­estions/36384195/how-to-correctly-assign-a-pointer-returned-by-dlsym-into-a-variable-of-function
  9. Faster Python with Go shared objects (the easy way)
    https://blog.kchung.co/faster-python-with-go-shared-objects/
  10. Programovací jazyk Rust: použití FFI pro volání funkcí z nativních knihoven
    https://www.root.cz/clanky/pro­gramovaci-jazyk-rust-pouziti-ffi-pro-volani-funkci-z-nativnich-knihoven/
  11. Programovací jazyk Rust: použití FFI pro volání funkcí z nativních knihoven (2. část)
    https://www.root.cz/clanky/pro­gramovaci-jazyk-rust-pouziti-ffi-pro-volani-funkci-z-nativnich-knihoven-2-cast/
  12. Programovací jazyk Rust: použití FFI při předávání struktur
    https://www.root.cz/clanky/pro­gramovaci-jazyk-rust-pouziti-ffi-pri-predavani-struktur/
  13. GNU C Library: Integers
    https://www.gnu.org/softwa­re/libc/manual/html_node/In­tegers.html
  14. Position-independent code
    https://cs.wikipedia.org/wiki/Position-independent_code
  15. Creating a shared and static library with the gnu compiler [gcc]
    http://www.adp-gmbh.ch/cpp/gcc/create_lib.html
  16. FFI: Foreign Function Interface
    https://doc.rust-lang.org/book/ffi.html
  17. Primitive Type pointer
    https://doc.rust-lang.org/std/primitive.pointer.html