Propojení Pythonu s nativními knihovnami s využitím balíčku cffi (2)

1. 6. 2023
Doba čtení: 27 minut

Sdílet

Autor: Python
Ukážeme si některé další možnosti použití cffi. Ukážeme si zpracování hlavičkových souborů, automatický překlad kódů z jazyka C do dynamicky linkované knihovny a předávání struktur či ukazatelů na struktury do funkcí psaných v C.

Obsah

1. Propojení Pythonu s nativními knihovnami s využitím balíčku cffi (2)

2. Funkce naprogramovaná v céčku, jejíž prototyp je uložen v hlavičkovém souboru

3. Využití hlavičkového souboru v Pythonních skriptech

4. Zpracování skutečného hlavičkového souboru s příkazy preprocesoru

5. Jedno z možných řešení tohoto problému

6. Překlad nativní knihovny a spuštění upraveného skriptu

7. Automatický překlad céčkovských kódů do dynamicky linkované knihovny

8. Skript v Pythonu, který provede překlad céčkovského kódu do nativní knihovny i se zavoláním tohoto kódu

9. Průběh překladu a výsledek činnosti skriptu

10. Soubory, které vznikly při běhu skriptu

11. Předávání parametrů dalších typů do nativních funkcí

12. Funkce psaná v C, která akceptuje parametr typu struktura (struct)

13. Předání struktury (struct) do nativní funkce

14. Výsledný skript psaný v Pythonu, který do céčkové funkce předá strukturu

15. Funkce psaná v C, která akceptuje parametr typu ukazatel na strukturu

16. Výsledný skript psaný v Pythonu, který do céčkové funkce předá ukazatel na strukturu

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

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

19. Odkazy na Internetu

1. Propojení Pythonu s nativními knihovnami s využitím balíčku cffi (2)

Na úvodní článek o propojení skriptů či celých aplikací naprogramovaných v Pythonu s nativními knihovnami (typicky naprogramovanými v jazyku C) s využitím balíčku cffi dnes navážeme. Nejprve se zmíníme o některých (a nutno říci, že mnohdy významných) omezeních knihovny cffi při zpracovávání hlavičkových souborů. Posléze si ukážeme, jakým způsobem je možné realizovat automatický překlad céčkovských zdrojových kódů přímo ze skriptu vytvořeného v Pythonu (na tuto část navážeme příště). A nakonec si řekneme a pochopitelně též ukážeme na demonstračních příkladech, jakými způsoby lze volat funkce psané v C, které jako své parametry vyžadují struktury (struct) popř. ukazatele na struktury. S těmito požadavky se totiž velmi často setkáme v praxi.

Obrázek 1: Programovací jazyk C je prozatím ve své nice prakticky nenahraditelný.

2. Funkce naprogramovaná v céčku, jejíž prototyp je uložen v hlavičkovém souboru

Velmi často se setkáme se situací, v níž jsou céčkovské funkce deklarovány v běžném zdrojovém souboru s koncovkou .c (jedná se o takzvanou definiční deklaraci), ovšem prototypy exportovaných funkcí jsou navíc uloženy v hlavičkovém souboru (jedná se o předběžnou deklaraci). S takovým hlavičkovým souborem dokáže knihovna cffi do určité míry pracovat a ušetřit tak programátorům práci.

Ukažme si to na jednoduchém příkladu. Hlavička (neboli přesněji řečeno předběžná deklarace) funkce greet je uložena v hlavičkovém souboru nazvaném „greet.h“, který pro jednoduchost obsahuje pouze tuto hlavičku a nikoli další deklarace či příkazy preprocesoru:

void greet(char *x);

Naproti tomu samotná definice funkce (přesněji řečeno definiční deklarace) je uložena v souboru „greet.h“, jehož obsah je následující:

#include <stdio.h>
 
#include "greeter.h"
 
extern void greet(char *x) {
    printf("Hello %s!\n", x);
}

Samotnou dynamickou knihovnu budeme překládat stále stejným způsobem:

$ gcc -Wall -ansi -c -fPIC greeter.c -o greeter.o
 
$ gcc -shared -Wl,-soname,libgreeter.so -o libgreeter.so greeter.o

Výsledkem bude soubor s názvem „libgreeter.so“:

$ file libgreeter.so
 
libgreeter.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=d6391750471a7443add6191a9836449a67f6283b, not stripped

3. Využití hlavičkového souboru v Pythonních skriptech

