Hlavní navigace

Propojení Go s Pythonem s využitím cgo a ctypes (2. část)

13. 1. 2022
Doba čtení: 26 minut

Sdílet

 Autor: Go lang
Pro plnohodnotnou integraci jazyka Go s Pythonem je nutné umět předávat i hodnoty neprimitivních typů, zejména pole, řezy, struktury (záznamy) atd. S touto již poměrně složitější problematikou se částečně seznámíme v dnešním článku.

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

12. Předání pole z Pythonu

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

18. Odkazy na Internetu

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)
Poznámka: nezapomeňte proces se skriptem ukončit ještě předtím, než se začne swapovat nebo se dokonce spustí OOM killer.

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)
Poznámka: mimochodem stojí za povšimnutí, jak relativně složité jsou kódy v Go i Pythonu ve chvíli, kdy se oba jazyky musí navzájem domlouvat přes typový systém programovacího jazyka C.

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)
Poznámka: při běžném testování se na tento problém nemusí narazit, protože řetězce bývají v porovnání s kapacitami moderních RAM relativně malé. Ovšem v produkčním nasazení je problém způsobený použitím prvního skriptu prakticky zaručen :-)

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
Poznámka: první vrácená hodnota indikuje, že předané pole bylo prázdné.

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
}
Poznámka: prostřední příkaz, tedy volání fmt, je pochopitelně možné po odladění odstranit.

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:

Hacking tip

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:

# 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
       
33 so8.go funkce spojující dva céčkové řetězce https://github.com/tisnik/go-root/blob/master/article86/so8.go
34 so8.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article86/so8.h
35 use_so8A.py korektní zavolání funkce pro uvolnění řetězce z paměti https://github.com/tisnik/go-root/blob/master/article86/u­se_so8A.py
36 use_so8B.py přímé zavolání standardní funkce free https://github.com/tisnik/go-root/blob/master/article86/u­se_so8B.py
37 use_so8C.py test na memory leak https://github.com/tisnik/go-root/blob/master/article86/u­se_so8C.py
38 use_so8C.py test na memory leak https://github.com/tisnik/go-root/blob/master/article86/u­se_so8C.py
       
39 so9.go funkce zpracovávající pole s prvky typu int https://github.com/tisnik/go-root/blob/master/article86/so9.go
40 so9.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article86/so9.h
41 use_so9.c volání funkce sum z céčka https://github.com/tisnik/go-root/blob/master/article86/use_so9.c
42 use_so9A.py předání pole z Pythonu do Go https://github.com/tisnik/go-root/blob/master/article86/u­se_so9A.py
43 use_so9B.py předání pole získaného ze seznamu z Pythonu do Go https://github.com/tisnik/go-root/blob/master/article86/u­se_so9B.py
44 use_so9C.py pokus o vytvoření pole z nekompatibilních hodnot https://github.com/tisnik/go-root/blob/master/article86/u­se_so9C.py
       
45 so10.go funkce zpracovávající pole s prvky typu double https://github.com/tisnik/go-root/blob/master/article86/so10.go
46 so10.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article86/so10.h
47 use_so10A.py volání nativní funkce akceptující pole z Pythonu https://github.com/tisnik/go-root/blob/master/article86/u­se_so10.py
       
48 so11.go funkce zpracovávající strukturu https://github.com/tisnik/go-root/blob/master/article86/so11.go
49 so11.h vygenerovaný hlavičkový soubor https://github.com/tisnik/go-root/blob/master/article86/so11.h
50 use_so11.c volání funkce length z céčka https://github.com/tisnik/go-root/blob/master/article86/u­se_so11.c
51 use_so11.py volání funkce length z Pythonu https://github.com/tisnik/go-root/blob/master/article86/u­se_so11.py

18. 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. Introduction to cgo (wiki)
    https://github.com/golang/go/wiki/cgo
  5. C? Go? Cgo!
    https://go.dev/blog/cgo
  6. ctypes return a string from c function
    https://newbedev.com/ctypes-return-a-string-from-c-function
  7. cgo: Passing a slice/array pointer between Go/C
    https://groups.google.com/g/Golang-Nuts/c/DXCIP6pMMN0
  8. cgo-struct-array
    https://github.com/llgoer/cgo-struct-array
  9. C structs and Pointers
    https://www.programiz.com/c-programming/c-structures-pointers
  10. Pass struct and array of structs to C function from Go
    https://stackoverflow.com/qu­estions/19910647/pass-struct-and-array-of-structs-to-c-function-from-go
  11. dlopen(3) — Linux manual page
    https://man7.org/linux/man-pages/man3/dlopen.3.html
  12. dlclose(3p) — Linux manual page
    https://man7.org/linux/man-pages/man3/dlclose.3p.html
  13. dlsym(3) — Linux manual page
    https://man7.org/linux/man-pages/man3/dlsym.3.html
  14. 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
  15. Faster Python with Go shared objects (the easy way)
    https://blog.kchung.co/faster-python-with-go-shared-objects/
  16. 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/
  17. 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/
  18. 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/
  19. GNU C Library: Integers
    https://www.gnu.org/softwa­re/libc/manual/html_node/In­tegers.html
  20. Position-independent code
    https://cs.wikipedia.org/wiki/Position-independent_code
  21. Creating a shared and static library with the gnu compiler [gcc]
    http://www.adp-gmbh.ch/cpp/gcc/create_lib.html
  22. FFI: Foreign Function Interface
    https://doc.rust-lang.org/book/ffi.html
  23. Primitive Type pointer
    https://doc.rust-lang.org/std/primitive.pointer.html

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.