Skip to content

20. Αλληλεπίδραση της C με την Python

Σύνοψη Κλήση κώδικα C από κώδικα Python, η βιβλιοθήκη ctypes, η βιβλιοθήκη CFFI, CFFI/ABI, CFFI/API, γραφικό περιβάλλον διεπαφής με Python που συμπληρώνεται με αποτελέσματα που παράγονται από κώδικα C, κλήση κώδικα Python από κώδικα C.

Προαπαιτούμενη γνώση Τύποι δεδομένων, είσοδος/έξοδος, δομές επιλογής και επανάληψης, συναρτήσεις, πίνακες, δείκτες, αλφαριθμητικά, διαμερισμός κώδικα, βασικές γνώσεις προγραμματισμού σε Python.

20.1 Εισαγωγή

Μερικές φορές είναι επιθυμητό να κληθεί κώδικας που έχει γραφεί στη γλώσσα C μέσα από προγράμματα Python. Αυτό μπορεί να συμβεί διότι επιθυμούμε να χρησιμοποιήσουμε βιβλιοθήκες της C για τις οποίες δεν υπάρχουν διεπαφές χρήσης τους μέσω της Python ή μπορεί να πρόκειται για κώδικα σε C που έχουμε αναπτύξει οι ίδιοι. Τρεις διαφορετικοί τρόποι για να επιτευχθεί αυτό είναι: α) με την ενσωματωμένη στην Python βιβλιοθήκη ctypes 1, β) με τη βιβλιοθήκη CFFI 2 και γ) με χρήση των native C/C++ extensions 3. Ο τρίτος τρόπος είναι χαμηλότερου επιπέδου, δίνει πολλές δυνατότητες αλλά είναι σχετικά πολύπλοκος, οπότε συνήθως προτιμούνται οι δύο πρώτοι.
Κίνητρο επέκτασης της Python με C κώδικα μπορεί να αποτελέσει η επιθυμία επιτάχυνσης της «αργής» εκτέλεσης της Python. Η ενσωμάτωση κώδικα C σε προγράμματα Python μπορεί να οδηγήσει σε σημαντικές επιταχύνσεις αλλά και να δώσει τη δυνατότητα αποφυγής του περιορισμού της Python (CPython(1)) που είναι γνωστός ως GIL (Global Interpreter Lock) και επιτρέπει μόνο σε ένα νήμα να εκτελείται σε κάθε χρονική στιγμή. Ωστόσο, ο ενδεδειγμένος τρόπος επιτάχυνσης της Python είναι μέσω βιβλιοθηκών της ίδιας της γλώσσας όπως η numpy, η numba, η multiprocessing και πολλές άλλες. Ένας άλλος τρόπος επιτάχυνσης του κώδικα Python είναι η συγγραφή προγραμμάτων σε Cython (https://cython.org/) που αποτελεί μια γλώσσα υπερσύνολο της Python. Ο Cython κώδικας μεταγλωττίζεται σε C και εκτελείται με υψηλή ταχύτητα.

  1. CPython είναι ο προκαθορισμένος byte-code διερμηνευτής της Python, που έχει γραφεί σε C.

Σπανιότερα, μπορεί να προκύψει ανάγκη ενσωμάτωσης κώδικα Python σε κώδικα C. Και αυτό μπορεί να γίνει σχετικά εύκολα, καθώς η Python παρέχει το αρχείο επικεφαλίδας Python.h, που περιλαμβάνει όλες τις απαιτούμενες δηλώσεις για την ενσωμάτωση του διερμηνευτή της σε προγράμματα C.
Αξίζει επίσης να αναφερθεί το λογισμικό SWIG (Simplified Wrapper and Interface Generator, https://www.swig.org/) που αποτελεί έναν ακόμη τρόπο διασύνδεσης προγραμμάτων που είναι γραμμένα σε C (ή σε C++) με την Python καθώς και με πολλές άλλες γλώσσες υψηλού επιπέδου (π.χ. Javascript, Perl, PHP, Tcl, Ruby, Java, C#, Go).

20.2 Κλήση κώδικα C μέσα από κώδικα Python

Η Python είναι μια ιδιαίτερα δημοφιλής και ισχυρή γλώσσα προγραμματισμού. Κατά την ανάπτυξη μιας εφαρμογής σε Python μπορεί να παρουσιαστεί το σενάριο όπου μια επιθυμητή λειτουργία έχει ήδη υλοποιηθεί με αξιόπιστο C κώδικα. Για να αποφευχθεί η συγγραφή εκ νέου του κώδικα στην Python, μπορεί να χρησιμοποιηθεί απευθείας ο διαθέσιμος κώδικας C.

Παράδειγμα κώδικα σε C Ως παράδειγμα κώδικα C που θα κληθεί από την Python θα χρησιμοποιηθεί κώδικας που ορίζει και χρησιμοποιεί μια δομή και κάποιες συναρτήσεις. Η δομή αφορά υποθετικούς χρήστες και έχει ως πεδία το name (όνομα χρήστη), το password (συνθηματικό) και το salt (αλάτι, που μεταφορικά σημαίνει κάτι πρόσθετο στα δεδομένα) που ο ρόλος του θα διευκρινιστεί στη συνέχεια. Ο χρήστης θα εισάγει όνομα και συνθηματικό ως λεκτικά και το συνθηματικό θα αποθηκεύεται ως η κατακερματισμένη τιμή της συνένωσης του συνθηματικού και του salt, που εδώ είναι μια τυχαία ακέραια τιμή από το 0 μέχρι το 10000. Η τιμή του salt επίσης αποθηκεύεται στη δομή και είναι ένας τρόπος έτσι ώστε η τιμή κατακερματισμού που αποθηκεύεται για το συνθηματικό να μην προκύπτει απευθείας από την εφαρμογή της συνάρτησης κατακερματισμού στο συνθηματικό που δίνεται ως λεκτικό. Αυτό συμβαίνει διότι η τιμή κατακερματισμού δημιουργείται για το συνθηματικό με προσαρτημένο στο τέλος του το salt. Ένα παράδειγμα εισαγωγής των δεδομένων στη δομή user φαίνεται στο Σχήμα 20.1. Η συνάρτηση jhash() αποτελεί υλοποίηση της συνάρτησης κατακερματισμού του Jenkins 4 και δέχεται ως ορίσματα ένα λεκτικό και το μήκος του και επιστρέφει μια ακέραια τιμή 32 bits(1). Επιπλέον, υπάρχει η συνάρτηση jhash_w(), που αποτελεί έναν συντομότερο τρόπο κλήσης της jhash(), που δέχεται ως όρισμα μόνο το λεκτικό για το οποίο ζητείται η τιμή κατακερματισμού του. Συναρτήσεις αυτής της μορφής καλούνται συναρτήσεις wrappers (περιτυλίγματα).

  1. Τυπικά θα χρησιμοποιούνταν μια κρυπτογραφική συνάρτηση κατακερματισμού, αλλά για λόγους απλότητας χρησιμοποιείται η jhash().

ch20_salt.png

Σχήμα 20.1: Παράδειγμα αποθήκευσης τιμών στη δομή user.

Ο κώδικας C περιλαμβάνεται σε ένα αρχείο επικεφαλίδας (κώδικας 20.1), ένα αρχείο με τους ορισμούς των συναρτήσεων (κώδικας 20.2) και ένα αρχείο οδηγό (κώδικας 20.3) από το οποίο ξεκινά η εκτέλεση. Ο κώδικας των τριών αρχείων παρουσιάζεται στη συνέχεια:

Κώδικας 20.1: ch20_blackbox.h - δήλωση της δομής user και των συναρτήσεων jhash() και jhash_w().
#include <stdint.h>
#include <stdlib.h>

typedef struct {
  char name[21];
  uint32_t password;
  uint32_t salt;
} user;

uint32_t jhash(const char *str, size_t len);
uint32_t jhash_w(const char *str);
Κώδικας 20.2: ch20_blackbox.c - ορισμός συναρτήσεων jhash() και jhash_w().
#include "ch20_blackbox.h"
#include <string.h>

uint32_t jhash(const char *str, size_t len) {
  uint32_t hash = 0;
  for (size_t i = 0; i < len; i++) {
    hash += str[i];
    hash += (hash << 10);
    hash ^= (hash >> 6);
  }
  hash += (hash << 3);
  hash ^= (hash >> 11);
  hash += (hash << 15);
  return hash;
}

uint32_t jhash_w(const char *str) { return jhash(str, strlen(str)); }
Κώδικας 20.3: ch20_main.c - πρόγραμμα οδηγός.
#include "ch20_blackbox.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

#define MAX_LENGTH 21 // μέγιστο μήκος λεκτικών
#define MAX_USERS 3   // πλήθος χρηστών
#define UB 10000 // ανώτατο όριο τυχαίων ακέραιων τιμών

int main(void) {
  char names[MAX_USERS][MAX_LENGTH] = {"daredevil", "happyhippo", "nerdalert"};
  char passwords[MAX_USERS][MAX_LENGTH] = {"12345", "password", "qwerty"};
  srand(1729); // ορίζουμε ως seed μια σταθερή τιμή για επαναληψιμότητα
  uint32_t salts[MAX_USERS] = {rand() % UB, rand() % UB, rand() % UB};
  char tmp[MAX_LENGTH];

  user users[MAX_USERS];
  for (size_t i = 0; i < MAX_USERS; ++i) {
    strcpy(users[i].name, names[i]);
    snprintf(tmp, MAX_LENGTH, "%u", salts[i]);
    users[i].password = jhash_w(strcat(passwords[i], tmp));
    users[i].salt = salts[i];
    printf("user = %s, password = %u salt = %u\n", users[i].name,
           users[i].password, users[i].salt);
  }

  char a_user_name[MAX_LENGTH];
  char pwd[MAX_LENGTH];
  printf("Enter user name: ");
  scanf("%20s", a_user_name);
  printf("Enter the password of user %s: ", a_user_name);
  scanf("%20s", pwd);

  bool login = false;
  for (size_t i = 0; i < MAX_USERS; ++i) {
    if (strcmp(users[i].name, a_user_name) != 0)
      continue;
    snprintf(tmp, MAX_LENGTH, "%u", users[i].salt);
    if (users[i].password == jhash_w(strcat(pwd, tmp))) {
      login = true;
      break;
    }
  }
  printf("LOGIN %s\n", login ? "SUCCESS" : "FAIL");
}

Το πρόγραμμα οδηγός ch20_main.c δημιουργεί τρεις υποθετικούς χρήστες χρησιμοποιώντας τυχαίες τιμές για τα salts, ζητά να εισαχθεί όνομα χρήστη και συνθηματικού και εμφανίζει μήνυμα σχετικά με το εάν ο συνδυασμός ονόματος χρήστη και συνθηματικού είναι σωστός ή όχι.

Ακολουθεί η μεταγλώττιση και η εκτέλεση του κώδικα:

$ gcc ch20_blackbox.c ch20_main.c ‐o ch20_main
$ ./ch20_main
user = daredevil , password = 3851113929 salt = 6234
user = happyhippo , password = 1996416210 salt = 2698
user = nerdalert , password = 4242763575 salt = 720
Enter user name: daredevil
Enter the password of user daredevil: 12345
LOGIN SUCCESS

Στη συνέχεια θα υλοποιηθεί στην Python κώδικας που θα χρησιμοποιεί τη δομή user και τις συναρτήσεις jhash() και jhash_w() του κώδικα της C και θα έχει την ίδια λειτουργικότητα με το πρόγραμμα σε C. Το ίδιο παράδειγμα θα υλοποιηθεί με τη βιβλιοθήκη ctypes και σε δύο ακόμα εκδόσεις (ABI και API) με τη βιβλιοθήκη CFFI.
Τόσο για το ctypes όσο και για την έκδοση CFFI/ΑΒΙ θα χρειαστεί να δημιουργηθεί η βιβλιοθήκη C που θα περιέχει τις υλοποιήσεις των συναρτήσεων και τη δήλωση της δομής. Αυτό γίνεται με την ακόλουθη εντολή που δημιουργεί τη βιβλιοθήκη blackbox στο αρχείο libblackbox.so για Linux:

$ gcc ‐shared ‐o libblackbox.so ch20_blackbox.c ‐fPIC

Από τη στιγμή που έχει δημιουργηθεί η βιβλιοθήκη blackbox, o κώδικας μπορεί να μεταγλωττιστεί και να εκτελεστεί ως εξής:

$ gcc ch20_main.c ‐Wl,‐rpath='${ORIGIN}' ‐L. ‐lblackbox ‐o ch20_main
$ ./ch20_main
user = daredevil , password = 3851113929 salt = 6234
user = happyhippo , password = 1996416210 salt = 2698
user = nerdalert , password = 4242763575 salt = 720
Enter user name: daredevil
Enter the password of user daredevil: 54321
LOGIN FAIL

Η εντολή μεταγλώττισης και σύνδεσης δεν μεταγλωττίζει πλέον τον πηγαίο κώδικα ch20_blackbox.c, που ορίζει τις συναρτήσεις jhash() και jhash_w(), αλλά συνδέει το εκτελέσιμο του τελικού προγράμματος με τον εκτελέσιμο κώδικα που βρίσκεται στη δυναμική βιβλιοθήκη libblackbox.so που έχει δημιουργηθεί. Ο διακόπτης ‐Wl δίνει την οδηγία να περάσουν οι επιλογές που ακολουθούν στον συνδέτη. Η επιλογή ‐rpath='${ORIGIN}' ορίζει ως βασική διαδρομή στην οποία θα αναζητηθούν βιβλιοθήκες κατά την εκτέλεση, τον κατάλογο από όπου γίνεται η εκτέλεση του προγράμματος. Ο διακόπτης ‐L. δίνει την οδηγία να αναζητηθούν βιβλιοθήκες στον τρέχοντα κατάλογο. Ο διακόπτης ‐lblackbox καθορίζει ότι η σύνδεση θα γίνει με μια βιβλιοθήκη με όνομα libblackbox (το lib στην αρχή του ονόματος εννοείται, οπότε το όνομα είναι μόνο blackbox). Αυτό σημαίνει ότι η βιβλιοθήκη θα πρέπει να βρίσκεται σε ένα αρχείο με όνομα libblackbox.so για δυναμική βιβλιοθήκη ή με όνομα libblackbox.a για στατική βιβλιοθήκη (στο συγκεκριμένο παράδειγμα η βιβλιοθήκη είναι δυναμική).
Για τους κώδικες Python που θα ακολουθήσουν τα στοιχεία υποθετικών χρηστών έχουν τοποθετηθεί στο αρχείο ch20_main_data.py (κώδικας 20.4), έτσι ώστε να μπορεί να γίνει εύκολα import όπου χρειάζεται.

Κώδικας 20.4: ch20_main_data.py - κώδικας Python που ορίζει δεδομένα υποθετικών χρηστών.
from random import randrange, seed

UB = 10000  # ανώτατο όριο τυχαίων αριθμών
seed(1729)  # ορίζουμε ως seed μια σταθερή τιμή για επαναληψιμότητα
raw_data = [
    ("daredevil", "12345", randrange(UB)),
    ("happyhippo", "password", randrange(UB)),
    (
        "nerdalert",
        "qwerty",
        randrange(UB),
    ),
]

20.2.1 Η βιβλιοθήκη ctypes

Η ctypes είναι μια εύχρηστη βιβλιοθήκη που επιτρέπει σε προγράμματα Python να προσπελαύνουν δομές και να καλούν συναρτήσεις που έχουν οριστεί σε δυναμικές βιβλιοθήκες της γλώσσας C. Η χρήση της ctypes σε έναν κώδικα Python συνήθως ξεκινά με φόρτωση μιας δυναμικής βιβλιοθήκης και τη δήλωση του τύπου ορισμάτων και του τύπου επιστροφής για τις συναρτήσεις της βιβλιοθήκης που πρόκειται να χρησιμοποιηθούν. Οι συναρτήσεις της βιβλιοθήκης καλούνται με κατάλληλα ορίσματα στον κώδικα Python και επιστρέφουν αποτελέσματα που είναι διαθέσιμα για τη συνέχεια εκτέλεσης του προγράμματος. Επίσης, μπορούν να κατασκευαστούν κλάσεις της Python που προσομοιώνουν τις δομές του κώδικα C. Στον κώδικα 20.5 παρουσιάζεται ένα σχετικό παράδειγμα.

Κώδικας 20.5: ch20_main_ctypes.py - κώδικας που χρησιμοποιεί τη βιβλιοθήκη ctypes
import ch20_main_data
import ctypes
import os
import sys

if sys.platform.startswith("win"):
    clib = ctypes.CDLL(os.path.join(os.getcwd(), "blackbox.dll"))
elif sys.platform.startswith("linux"):
    clib = ctypes.CDLL(os.path.join(os.getcwd(), "libblackbox.so"))
elif sys.platform.startswith("darwin"):
    clib = ctypes.CDLL(os.path.join(os.getcwd(), "libblackbox.dylib"))
else:
    print("Unknown OS")
    exit()

# δήλωση τύπων δεδομένων για ορίσματα και τιμές επιστροφής
clib.jhash.argtypes = [ctypes.c_char_p, ctypes.c_uint32]
clib.jhash.restype = ctypes.c_uint32
clib.jhash_w.argtypes = [ctypes.c_char_p]
clib.jhash_w.restype = ctypes.c_uint32


# δήλωση τύπου δεδομένων για τη δομή User
class User(ctypes.Structure):
    _fields_ = [
        ("name", ctypes.c_char_p),
        ("password", ctypes.c_uint32),
        ("salt", ctypes.c_uint32),
    ]


users = []
for name, password_txt, salt in ch20_main_data.raw_data:
    byte_string = (password_txt + str(salt)).encode("ascii")
    password = clib.jhash(byte_string, len(byte_string))
    u = User(name.encode("ascii"), password, ctypes.c_uint32(salt))
    print(f"user={name}, password={password}, salt={salt}")
    users.append(u)

n = input("Enter user name: ")
p = input(f"Enter the password of user {n}: ")
login = False
for u in users:
    if u.name != n.encode("ascii"):
        continue
    sp_txt = p + str(u.salt)
    c = clib.jhash_w(sp_txt.encode("ascii"))
    if c == u.password:
        login = True
        break
print("LOGIN SUCCESS") if login else print("LOGIN FAILED")

Στις γραμμές 6-11 πραγματοποιείται η φόρτωση της C βιβλιοθήκης blackbox, για παράδειγμα από το αρχείο libblackbox.so σε σύστημα Linux. Το αρχείο της βιβλιοθήκης θα πρέπει να βρίσκεται στον ίδιο κατάλογο με τον κώδικα Python που εκτελείται ή σε κατάλογο που να είναι στο PATH του συστήματος. Στις γραμμές 17-20 ορίζονται οι τύποι δεδομένων ορισμάτων και τιμών επιστροφής για τις συναρτήσεις jhash() και jhash_w(). Για παράδειγμα, τα ορίσματα της συνάρτησης clib.jhash είναι ctypes.c_char_p (δείκτης σε χαρακτήρα) και ctypes.c_uint32 (μη προσημασμένος ακέραιος 32 bits) που αντιστοιχούν στους τύπους δεδομένων της συνάρτησης jhash() στη C, char* και uint32_t αντίστοιχα.
Στις γραμμές 24-29 ορίζεται η κλάση User που αντιστοιχεί στη δομή user του προγράμματος C, όπου για κάθε πεδίο της κλάσης ορίζεται ο κατάλληλος τύπος δεδομένων σε συμφωνία με τη δομή της C. Στη γραμμή 36 δημιουργείται σε κάθε επανάληψη ένα αντικείμενο της κλάσης User που εισάγεται στη λίστα users.
Στη γραμμή 35 γίνεται η κλήση της συνάρτησης jhash() και στη γραμμή 47 γίνεται η κλήση της συνάρτησης jhash_w(). Προσοχή πρέπει να δοθεί στα ορίσματά τους, καθώς πρέπει να κωδικοποιηθούν κατάλληλα, έτσι ώστε να ταιριάζουν με τους τύπους δεδομένων που αναμένονται.

Τελικά, η εκτέλεση του ch20_main_ctypes.py γίνεται όπως στη συνέχεια και δίνει την ακόλουθη έξοδο:

$ python ch20_main_ctypes.py
user=daredevil , password=3714596007, salt=736
user=happyhippo , password=175962796, salt=6730
user=nerdalert , password=3144561193, salt=8213
Enter user name: nerdalert
Enter the password of user nerdalert: qwerty
LOGIN SUCCESS

20.2.2 Η βιβλιοθήκη CFFI

Η βιβλιοθήκη CFFI (C Foreign Function Interface) είναι μια βιβλιοθήκη που παρέχει μια διεπαφή προς συναρτήσεις της C μέσα από την Python. Ο προγραμματιστής χρειάζεται να παραθέτει εντός του κώδικα Python δηλώσεις συναρτήσεων και δομών δεδομένων απευθείας σε κώδικα C. Η CFFI διαθέτει δύο καταστάσεις λειτουργίας, το ABI (Application Binary Interface) και το API (Application Programming Interface). Στο ABI η πρόσβαση στη βιβλιοθήκη που έχει δημιουργηθεί με τον κώδικα C γίνεται στο δυαδικό επίπεδο, ενώ στο API χρησιμοποιείται ο μεταγλωττιστής της C και πραγματοποιείται ένα ενδιάμεσο βήμα μεταγλώττισης. Το API θεωρείται γενικά προτιμότερο καθώς είναι ταχύτερο και πιο σταθερό.
Αξίζει να αναφερθεί ότι ενώ η CFFI υποστηρίζει τη C, δεν υποστηρίζει τη C++. Για την C++ υπάρχουν άλλες λύσεις όπως η Python βιβλιοθήκη pybind11 (https://pybind11.readthedocs.io/en/stable/). Καθώς η CFFI είναι εξωτερική βιβλιοθήκη θα πρέπει πρώτα να εγκατασταθεί. Αυτό γίνεται εύκολα με το pip ως εξής:

$ pip install cffi

Παράδειγμα χρήσης της CFFI στην κατάσταση λειτουργίας ABI Στη συνέχεια, στον κώδικα 20.6, υλοποιείται η ίδια λειτουργικότητα όπως και στο παράδειγμα με τη βιβλιοθήκη ctypes. Για το ABI θα χρησιμοποιηθεί η βιβλιοθήκη libblackbox.so που δημιουργήθηκε παραπάνω, στο τμήμα 20.2.

Κώδικας 20.6: ch20_main_cffi_abi.py - κώδικας που χρησιμοποιεί τη βιβλιοθήκη CFFI στην κατάσταση ABI
import ch20_main_data
import cffi
import os
import sys

ffi = cffi.FFI()
if sys.platform.startswith("win"):
    clib = ffi.dlopen(os.path.join(os.getcwd(), "blackbox.dll"))
elif sys.platform.startswith("linux"):
    clib = ffi.dlopen(os.path.join(os.getcwd(), "libblackbox.so"))
elif sys.platform.startswith("darwin"):
    clib = ffi.dlopen(os.path.join(os.getcwd(), "libblackbox.dylib"))
else:
    print("Unknown OS")
    exit()

ffi.cdef("uint32_t jhash(const char *str, size_t len);")
ffi.cdef("uint32_t jhash_w(const char *str);")
ffi.cdef(
    """
         typedef struct
        {
            char name[21];
            uint32_t password;
            uint32_t salt;
        } user;
         """
)

users = []
for name, password_txt, salt in ch20_main_data.raw_data:
    v = ffi.new("user*")
    v.name = name.encode("ascii")
    v.salt = salt
    salted_password_txt = password_txt + str(salt)
    s = ffi.new("char[]", salted_password_txt.encode("ascii"))
    v.password = clib.jhash(s, len(salted_password_txt))
    print(
        f"user={ffi.string(v.name).decode('ascii')}, password={v.password}, salt={v.salt}"
    )
    users.append(v)

n = input("Enter user name: ")
p = input(f"Enter the password of user {n}: ")
login = False
for v in users:
    if ffi.string(v.name).decode("ascii") != n:
        continue
    sp_txt = p + str(v.salt)
    s = ffi.new("char[]", sp_txt.encode("ascii"))
    c = clib.jhash_w(s)
    if c == v.password:
        login = True
        break
print("LOGIN SUCCESS") if login else print("LOGIN FAILED")

Στις γραμμές 6-15 φορτώνεται το αρχείο βιβλιοθήκης, ανάλογα με το λειτουργικό σύστημα (π.χ. για ένα σύστημα Linux είναι το libblackbox.so), που πρέπει να βρίσκεται στον ίδιο κατάλογο με τον κώδικα Python που εκτελείται.
Στις γραμμές 17-18 υπάρχουν οι εντολές που γνωστοποιούν στο CFFI τις δηλώσεις των συναρτήσεων jhash() και jhash_w().
Στις γραμμές 19-28 υπάρχει η δήλωση της δομής user. Όπως και στις δηλώσεις των συναρτήσεων, ο κώδικας που χρησιμοποιείται είναι αυτούσιος C κώδικας που μπορεί να αντιγραφεί κατευθείαν από το αρχείο επικεφαλίδας ch20_blackbox.h.
Στη γραμμή 32 χρησιμοποιείται μέσα στα εισαγωγικά ο συμβολισμός δείκτη προς τη δομή user και στις επόμενες γραμμές η αναφορά v συμπληρώνεται με τιμές χρησιμοποιώντας τον συμβολισμό τελείας (dot notation).
Στη γραμμή 36 καθορίζεται ο τύπος δεδομένων και το περιεχόμενο που θα περάσει ως πρώτο όρισμα στη συνάρτηση jhash(). Στη γραμμή 37 καλείται η συνάρτηση jhash() και επιστρέφει το αποτέλεσμά της στο πεδίο password της δομής user. Στη γραμμή 51 καλείται η συνάρτηση jhash_w() και η τιμή που επιστρέφει ελέγχεται για την επιβεβαίωση της ορθότητας του password που έχει εισαχθεί.
Η εκτέλεση του ch20_main_cffi_abi.py θα δώσει την ακόλουθη έξοδο:

$ python ch20_main_cffi_abi.py
...
Enter user name: happyhippo
Enter the password of user happyhippo: password
LOGIN SUCCESS

Παράδειγμα χρήσης της CFFI στην κατάσταση λειτουργίας API Σε αυτό το παράδειγμα θα παρουσιαστεί κώδικας που επιτυγχάνει την ίδια λειτουργικότητα με τα δύο προηγούμενα παραδείγματα, όμως αυτήν τη φορά με το CFFI/API. Ο κώδικας Python χωρίζεται σε δύο αρχεία, με το πρώτο (κώδικας 20.7) να δημιουργεί τη βιβλιοθήκη μεταγλωττίζοντας τον C κώδικα και επιτρέποντας τον χειρισμό της ως module της Python. Ο δεύτερος κώδικας Python (κώδικας 20.8) είναι παρόμοιος με τον κώδικα στο παράδειγμα CFFI/ABI, αλλά πλέον είναι συντομότερος.

Κώδικας 20.7: ch20_main_cffi_api_builder.py - κώδικας δημιουργίας module με συναρτήσεις και δομές C κώδικα.
import cffi

ffi = cffi.FFI()
ffi.set_source(
    "_jhash",
    '#include "ch20_blackbox.h"',
    sources=["ch20_blackbox.c"],
)
ffi.cdef("uint32_t jhash(const char *str, size_t len);")
ffi.cdef("uint32_t jhash_w(const char *str);")
ffi.cdef(
    """
         typedef struct
        {
            char name[21];
            uint32_t password;
            uint32_t salt;
        } user;
         """
)
library = ffi.compile(verbose=True)

Στις γραμμές 4-8 ορίζεται το όνομα του module που θα δημιουργηθεί (_jhash), καθορίζεται ποια θα είναι τα αρχεία πηγαίου κώδικα που θα μεταγλωττιστούν και ποιες εντολές συμπερίληψης αρχείων επικεφαλίδων θα χρειαστεί να δοθούν.
Στις γραμμές 9-20, σε αντιστοιχία με το CFFI/ABI υπάρχουν οι δηλώσεις των συναρτήσεων jhash() και jhash_w() και της δομής user. Στη γραμμή 21 υπάρχει η εντολή που εκκινεί τη μεταγλώττιση του C κώδικα, προκειμένου να δημιουργηθεί η βιβλιοθήκη και το module που θα χρησιμοποιηθεί στη συνέχεια.
Η επιτυχής εκτέλεση του ch20_main_cffi_abi_builder.py εμφανίζει τα ακόλουθα μηνύματα:

$ python ch20_main_cffi_api_builder.py
generating ./_jhash.c
the current directory is '/.../src'

Ο κώδικας 20.8 χρησιμοποιεί το module που δημιουργήθηκε έτσι ώστε να επιτευχθεί η ίδια λειτουργικότητα με τα προηγούμενα παραδείγματα.

Κώδικας 20.8: ch20_main_cffi_api.py - κώδικας που χρησιμοποιεί τη βιβλιοθήκη CFFI στην κατάσταση API.
import ch20_main_data
from _jhash import lib, ffi

users = []
for name, password_txt, salt in ch20_main_data.raw_data:
    v = ffi.new("user*")
    v.name = name.encode("ascii")
    v.salt = salt
    salted_password_txt = password_txt + str(salt)
    s = ffi.new("char[]", salted_password_txt.encode("ascii"))
    v.password = lib.jhash(s, len(salted_password_txt))
    print(
        f"user={ffi.string(v.name).decode('ascii')}, password={v.password}, salt={v.salt}"
    )
    users.append(v)

n = input("Enter user name: ")
p = input(f"Enter the password of user {n}: ")
login = False
for v in users:
    if ffi.string(v.name).decode("ascii") != n:
        continue
    sp_txt = p + str(v.salt)
    s = ffi.new("char[]", sp_txt.encode("ascii"))
    c = lib.jhash_w(s)
    if c == v.password:
        login = True
        break
print("LOGIN SUCCESS") if login else print("LOGIN FAILED")

Στη γραμμή 2 γίνονται import τα προκαθορισμένα αντικείμενα lib και ffi του νέου module _jhash. Στις γραμμές 11 και 25 καλούνται ως μέλη του lib οι συναρτήσεις jhash() και jhash_w(). Τελικά, η εκτέλεση του ch20_main_cffi_api.py δίνει την ίδια έξοδο όπως και στα προηγούμενα παραδείγματα.

$ python ch20_main_cffi_api.py
...
Enter user name: happyhippo
Enter the password of user happyhippo: password
LOGIN SUCCESS

20.2.3 Δημιουργία GUIs με Python και "λογικής" με C

Όπως αναφέρθηκε στο Κεφάλαιο 19 η Python μπορεί να χρησιμοποιηθεί για να δημιουργηθεί ένα γραφικό περιβάλλον διεπαφής, ενώ ο κώδικας που παράγει τα αποτελέσματα μπορεί να είναι σε C. Ένα σχετικό παράδειγμα θα παρουσιαστεί στη συνέχεια όπου μια συνάρτηση που έχει γραφεί σε C εκτελεί μια υπολογιστικά χρονοβόρα (υποθετικά) διαδικασία και επιστρέφει έναν πίνακα αποτελεσμάτων. Για την εμφάνιση των αποτελεσμάτων χρησιμοποιείται η Python και με τη χρήση της βιβλιοθήκης Tkinter δημιουργείται ένα παράθυρο με ένα πλήκτρο και χώρο εμφάνισης ενός γραφήματος. Όταν ο χρήστης πατήσει το πλήκτρο, εκτελείται ο κώδικας C και τα αποτελέσματα που επιστρέφει χρησιμοποιούνται για να σχεδιαστεί ένα γράφημα με τη βιβλιοθήκη matplotlib. Στο παράδειγμα αυτό θα χρησιμοποιηθεί η διασύνδεση CFFI/ABI και οι εντολές μεταγλώττισης και εκτέλεσης που θα παρουσιαστούν θα αφορούν τα Windows, αλλά η διαδικασία είναι παρόμοια και σε Linux ή MacOS. Ο κώδικας 20.9 είναι ο κώδικας C του παραδείγματος, ενώ ο κώδικας 20.10 είναι κώδικας Python που καλεί τον κώδικα C και δημιουργεί το παράθυρο της εφαρμογής.

```{.c title="Κώδικας 20.9: ch20_gui_logic.c - κώδικας σε C που επιστρέφει έναν πίνακα 5 τιμών, προκειμένου να απεικονιστεί στη συνέχεια διαγραμματικά." linenums="1"}

include

int *get_values(void) { static int values[5] = {4, 7, 2, 3, 9}; return values; }

Για να δημιουργηθεί η βιβλιοθήκη <span class="p-style">gui_logic</span> θα πρέπει να δοθεί η ακόλουθη εντολή μεταγλώττισης:

gcc ‐shared ‐o gui_logic.dll ch20_gui_logic.c ‐fPIC

Το αρχείο <span class="p-style">gui_logic.dll</span> που δημιουργείται, χρησιμοποιείται στον κώδικα 20.10 που ακολουθεί:

```{.py title="Κώδικας 20.10: ch20_gui_cffi.py - κώδικας Python που καλεί κώδικα C, κάνοντας χρήση της βιβλιοθήκης CFFI/ABI." linenums="1"}
import tkinter as tk
import os
import sys
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from cffi import FFI

ffi = FFI()  # δημιουργία ενός αντικειμένου FFI
if sys.platform.startswith("win"):
    clib = ffi.dlopen(os.path.join(os.getcwd(), "gui_logic.dll"))
elif sys.platform.startswith("linux"):
    clib = ffi.dlopen(os.path.join(os.getcwd(), "libgui_logic.so"))
elif sys.platform.startswith("darwin"):
    clib = ffi.dlopen(os.path.join(os.getcwd(), "libgui_logic.dylib"))
else:
    print("Unknown OS")
    exit()

ffi.cdef("int* get_values();")  # δήλωση του πρωτοτύπου της συνάρτησης C


# ανάκτηση τιμών από τη C και μετατροπή τους σε λίστα Python
def get_c_values():
    c_values = clib.get_values()
    return [c_values[i] for i in range(5)]


# ανανέωση του γραφήματος
def update_plot():
    data = get_c_values()
    ax.clear()
    ax.plot(data, marker="o")
    canvas.draw()


# δημιουργία του παραθύρου
root = tk.Tk()
root.title("CFFI Tkinter App")

# δημιουργία του γραφήματος
fig = Figure(figsize=(5, 4), dpi=100)
ax = fig.add_subplot(111)

# ενσωμάτωση του γραφήματος στο παράθυρο
canvas = FigureCanvasTkAgg(fig, master=root)
canvas_widget = canvas.get_tk_widget()
canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

# προσθήκη κουμπιού στο παράθυρο
button = tk.Button(root, text="Get C Values", command=update_plot)
button.pack(side=tk.BOTTOM)

root.mainloop()

Θα πρέπει να έχει προηγηθεί η εγκατάσταση των βιβλιοθηκών της Python, cffi και matplotlib, που μπορεί να γίνει με τις ακόλουθες εντολές:

> pip install cffi
> pip install matplotlib

Η εκτέλεση του προγράμματος γίνεται με την ακόλουθη εντολή:

> python ch20_gui_cffi.py

και εμφανίζεται η οθόνη του Σχήματος 20.2 όταν ο χρήστης πατήσει το πλήκτρο “Get C Values”.

ch20_gui.png

Σχήμα 20.2: GUI που έχει δημιουργηθεί με την Python και που λαμβάνει τιμές από εκτέλεση κώδικα C.

20.3 Κλήση κώδικα Python από κώδικα C

Η Python επιτρέπει τη συγγραφή σύντομων προγραμμάτων για τη λύση προβλημάτων. Αυτός μπορεί να είναι ένας λόγος για να επιδιωχθεί η κλήση κώδικα Python από C. Για παράδειγμα η αντιστροφή ενός λεκτικού είναι πολύ απλή υπόθεση στην Python, όπως φαίνεται στον κώδικα 20.11 που ορίζει τη συνάρτηση reverse_string(), που επιστρέφει αντεστραμμένο το όρισμα που δέχεται.

Κώδικας 20.11: ch20_script.py - κώδικας σε Python που θα κληθεί από τη C.
def reverse_string(string):
    return string[::-1]

Έστω ότι επιθυμούμε να καλέσουμε Python εντολές μέσα από τη C. Αυτό μπορεί να γίνει, όπως συμβαίνει στον κώδικα 20.12 που ξεκινά κάνοντας include την επικεφαλίδα Python.h, η οποία δίνει πρόσβαση σε όλες τις λειτουργίες του διερμηνευτή της Python.

Κώδικας 20.12: ch20_c_calls_python.c - κώδικας σε C που καλεί Python κώδικα.
#include <Python.h>

int main() {
  // Αρχικοποίηση του διερμηνευτή της Python
  Py_Initialize();
  // Εύρεση του Python script στον ίδιο κατάλογο
  PyRun_SimpleString("import sys");
  PyRun_SimpleString("sys.path.append(\".\")");

  // Import του module ch20_script και της συνάρτησης reverse_string
  PyObject *moduleName = PyUnicode_FromString("ch20_script");
  PyObject *module = PyImport_Import(moduleName);
  PyObject *functionName = PyUnicode_FromString("reverse_string");
  PyObject *function = PyObject_GetAttr(module, functionName);

  // Δημιουργία ορισμάτων και κλήση της συνάρτησης Python
  PyObject *args = PyTuple_New(1);
  PyTuple_SetItem(args, 0, PyUnicode_FromString("Hello, world!"));
  PyObject *result = PyObject_CallObject(function, args);

  // Λήψη του ανεστραμμένου λεκτικού από το αποτέλεσμα που επιστρέφει η Python
  const char *reversedString = PyUnicode_AsUTF8(result);
  printf("%s\n", reversedString);

  // Απελευθέρωση μνήμης
  Py_DECREF(moduleName);
  Py_DECREF(module);
  Py_DECREF(functionName);
  Py_DECREF(function);
  Py_DECREF(args);
  Py_DECREF(result);

  // Τερματισμός του διερμηνευτή της Python
  Py_Finalize();

  return 0;
}

Στη γραμμή 5 αρχικοποιείται ο διερμηνευτής της Python και δεσμεύονται πόροι που θα αποδεσμευθούν στο τέλος του προγράμματος.
Στις γραμμές 7-8 ορίζεται ότι η αναζήτηση του κώδικα Python που βρίσκεται σε ένα αρχείο .py θα γίνεται στον ίδιο κατάλογο με το τρέχον αρχείο κώδικα.
Στις γραμμές 11-14 φορτώνεται το module ch20_script.py και η συνάρτησή του, reverse_string().
Στις γραμμές 17-18 δημιουργείται το όρισμα που θα περάσει στη συνάρτηση.
Στη γραμμή 19 καλείται η συνάρτηση με το όρισμα που προετοιμάστηκε και επιστρέφεται το αποτέλεσμα στη μεταβλητή result ως ένας δείκτης προς PyObject.
Στη γραμμή 22 μετατρέπεται το αποτέλεσμα σε τιμή που μπορεί να εκτυπωθεί από τη συνάρτηση printf().
Τέλος, στις γραμμές 26-31 απελευθερώνονται πόροι που έχουν δεσμευθεί νωρίτερα.
Η μεταγλώττιση και σύνδεση του κώδικα C θα πρέπει να προσδιορίζει τη θέση όπου θα βρεθεί το αρχείο επικεφαλίδας Python.h με τον διακόπτη ‐I, καθώς και τη βιβλιοθήκη που δίνει πρόσβαση στον διερμηνευτή της Python με τον διακόπτη ‐l.

$ gcc ‐o ch20_c_calls_python ch20_c_calls_python.c ‐I/usr/include/python3.10
↪ ‐lpython3.10

Η εκτέλεση του προγράμματος δίνει ως αποτελέσματα το κείμενο !dlrow ,olleH. Περισσότερες πληροφορίες για την ενσωμάτωση κώδικα Python μέσα σε κώδικα C μπορούν να αναζητηθούν στο 5.

20.4 Ασκήσεις

Άσκηση 1
Χρησιμοποιήστε τη συνάρτηση rand() της τυπικής βιβλιοθήκης της C για να δημιουργήσετε μια λίστα 10 τυχαίων τιμών στο διάστημα [0,1) που θα εκτυπωθεί από κώδικα Python με τη βιβλιοθήκη ctypes.

Άσκηση 2
Θεωρήστε ότι πετάτε βελάκια σε ένα τετράγωνο 2 x 2 εκατοστών που στο κέντρο του υπάρχει τοποθετημένος ένας κύκλος με διάμετρο 2 εκατοστά. Το εμβαδόν του τετραγώνου είναι 4 τετραγωνικά εκατοστά, ενώ ο κύκλος έχει εμβαδόν 𝜋 τετραγωνικά εκατοστά. Η πιθανότητα να προσγειωθεί ένα βελάκι εντός του κύκλου είναι ο λόγος του εμβαδού του κύκλου προς το εμβαδόν του τετραγώνου. Αν πετάξετε έναν μεγάλο αριθμό από βελάκια και υπολογίσετε το πλήθος από τα βελάκια που πέφτουν μέσα στον κύκλο έναντι του συνολικού πλήθους ρίψεων, η τιμή αυτή θα πρέπει να είναι 𝜋/4. Χρησιμοποιήστε τον παραπάνω τρόπο για να υπολογίσετε μια προσέγγιση του 𝜋 στην Python για ένα εκατομμύριο βελάκια. Υπολογίστε την επιτάχυνση του χρόνου εκτέλεσης αν χρησιμοποιηθεί λύση που καλεί κώδικα C με το CFFI/API για την πραγματοποίηση των υπολογισμών.

Άσκηση 3
Γράψτε ένα πρόγραμμα σε Python που να εμφανίζει ένα GUI μέσω του οποίου θα ζητά από τον χρήστη να εισάγει έναν αριθμό. Το πρόγραμμα να εμφανίζει τις ρίζες του αριθμού από την τετραγωνική μέχρι και τη ρίζα δέκατης τάξης. Για τον υπολογισμό των ριζών να χρησιμοποιηθεί ο C κώδικας 20.13, που υπολογίζει τη ρίζα ενός αριθμού με βάση την τάξη που ζητείται(1). Χρησιμοποιήστε CFFI/API για τη διασύνδεση του Python και του C κώδικα.

  1. Η μέθοδος που χρησιμοποιείται είναι η μέθοδος Newton-Raphson που περιγράφεται στο https://en.wikipedia.org/wiki/Nth_root#Computing_principal_roots
Κώδικας 20.13: ch20_e3.c - συνάρτηση υπολογισμού της n-οστής ρίζας ενός αριθμού με τη μέθοδο Newton-Raphson.
#include <math.h>

double nth_root(double x, int n) {
  if (x < 0 && n % 2 == 0) {
    // Δεν υπάρχει πραγματική ρίζα για αρνητικό x όταν ο n είναι άρτιος
    return NAN; // Not A Number, ορίζεται στο math.h
  }

  double guess = x;
  double epsilon = 0.000001; // Όριο ακρίβειας
  double delta;
  do {
    double nx_minus_1 = pow(guess, n - 1);
    delta = (x - nx_minus_1 * guess) / (n * nx_minus_1);
    guess += delta;
  } while (fabs(delta) > epsilon);
  return guess;
}

Άσκηση 4
Δίνεται ο κώδικας 20.14, σε Python, που ορίζει τη συνάρτηση fibonacci(n), η οποία επιστρέφει το n-οστό όρο της ακολουθίας Fibonacci. Καλέστε τη συνάρτηση μέσα από κώδικα C, προκειμένου να εμφανιστούν οι 10 πρώτοι όροι της ακολουθίας.

Κώδικας 20.14: ch20_e4.py - συνάρτηση σε Python που επιστρέφει μια λίστα με τους n πρώτους όρους της ακολουθίας Fibonacci.
1
2
3
4
5
6
7
def fibonacci(n):
    sequence = []
    a, b = 0, 1
    for _ in range(n):
        sequence.append(a)
        a, b = b, a + b
    return sequence

  1. ctypes — A foreign function library for Python. https://docs.python.org/3/library/ctypes.html. Accessed: 2023-06-01. 

  2. CFFI documentation. https://cffi.readthedocs.io/en/latest/. Accessed: 2023-06-01. 

  3. Extending Python with C or C++. https://docs.python.org/3/extending/extending.html. Accessed: 2023-06-01. 

  4. Bob Jenkins. The Hash. http://www.burtleburtle.net/bob/hash/doobs.html. Accessed:2023-06-01. 

  5. Embedding Python in Another Application. https://docs.python.org/3/extending/embedding.html. Accessed: 2023-06-01.