Hlavičkový soubor „greeter.h“, který byl ukázán v předchozí kapitole, je možné využít přímo v Pythoním skriptu, protože ho knihovna cffi za určitých okolností (viz dále) dokáže zpracovat a tudíž nebude po autorovi skriptu vyžadovat předběžné deklarace použitých funkcí. Pro načtení souboru „greeter.h“ použijeme funkci cffi.cdef:

ffi.cdef(load_header("greeter.h"))

A takto konkrétně vypadá zařazení volání této funkce do Pythonovského skriptu, který načte dynamickou knihovnu a zavolá nativní funkci z této knihovny:

import pathlib
 
from cffi import FFI
 
ffi = FFI()
 
 
def load_header(filename):
    directory = pathlib.Path().absolute()
    header = directory / filename
    with open(header) as fin:
        return fin.read()
 
 
def load_library(library_name):
    return ffi.dlopen(library_name)
 
 
ffi.cdef(load_header("greeter.h"))
greeter = load_library("libgreeter.so")
greeter.greet("world".encode("utf-8"))

4. Zpracování skutečného hlavičkového souboru s příkazy preprocesoru

Skutečné hlavičkové soubory ovšem neobsahují pouze předběžné deklarace funkcí, ale mnohdy i různé příkazy preprocesoru, například příkazy, které zamezují dvojímu zpracování skutečných definic (například datových typů). Velmi často se setkáme s následující strukturou hlavičkových souborů, v nichž je makro použito právě pro zamezení dvojího zpracování obsahu souboru_:

#ifndef _GREET_H_
#define _GREET_H_
 
void greet(char *x);
 
#endif

Současná verze knihovny cffi ovšem (možná kupodivu) takto strukturované hlavičkové soubory nedokáže zpracovat:

import pathlib
 
from cffi import FFI
 
ffi = FFI()
 
 
def load_header(filename):
    directory = pathlib.Path().absolute()
    header = directory / filename
    with open(header) as fin:
        return fin.read()
 
 
def load_library(library_name):
    return ffi.dlopen(library_name)
 
 
ffi.cdef(load_header("greeter.h"))
greeter = load_library("libgreeter.so")
greeter.greet("world".encode("utf-8"))

Po spuštění tohoto skriptu dojde k běhové chybě:

Traceback (most recent call last):
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/cffi/cparser.py", line 336, in _parse
    ast = _get_parser().parse(fullcsource)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/pycparser/c_parser.py", line 147, in parse
    return self.cparser.parse(
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/pycparser/ply/yacc.py", line 331, in parse
    return self.parseopt_notrack(input, lexer, debug, tracking, tokenfunc)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/pycparser/ply/yacc.py", line 1118, in parseopt_notrack
    p.callable(pslice)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/pycparser/c_parser.py", line 571, in p_pp_directive
    self._parse_error('Directives not supported yet',
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/pycparser/plyparser.py", line 67, in _parse_error
    raise ParseError("%s: %s" % (coord, msg))
pycparser.plyparser.ParseError: <cdef source string>:1:1: Directives not supported yet
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "call_via_cffi4.py", line 19, in
    ffi.cdef(load_header("greeter.h"))
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/cffi/api.py", line 112, in cdef
    self._cdef(csource, override=override, packed=packed, pack=pack)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/cffi/api.py", line 126, in _cdef
    self._parser.parse(csource, override=override, **options)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/cffi/cparser.py", line 389, in parse
    self._internal_parse(csource)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/cffi/cparser.py", line 394, in _internal_parse
    ast, macros, csource = self._parse(csource)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/cffi/cparser.py", line 338, in _parse
    self.convert_pycparser_error(e, csource)
  File "/home/ptisnovs/.local/lib/python3.8/site-packages/cffi/cparser.py", line 367, in convert_pycparser_error
    raise CDefError(msg)
cffi.CDefError: cannot parse "#ifndef _GREET_H_"
<cdef source string>:1:1: Directives not supported yet

5. Jedno z možných řešení tohoto problému

Výše zmíněný problém, který je způsoben nedostatky v knihovně cparser, je ve skutečnosti řešitelný pouze částečně. Jedno z možných řešení spočívá v tom, že rozdělíme hlavičkový soubor na dvě části – na část určenou pro preprocesor a překladač céčka a na část určenou pro cffi.

Část určená pro preprocesor a překladač céčka může vypadat následovně:

#ifndef _GREET_H_
#define _GREET_H_
 
#include "_greeter.h"
 
#endif

Stále zde tedy zajišťujeme, aby se každý symbol definoval pouze jedenkrát. Ovšem samotná hlavička (prototyp) funkce překládané do dynamicky linkované knihovny je uložena ve druhém souboru _greeter.h:

void greet(char *x);

Samotný céčkovský kód se přitom nemusí nijak měnit:

#include <stdio.h>
 
#include "greeter.h"
 
extern void greet(char *x) {
    printf("Hello %s!\n", x);
}

6. Překlad nativní knihovny a spuštění upraveného skriptu

Nyní by mělo být stále možné přeložit naši funkci greeter do nativní knihovny, protože z pohledu překladače jazyka C vlastně k žádné velké změně nedošlo:

$ gcc -Wall -ansi -c -fPIC greeter.c -o greeter.o
 
$ gcc -shared -Wl,-soname,libgreeter.so -o libgreeter.so greeter.o

Modifikovat je ovšem nutné skript v Pythonu, a to tak, aby zpracoval prototyp funkce ze souboru „_greeter.h“ a nikoli ze souboru „greeter.h“ (viz podtrženou část kódu):

import pathlib
 
from cffi import FFI
 
ffi = FFI()
 
 
def load_header(filename):
    directory = pathlib.Path().absolute()
    header = directory / filename
    with open(header) as fin:
        return fin.read()
 
 
def load_library(library_name):
    return ffi.dlopen(library_name)
 
 
ffi.cdef(load_header("_greeter.h"))
greeter = load_library("libgreeter.so")
greeter.greet("world".encode("utf-8"))

Výsledek:

export LD_LIBRARY_PATH=.
python3 call_via_cffi6.py
 
Hello world!

7. Automatický překlad céčkovských kódů do dynamicky linkované knihovny

Knihovna cffi vývojářům nabízí ještě jeden potenciálně užitečný režim činnosti. Namísto načtení již dříve vytvořené dynamicky linkované knihovny je totiž možné zajistit, že se překlad této knihovny provede přímo v rámci skriptu napsaného v Pythonu. Co k této operaci potřebujeme? Samozřejmě je nutné mít k dispozici céčkovský zdrojový kód s funkcemi, které budeme chtít volat z Pythonu. To je v našem případě soubor „greeter.c“ s tímto obsahem:

#include <stdio.h>
 
extern void greet(char *x) {
    printf("Hello %s!\n", x);
}

Dále je vhodné (ale nikoli striktně nutné), aby existoval hlavičkový soubor s prototypem výše uvedené funkce, a to opět ve zjednodušené podobě:

void greet(char *x);

V Pythonu se překlad této céčkovské části do nativní knihovny provede následovně:

ffi.set_source(
    "_greeter",
    '#include "greeter.h"',
    sources=["greeter.c"],
)
 
ffi.compile(verbose=True)

kde první parametr funkce ffi.set_source určuje jak jméno vygenerovaného céčkovského souboru (viz další kapitoly), tak i začátek jména souboru s dynamicky linkovanou knihovnou.

Poznámka: jak uvidíme příště, je tento režim v mnoha ohledech výhodný, protože nás dokáže odstínit od klasického ABI. To je však skutečně téma na samostatný článek.

8. Skript v Pythonu, který provede překlad céčkovského kódu do nativní knihovny i se zavoláním tohoto kódu

Celý skript, který provede překlad céčkovského kódu do nativní knihovny i se zavoláním tohoto kódu, bude vypadat následovně. Povšimněte si třetího příkazu import na konci skriptu:

import pathlib
 
from cffi import FFI
 
ffi = FFI()
 
 
def load_header(filename):
    directory = pathlib.Path().absolute()
    header = directory / filename
    with open(header) as fin:
        return fin.read()
 
 
def load_library(library_name):
    return ffi.dlopen(library_name)
 
 
ffi.cdef(load_header("greeter.h"))
 
ffi.set_source(
    "_greeter",
    '#include "greeter.h"',
    sources=["greeter.c"],
)
 
ffi.compile(verbose=True)
 
from _greeter import ffi, lib
lib.greet("world".encode("utf-8"))

9. Průběh překladu a výsledek činnosti skriptu

Předchozí skript se pokouší provádět čtyři kroky:

  1. Transpřeklad céčkovské funkce s vytvořením pomocného céčkovského souboru
  2. Překlad všech těchto zdrojových kódů do dynamicky linkované knihovny
  3. Načtení a inicializace této knihovny ze skriptu psaného v Pythonu
  4. Zavolání nativní funkce z právě načtené knihovny

Průběh činnosti můžeme sledovat na terminálu:

$ python3 call_via_cffi7.py

Průběh kroků 1, 2 a 4 se zobrazí na ploše terminálu:

generating ./_greeter.c
the current directory is '/home/ptisnovs/src/most-popular-python-libs/cffi/greeter_build'
running build_ext
building '_greeter' extension
x86_64-linux-gnu-gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -I/usr/include/python3.8 -c _greeter.c -o ./_greeter.o
x86_64-linux-gnu-gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -I/usr/include/python3.8 -c greeter.c -o ./greeter.o
x86_64-linux-gnu-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 ./_greeter.o ./greeter.o -o ./_greeter.cpython-38-x86_64-linux-gnu.so
Hello world!
Poznámka: povšimněte si, že v tomto případě není nutné nastavovat LD_LIBRARY_PATH atd.

10. Soubory, které vznikly při běhu skriptu

Po dokončení činnosti skriptu uvedeného v osmé kapitole se podívejme na obsah adresáře, z něhož byl skript spuštěn. Kromě původních souborů by se zde měly nacházet i čtyři nové soubory, které jsou ve výpisu podtrženy:

$ ls -l
 
total 180
-rw-rw-r-- 1 ptisnovs ptisnovs   504 May 25 18:44 call_via_cffi7.py
-rwxrw-r-- 1 ptisnovs ptisnovs    19 May 25 18:44 clean.sh
-rw-rw-r-- 1 ptisnovs ptisnovs 23334 May 31 07:57 _greeter.c
-rw-rw-r-- 1 ptisnovs ptisnovs    81 May 25 17:55 greeter.c
-rwxrwxr-x 1 ptisnovs ptisnovs 35088 May 31 07:57 _greeter.cpython-38-x86_64-linux-gnu.so
-rw-rw-r-- 1 ptisnovs ptisnovs    21 May 25 18:32 greeter.h
-rw-rw-r-- 1 ptisnovs ptisnovs 32544 May 31 07:57 _greeter.o
-rw-rw-r-- 1 ptisnovs ptisnovs  7024 May 31 07:57 greeter.o

Soubor „greeter.o“ obsahuje přeloženou formu naší funkce greet:

$ nm greeter.o 
 
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T greet
0000000000000000 r .LC0
                 U __printf_chk

Soubor „_greeter.c“ vznikl transpřekladem a obsahuje mnoho pomocných funkcí použitelných z Pythonu. Mj. zde nalezneme i definici symbolu odkazujícího (nepřímo) na naši funkci:

static const struct _cffi_global_s _cffi_globals[] = {
  { "greet", (void *)_cffi_f_greet, _CFFI_OP(_CFFI_OP_CPYTHON_BLTN_O, 0), (void *)_cffi_d_greet },
};

Překladem souboru „_greeter.c“ do objektového kódu vznikne soubor „_greeter.o“, jenž obsahuje větší množství symbolů:

$ nm _greeter.o 
 
0000000000000000 t _cffi_d_greet
0000000000000000 b _cffi_exports
0000000000000010 t _cffi_f_greet
0000000000000060 d _cffi_globals
0000000000000000 d _cffi_type_context
0000000000000000 d _cffi_types
                 U _GLOBAL_OFFSET_TABLE_
                 U greet
0000000000000000 r .LC0
0000000000000009 r .LC1
0000000000000017 r .LC2
0000000000000000 r .LC3
                 U memset
                 U _Py_Dealloc
                 U PyEval_RestoreThread
                 U PyEval_SaveThread
                 U PyImport_ImportModule
00000000000001c0 T PyInit__greeter
                 U PyLong_FromVoidPtr
                 U _Py_NoneStruct
                 U PyObject_CallMethod
                 U PyObject_Free
                 U PyObject_Malloc
                 U __stack_chk_fail

A konečně výsledkem činnosti první části skriptu je kýžená dynamicky linkovaná knihovna „_greeter.cpython-38-x86_64-linux-gnu.so“:

$ nm _greeter.cpython-38-x86_64-linux-gnu.so
 
0000000000001260 t _cffi_d_greet
0000000000004100 b _cffi_exports
0000000000001270 t _cffi_f_greet
0000000000003de0 d _cffi_globals
0000000000003d80 d _cffi_type_context
00000000000040a0 d _cffi_types
00000000000040e0 b completed.8061
                 w __cxa_finalize@@GLIBC_2.2.5
00000000000011a0 t deregister_tm_clones
0000000000001210 t __do_global_dtors_aux
0000000000003d78 d __do_global_dtors_aux_fini_array_entry
0000000000004080 d __dso_handle
0000000000003e00 d _DYNAMIC
000000000000152c t _fini
0000000000001250 t frame_dummy
0000000000003d70 d __frame_dummy_init_array_entry
000000000000218c r __FRAME_END__
0000000000004000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
000000000000204c r __GNU_EH_FRAME_HDR
0000000000001510 T greet
0000000000001000 t _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U memset@@GLIBC_2.2.5
                 U __printf_chk@@GLIBC_2.3.4
                 U _Py_Dealloc
                 U PyEval_RestoreThread
                 U PyEval_SaveThread
                 U PyImport_ImportModule
0000000000001420 T PyInit__greeter
                 U PyLong_FromVoidPtr
                 U _Py_NoneStruct
                 U PyObject_CallMethod
                 U PyObject_Free
                 U PyObject_Malloc
00000000000011d0 t register_tm_clones
                 U __stack_chk_fail@@GLIBC_2.4
00000000000040c8 d __TMC_END__

11. Předávání parametrů dalších typů do nativních funkcí

Všechny ukázané příklady komunikace mezi programem psaným v Pythonu a céčkovým kódem byly poměrně bezproblémové, a to z toho prostého důvodu, že jsme prozatím používali takové datové typy, které jsou do značné míry kompatibilní mezi Pythonem a C – tedy celočíselné hodnoty popř. ukazatele na pole bajtů. Ovšem praxe je zcela odlišná, protože se setkáme s céčkovskými funkcemi akceptujícími například struktury, ukazatele na struktury, pole, ukazatele na funkce atd. atd. A i s takovými hodnotami je nutné umět nějakým způsobem pracovat. V dalším textu se zaměříme na předávání struktur (struct) a taktéž ukazatelů na struktury. Příště se navíc zaměříme na složitější případy, z nichž některé jsou převzaty z praxe.

12. Funkce psaná v C, která akceptuje parametr typu struktura (struct)

Vyzkoušejme si nyní poměrně typický případ. Mějme datový typ struktura (struct), který například reprezentuje vektor v 3D prostoru. Jedná se tedy o strukturu obsahující tři prvky typu double:

typedef struct {
    double x;
    double y;
    double z;
} vector_t;

Dále definujme funkci print_vector, která bude tisknout hodnoty prvků vektoru. Přitom struktura s vektorem je do této funkce předána hodnotou a nikoli odkazem:

#include <stdio.h>
 
#include "vector_printer.h"
 
extern void print_vector(vector_t v)
{
    printf("Vector (%.1f  %.1f  %.1f)\n", v.x, v.y, v.z);
}

V dalším textu si ukážeme, jak lze tuto strukturu vytvořit v cffi a jak se předává do céčkovské funkce.

13. Předání struktury (struct) do nativní funkce

Nyní nastává poněkud komplikovaná situace, protože ještě před zavoláním céčkovské funkce musíme na straně Pythonu vytvořit céčkovskou strukturu resp. obdobu této struktury. V případě použití cffi se tato operace provádí nepřímo, protože zkonstruujeme objekt, který je ukazatelem na strukturu a nikoli přímo strukturou samotnou. Tato operace se provádí konstruktorem ffi.new, kterému se v řetězci předá datový typ výsledku. Musí se přitom vždy jednat buď o ukazatel nebo o pole:

vector = ffi.new("vector_t *")

nebo:

vector = ffi.new("vector_t *", výchozí hodnoty prvků struktury)
Poznámka: datový typ se kontroluje v době běhu Pythonovského skriptu. Knihovna cffi pochopitelně musí mít k dispozici definici typu vector_t (ta se načte z hlavičkového souboru).

Nyní můžeme prvky vektoru naplnit přímo v Pythonu velmi idiomatickým způsobem (na pozadí se ovšem provádí složitější operace):

vector.x = 1
vector.y = 2
vector.z = 3

Poněkud nepohodlné je volání céčkovské funkce, protože té nechceme předat ukazatel na strukturu, ale přímo danou strukturu jako hodnotu. Musíme zde použít dosti ošklivý trik, který vypadá takto:

vprinter.print_vector(vector[0])
Poznámka: pochopitelně – a vyzkoušejte si to – je k dispozici jen první prvek „pole“. Pokus o použití indexů s odlišnou hodnotou povede k vyhození běhové výjimky:
Traceback (most recent call last):
  File "call_via_cffi.py", line 26, in
    vprinter.print_vector(vector[1])
IndexError: cdata 'vector_t *' can only be indexed by 0

14. Výsledný skript psaný v Pythonu, který do céčkové funkce předá strukturu

Podívejme se nyní na úplný kód skriptu, který dokáže zavolat céčkovskou funkci akceptující strukturu a předat jí korektní data. To, že jsou předaná data skutečně korektní, se dozvíme z vypsaného výsledku:

import pathlib
 
from cffi import FFI
 
ffi = FFI()
 
def load_header(filename):
    directory = pathlib.Path().absolute()
    header = directory / filename
    with open(header) as fin:
        return fin.read()
 
 
def load_library(library_name):
    return ffi.dlopen(library_name)
 
 
ffi.cdef(load_header("vector_printer.h"))
 
vector = ffi.new("vector_t *")
vector.x = 1
vector.y = 2
vector.z = 3
 
vprinter = load_library("libvprinter.so")
vprinter.print_vector(vector[0])

Skript spustíme nám již známým způsobem:

$ export LD_LIBRARY_PATH=.
$ python3 call_via_cffi.py

Výsledek vypsaný na terminál by měl vypadat následovně:

Vector (1.0  2.0  3.0)

15. Funkce psaná v C, která akceptuje parametr typu ukazatel na strukturu

Nyní se podívejme na další demonstrační příklad, s jehož obdobou se velmi často můžeme setkat v praxi. Opět budeme pracovat s datovou strukturou obsahující tři souřadnice vektoru:

typedef struct {
    double x;
    double y;
    double z;
} vector_t;

Nyní se ovšem do céčkovské funkce nebude struktura předávat hodnotou, ale odkazem (neboli se předá ukazatel na strukturu). Pochopitelně se změní typ parametru volané funkce, ale navíc se při přístupu k prvkům struktury předané odkazem může použít operátor → (namísto poměrně nepřehledné kombinace operátorů * a .):

#include <stdio.h>
 
#include "vector_printer.h"
 
extern void print_vector(vector_t *v)
{
    printf("Vector (%.1f  %.1f  %.1f)\n", v->x, v->y, v->z);
}

16. Výsledný skript psaný v Pythonu, který do céčkové funkce předá ukazatel na strukturu

Na straně Pythonu budeme postupovat prakticky stejným způsobem, jako v případě předchozího demonstračního příkladu. Necháme si zkonstruovat objekt, který reprezentuje Pythonovskou obdobu ukazatele na strukturu a naplníme prvky této struktury:

vector = ffi.new("vector_t *")
vector.x = 1
vector.y = 2
vector.z = 3

Volání céčkovské funkce z dynamicky linkované knihovny bude nyní (poněkud paradoxně) snazší, protože se nebudeme muset starat o získání hodnoty z „pole“:

vprinter = load_library("libvprinter.so")
vprinter.print_vector(vector)

Celý skript, který načte dynamicky linkovanou knihovnu, vytvoří strukturu, naplní její prvky a předá ukazatel na tuto strukturu do céčkovské funkce, může vypadat následovně:

import pathlib
 
from cffi import FFI
 
ffi = FFI()
 
def load_header(filename):
    directory = pathlib.Path().absolute()
    header = directory / filename
    with open(header) as fin:
        return fin.read()
 
 
def load_library(library_name):
    return ffi.dlopen(library_name)
 
 
ffi.cdef(load_header("vector_printer.h"))
 
vector = ffi.new("vector_t *")
vector.x = 1
vector.y = 2
vector.z = 3
 
vprinter = load_library("libvprinter.so")
vprinter.print_vector(vector)

Po spuštění skriptu napsaného v Pythonu získáme stejný výsledek, jako při spuštění skriptu ze čtrnácté kapitoly, i když se nyní interně volá céčkovská funkce s předáním jediného ukazatele a nikoli hodnoty celé datové struktury:

docker + kubernetes školení s dotací tip

Vector (1.0  2.0  3.0)

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

Ve třetím článku o propojení Pythonu s nativními knihovnami s využitím balíčku cffi si nejprve ukážeme způsob komunikace s céčkovskými funkcemi, které akceptují jako své parametry pole popř. naopak pole vrací. Užitečná je i znalost práce s callback funkcemi, což je opět téma, kterým se budeme zabývat příště. Taktéž si řekneme, jak se pracuje s hodnotami zkonstruovanými na straně Pythonu, které by měly být z operační paměti odstraňovány automatickým správcem paměti (GC – garbage collector).

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

Všechny Pythonovské skripty, které jsme si v dnešním i <a>předchozím článku ukázali, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je nutné mít nainstalovánu knihovnu cffi:

# Příklad Stručný popis Adresa
1 adder/adder.c funkce psaná v C, která sečte své dva celočíselné parametry https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/adder.c
2 adder/call_via_cffi1.py zavolání céčkovské funkce přes cffi s korektními parametry https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_cffi1.py
3 adder/call_via_cffi2.py zavolání céčkovské funkce přes cffi s nekorektními parametry https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_cffi2.py
4 adder/call_via_cffi3.py zavolání céčkovské funkce přes cffi s nekorektními parametry https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_cffi3.py
5 adder/call_via_cffi.sh nastavení cest a spuštění všech tří předchozích Pythonovských skriptů https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_cffi.sh
6 adder/call_via_ctypes1.py zavolání céčkovské funkce přes ctypes s korektními parametry https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_ctypes1.py
7 adder/call_via_ctypes2.py zavolání céčkovské funkce přes ctypes s nekorektními parametry https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_ctypes2.py
8 adder/call_via_ctypes3.py zavolání céčkovské funkce přes ctypes s nekorektními parametry https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_ctypes3.py
9 adder/call_via_ctypes.sh nastavení cest a spuštění všech tří předchozích Pythonovských skriptů https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ca­ll_via_ctypes.sh
10 adder/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/ma­ke_library.sh
11 adder/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/adder/clean.sh
       
12 greeter/greeter.c funkce psaná v C, která na standardní výstup vytiskne řetězec https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/greeter.c
13 greeter/call_via_cffi1.py zavolání céčkovské funkce přes cffi s nekorektním parametrem https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_cffi1.py
14 greeter/call_via_cffi2.py zavolání céčkovské funkce přes cffi s korektním parametrem https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_cffi2.py
15 greeter/call_via_cffi3.py zavolání céčkovské funkce přes cffi s korektním parametrem https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_cffi3.py
16 greeter/call_via_cffi.sh nastavení cest a spuštění všech tří předchozích Pythonovských skriptů https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_cffi.sh
17 greeter/call_via_ctypes1.py zavolání céčkovské funkce přes ctypes s nekorektním parametrem https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_ctypes1.py
18 greeter/call_via_ctypes2.py zavolání céčkovské funkce přes ctypes s korektním parametrem https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_ctypes2.py
19 greeter/call_via_ctypes3.py zavolání céčkovské funkce přes ctypes s korektním parametrem https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_ctypes3.py
20 greeter/call_via_ctypes.sh nastavení cest a spuštění všech tří předchozích Pythonovských skriptů https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/call_via_ctypes.sh
21 greeter/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter/make_library.sh
22 greeter/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/greeter/clean.sh
       
23 swapper/swapper.c céčkovská funkce prohazující obsah svých dvou parametrů předávaných referencí https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/swap­per/swapper.c
24 swapper/call_via_cffi1.py zavolání céčkovské knihovny z jazyka Python (korektní varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/swap­per/call_via_cffi1.py
25 swapper/call_via_cffi2.py zavolání céčkovské knihovny z jazyka Python (nekorektní varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/swap­per/call_via_cffi2.py
26 swapper/call_via_cffi.sh nastavení cest a spuštění Pythonovského skriptu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/swap­per/call_via_cffi.sh
27 swapper/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/swap­per/make_library.sh
28 swapper/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/swapper/clean.sh
       
29 filler/filler.c céčkovská funkce pro vyplnění části pole zadanou hodnotou https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/filler/filler.c
30 filler/call_via_cffi.py zavolání céčkovské knihovny z jazyka Python https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/filler/ca­ll_via_cffi.py
31 filler/call_via_cffi.sh nastavení cest a spuštění Pythonovského skriptu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/filler/ca­ll_via_cffi.sh
32 filler/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/filler/ma­ke_library.sh
32 filler/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/filler/clean.sh
       
33 greeter_h/greeter.c funkce psaná v C, která na standardní výstup vytiskne řetězec https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h/greeter.c
34 greeter_h/greeter.h prototyp (předběžná deklarace) funkce greeter https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h/greeter.h
35 greeter_h/call_via_cffi4.py zavolání céčkovské knihovny z jazyka Python https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h/call_via_cffi4.py
36 greeter_h/call_via_cffi.sh nastavení cest a spuštění Pythonovského skriptu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h/call_via_cffi.sh
37 greeter_h/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h/make_library.sh
38 greeter_h/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h/clean.sh
       
39 greeter_h2/greeter.c funkce psaná v C, která na standardní výstup vytiskne řetězec https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h2/greeter.c
40 greeter_h2/greeter.h prototyp (předběžná deklarace) funkce greeter obalená v testu na existenci symbolu/makra https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h2/greeter.h
41 greeter_h2/call_via_cffi5.py zavolání céčkovské knihovny z jazyka Python https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h2/call_via_cffi5.py
42 greeter_h2/call_via_cffi.sh nastavení cest a spuštění Pythonovského skriptu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h2/call_via_cffi.sh
43 greeter_h2/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h2/make_library.sh
44 greeter_h2/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h2/clean.sh
       
45 greeter_h3/greeter.c funkce psaná v C, která na standardní výstup vytiskne řetězec https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h3/greeter.c
46 greeter_h3/greeter.h test na existenci symbolu/makra, pokud makro neexistuje, provede se vložení dalšího souboru https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h3/greeter.h
47 greeter_h3/_greeter.h prototyp (předběžná deklarace) funkce greeter bez dalších informací https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h3/_greeter.h
48 greeter_h3/call_via_cffi5.py zavolání céčkovské funkce z knihovny z jazyka Python https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h3/call_via_cffi5.py
49 greeter_h3/call_via_cffi.sh nastavení cest a spuštění Pythonovského skriptu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h3/call_via_cffi.sh
50 greeter_h3/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h3/make_library.sh
51 greeter_h3/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_h3/clean.sh
       
52 greeter_build/greeter.c funkce psaná v C, která na standardní výstup vytiskne řetězec https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_build/greeter.c
53 greeter_build/greeter.h prototyp (předběžná deklarace) funkce greeter bez dalších informací https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_build/greeter.h
54 greeter_build/call_via_cffi7.py skript pro překlad céčkovské funkce, vytvoření dynamicky linkované knihovny a zavolání funkce z této knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_build/call_via_cffi7­.py
55 greeter_build/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/gre­eter_build/clean.sh
       
56 vector_printer/vector_printer.c funkce psaná v C, která akceptuje jako svůj parametr strukturu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer/vector_printer­.c
57 vector_printer/vector_printer.h prototyp (předběžná deklarace) funkce print_vector bez dalších informací https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer/vector_printer­.h
58 vector_printer/call_via_cffi.sh zavolání céčkovské funkce z knihovny z jazyka Python https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer/call_via_cffi­.sh
59 vector_printer/call_via_cffi.py nastavení cest a spuštění Pythonovského skriptu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer/call_via_cffi­.py
60 vector_printer/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer/make_library.sh
61 vector_printer/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer/clean.sh
       
62 vector_printer2/vector_printer.c funkce psaná v C, která akceptuje jako svůj parametr ukazatel na strukturu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer2/vector_printer­.c
63 vector_printer2/vector_printer.h prototyp (předběžná deklarace) funkce print_vector bez dalších informací https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer2/vector_printer­.h
64 vector_printer2/call_via_cffi.sh zavolání céčkovské funkce z knihovny z jazyka Python https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer2/call_via_cffi­.sh
65 vector_printer2/call_via_cffi.py nastavení cest a spuštění Pythonovského skriptu https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer2/call_via_cffi­.py
66 vector_printer2/make_library.sh skript pro překlad céčkovské funkce a vytvoření dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer2/make_library­.sh
67 vector_printer2/clean.sh skript pro smazání objektového souboru i dynamicky linkované knihovny https://github.com/tisnik/most-popular-python-libs/blob/master/cffi/vec­tor_printer2/clean.sh

19. Odkazy na Internetu

  1. TIOBE Index for May 2023
    https://www.tiobe.com/tiobe-index/
  2. PYPL PopularitY of Programming Language
    https://pypl.github.io/PYPL.html
  3. CFFI documentation
    https://cffi.readthedocs.i­o/en/latest/
  4. cffi 1.15.1 na PyPi
    https://pypi.org/project/cffi/
  5. Python Bindings: Calling C or C++ From Python
    https://realpython.com/python-bindings-overview/
  6. Interfacing with C/C++ Libraries
    https://docs.python-guide.org/scenarios/clibs/
  7. Cython, pybind11, cffi – which tool should you choose?
    http://blog.behnel.de/posts/cython-pybind11-cffi-which-tool-to-choose.html
  8. Python FFI with ctypes and cffi
    https://eli.thegreenplace­.net/2013/03/09/python-ffi-with-ctypes-and-cffi
  9. Propojení Go s Pythonem s využitím cgo a ctypes
    https://www.root.cz/clanky/propojeni-go-s-pythonem-s-vyuzitim-cgo-a-ctypes/
  10. Propojení Go s Pythonem s využitím cgo a ctypes (2. část)
    https://www.root.cz/clanky/propojeni-go-s-pythonem-s-vyuzitim-cgo-a-ctypes-2-cast/
  11. 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/
  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. 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/
  14. Dynamic-link library
    https://en.wikipedia.org/wiki/Dynamic-link_library
  15. Úvod do jazyka C: Deklarace funkcí
    https://www.fi.muni.cz/us­r/jkucera/pb071/sl5.htm
  16. Using standard library headers with CFFI
    https://stackoverflow.com/qu­estions/57481873/using-standard-library-headers-with-cffi
  17. Preparing and Distributing modules
    https://cffi.readthedocs.i­o/en/latest/cdef.html

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.