Използване на C код в Python#

План на лекцията:

  • Как работят C библиотеките ?

  • Съпоставяне на типове от C в Python

  • Простичка функция

  • Структури

  • Масиви и указатели

  • C++ код

Как работят C библиотеките ?#

Съществуват два вида C библиотеки:

  • Статични библиотеки

  • Динамични библиотеки

Статичните библиотеки (с разширение .a или .lib) се свързват към програмата по време на компилация на кода. Това означава, че кодът на библиотеката се “копира” в програмата. Това е по-бързо, но не е възможно да се променя кода на библиотеката без да се компилира отново програмата.

Динамичните библиотеки (с разширение .so, .dll или .dylib) се зареждат по време на изпълнение на програмата. Това означава, че кодът на библиотеката се зарежда в паметта по време на изпълнение на програмата. Това е по-бавно, но е възможно да се променя кода на библиотеката без да се компилира отново програмата.

В тази лекция ще използваме динамични библиотеки.

Съпоставяне на типовете от C в Python#

Основната библиотека, чрез която ще реализираме връзката между C код и Python, е ctypes. В нея е всичко необходимо за използването на външен C код в Python.

Както знаем, типовете в Python не са същите както в C. Python дефинира типове, имащи за цел да представят типовете в C. Те са разделени на три категории:

  1. Прости (fundamental) типове

  2. Сложни (structural) типове

  3. Масиви и указатели

Прости типове#

Простите типове са:

  • c_char - съотвества на C типа char

  • c_char_p - съотвества на C типа char*

  • c_double - съотвества на C типа double

  • c_float - съотвества на C типа float

  • c_int - съотвества на C типа int

  • c_longlong - съотвества на C типа long long

  • c_short - съотвества на C типа short

  • c_size_t- съотвества на C типа size_t

  • c_uint - съотвества на C типа unsigned int

  • c_void_p - съотвества на C типа void*

  • c_bool - съотвества на C типа bool

  • и други… (може да откриете пълния списък тук)

Когато една C функция върне прост тип, този тип автоматично се конвертира в подходящ Python тип.

Сложни типове#

Освен простите типове, ctypes ни предлага възможност да работим с union и struct типове.

За да работим с union, можем да използваме абстрактния клас ctypes.Union. За да работим със структури, можем да използваме абстрактния клас ctypes.Structure.

Масиви и указатели#

За работа с масиви и указатели, ctypes ни предлага класовете ctypes.Array и ctypes._Pointer/ctypes.POINTER. Ще разгледаме примери малко по-долу.

Простичка функция#

За да демонстрираме практически как можем да използваме C код в Python, ще съзадем C функция, която ще има за цел да събере две числа.

За целта ще използваме вече написан C код, както и предварително подготвен CMakeLists.txt файл. За да изпълните успешно кода, ще ви е нужен инсталиран CMake, както и C компилатор.

Може да разгледате C кода тук.

!cat "C/simple_function/sum.h"
!cat "C/simple_function/sum.c"

Единствената съществена разлика на този етап в C кода, е използването на ключовата дума extern в началото на декларацията на функцията. extern променя видимостта на функция, така че да е видима във външни библиотеки.

Компилираме нашия код до C библиотека с помощта на cmake и make командите

!cd "C/simple_function" && cmake . && make

След като вече имаме libSimpleFunction.so файла, можем да пристъпим към зареждането ѝ в Python.

Основната библиотека, която ще използваме е ctypes. Можем да заредим външна C библиотека с помощта на ctypes.CDLL функцията. Тя връща специален обект CDLL обект, който съдържа заредената бибилиотека.

След като успешно заредим нашата библиотека, в новополучената ни инстанция ще се появят атрибути, които са класове от тип _FuncPtr - те ще сочат към функциите в нашата C библиотека. По поздразибране те приемат всякакви ctypes аргументи и връщат резултат по подразбиране. Можем да специфицираме аргументите и типа на резултата чрез атрибутите argtypes и restype.

import ctypes
import os

lib_path = os.path.join("C", "simple_function", "libSimpleFunction.so")

def setup_lib(path: str) -> ctypes.CDLL:
    lib = ctypes.CDLL(path)
    print(type(lib.sum))
    lib.sum.argtypes = [ctypes.c_int, ctypes.c_int]
    lib.sum.restype = ctypes.c_int
    return lib

lib = setup_lib(lib_path)
print(type(lib))

result = lib.sum(2, 3)
print(type(result), result)
import ctypes
import os

lib_path = os.path.join("C", "simple_function", "libSimpleFunction.so")

def setup_lib(path: str) -> ctypes.CDLL:
    lib = ctypes.CDLL(path)
    print(type(lib.sum))
    lib.sum.argtypes = [ctypes.c_int, ctypes.c_int]
    lib.sum.restype = ctypes.c_int
    return lib

lib = setup_lib(lib_path)
print(type(lib))
a = int(input("Enter first number: "))
b = int(input("Enter second number: "))

result = lib.sum(a, b)

print(type(result))
print("{} + {} = {}".format(a, b, result))

Нека разгледаме в детайли кода.

Използваме ctypes библиотеката за работа с външни C библиотеки. За зареждането на библиотеката използваме CDLL конструктора, като му подаваме пътя към библиотеката.

След това е необходимо да посочим типа на аргументите и типа на резултата. Понеже работим с int променливи, типа на аргументите и резултата са c_int.

С получения обект, можем да извикваме функциите, които са отбелязани като extern в C кода.

Структури#

Нека усложним нещата една идея - нека се опитаме да подаваме C структури към нашия Python code.

Ще дефинираме структурата Rational, която ще моделира рационално число, съставено от две цели числа - числител и знаменател. Ще дефинираме също функции за събиране, изваждане, умножение, деление, както и функция, която конструира нов Rational обект на базата на две цели числа.

Целия C код може да разгледате тук.

!cd "C/structs" && cmake . && make

Разликата с предишния пример е, че този път имаме C структура. Как бихме могли да представим нашата структура Rational в Python ? Класът ctypes.Structure ни служи като база, върху която да създадем нашия Rational клас в Python. Чрез специалната клас-променлива _fields_ можем да зададем от какви променливи е създадена нашата структура - в случая на Rational, две променливи от тип int.

import ctypes

class Rational(ctypes.Structure):
    _fields_ = [("numerator", ctypes.c_int), ("denominator", ctypes.c_int)]

    def __str__(self):
        return str(self.numerator) + "/" + str(self.denominator)

Понеже нашите C структури са представени като Python класове, ние може да дефинираме допълнителни Python методи в тях - в примера сме дефинирали метода __str__ - него можем да използваме когато работим с нашата C структура през Python.

Зареждането на останалите функции става по познатия ни начин.

import ctypes
import os


def setup_lib(path) -> ctypes.CDLL:
    lib = ctypes.CDLL(path)

    lib.add.argtypes = [Rational, Rational]
    lib.add.restype = Rational

    lib.subtract.argtypes = [Rational, Rational]
    lib.subtract.restype = Rational

    lib.multiply.argtypes = [Rational, Rational]
    lib.multiply.restype = Rational

    lib.divide.argtypes = [Rational, Rational]
    lib.divide.restype = Rational

    lib.build.argtypes = [ctypes.c_int, ctypes.c_int]
    lib.build.restype = Rational

    return lib

lib_path = os.path.join("C", "structs", "libStructs.so")

lib = setup_lib(lib_path)

first_num = int(input("Enter first number numerator: "))
first_denom = int(input("Enter first number denominator: "))

first_rational = lib.build(first_num, first_denom)


second_num = int(input("Enter second number numerator: "))
second_denom = int(input("Enter second number denominator: "))
second_rational = lib.build(second_num, second_denom)

add = lib.add(first_rational, second_rational)
subtract = lib.subtract(first_rational, second_rational)
multiply = lib.multiply(first_rational, second_rational)
divide = lib.divide(first_rational, second_rational)

print(f"{first_rational} + {second_rational} = {add}")
print(f"{first_rational} - {second_rational} = {subtract}")
print(f"{first_rational} * {second_rational} = {multiply}")
print(f"{first_rational} / {second_rational} = {divide}")

Работа с масиви и указатели#

В Python можем да работим с масиви и указатели. Ако искаме да създадем C-стил масив в Python, трябва да създадем нов обект, който е със стойност типа на масива, който искаме да създадем. Например, ако искаме да създадем масив от 10 цели числа, трябва да направим следното:

arr = (ctypes.c_int * 10)()
import ctypes

IntArray = ctypes.c_int * 10
arr = IntArray(7, 8, 2, 3, -3, 12, 14, 9, 0, 1)

for i in range(10):
    print(arr[i])
import ctypes

arr = (ctypes.c_int * 10)()

for i in range(10):
    arr[i] = i + 1


for i in range(10):
    print(arr[i])

print(arr)

Можем да направим и указател, като това става чрез класа ctypes.POINTER. Например, ако искаме да създадем указател към целочислен тип, трябва да направим следното:

ctypes.POINTER(ctypes.c_int)

Нека разгледаме следния пример - ще реализираме динамичен масив на C, който ще използваме в Python.

dynamic_array.h#

#ifndef ARRAYS_POINTERS_H
#define ARRAYS_POINTERS_H

#include <stdio.h>
#include <stdlib.h>

struct DynamicArray{
    int* items;
    int capacity;
    int size;
};

extern struct DynamicArray create(const int capacity);
extern struct DynamicArray create_from_raw(const int* items, const int capacity);
extern void add(struct DynamicArray* instance, const int item);
extern int get(const struct DynamicArray* instance, const int index);
extern void resize(struct DynamicArray* instance);
extern void destruct(struct DynamicArray* instance);

#endif

dynamic_array.c#

#include "dynamic_array.h"

struct DynamicArray create(const int capacity) {
    struct DynamicArray instance;

    instance.capacity = capacity;
    instance.size = 0;
    instance.items = malloc(sizeof(int) * capacity);
    
    return instance;
}

struct DynamicArray create_from_raw(const int* items, const int capacity) {
    struct DynamicArray instance;

    instance.capacity = capacity;
    instance.size = capacity;
    instance.items = malloc(sizeof(int) * capacity);

    for (int i = 0; i < capacity; i++) {
        instance.items[i] = items[i];
    }

    return instance;
}

void add(struct DynamicArray* instance, const int item) {
    if (instance->capacity == instance->size) {
        resize(instance);
    }

    instance->items[instance->size++] = item;
}

int get(const struct DynamicArray* instance, const int index) {
    if (index >= instance->size || index < 0) {
        printf("Index %d out of bounds for size %d", index, instance->size);
    }

    return instance->items[index];
}

void resize(struct DynamicArray* instance) {
    instance->capacity *= 2;
    instance->items = realloc(instance->items, sizeof(int) * instance->capacity);
}

void destruct(struct DynamicArray* instance) {
    free(instance->items);
}
!cd "C/arrays_pointers" && cmake . && make
import ctypes
import os

class DynamicArray(ctypes.Structure):
    _fields_ = [("size", ctypes.c_int), ("capacity", ctypes.c_int), 
                ("items", ctypes.POINTER(ctypes.c_int))]


def setup_lib(path) -> ctypes.CDLL:
    lib = ctypes.CDLL(path)

    lib.create.argtypes = [ctypes.c_int]
    lib.create.restype = DynamicArray

    lib.create_from_raw.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
    lib.create_from_raw.restype = DynamicArray

    lib.add.argtypes = [ctypes.POINTER(DynamicArray), ctypes.c_int]

    lib.get.argtypes = [ctypes.POINTER(DynamicArray), ctypes.c_int]
    lib.get.restype = ctypes.c_int

    lib.resize.argtypes = [ctypes.POINTER(DynamicArray)]

    lib.destruct.argtypes = [ctypes.POINTER(DynamicArray)]

    return lib

lib_path = os.path.join("C", "arrays_pointers", "libDynamicArray.so")

lib = setup_lib(lib_path)

# Dynamic array from C
array = lib.create(5)

for i in range(10):
    lib.add(array, i * 2)

for i in range(10):
    print(lib.get(array, i), end=' ')

print('')

lib.destruct(array)


IntArray = ctypes.c_int * 10
c_arr = IntArray(7, 8, 2, 3, -3, 12, 14, 9, 0, 1)

arr = lib.create_from_raw(c_arr, 10)

for i in range(10):
    print(lib.get(arr, i), end=' ')

lib.destruct(arr)

C++ код#

Нямаме директен достъп до C++ код от Python. За да можем да използваме C++ код в Python, трябва да направим междинен C код, който да бъде използван от Python. Това е доста по-сложно от C, но също така ни дава доста по-голяма свобода в работата си.

Нека пренапишем нашия DynamicArray от C на C++.

dynamic_array.h#

class DynamicArray {
    public:
        DynamicArray(const int capacity);
        DynamicArray(const int* items, const int capacity);

        void add(const int item);
        int get(const int index) const;
        void resize();

        ~DynamicArray();
    private:
        int* items;
        int capacity;
        int size;
};

extern "C" {
    DynamicArray* create(const int capacity) {
        return new DynamicArray(capacity);
    }
    DynamicArray* create_from_raw(const int* items, const int capacity) {
        return new DynamicArray(items, capacity);
    }
    void add(DynamicArray* instance, const int item) {
        instance->add(item);
    }
    int get(const DynamicArray* instance, const int index) {
        return instance->get(index);
    }
    void resize(DynamicArray* instance) {
        instance->resize();
    }
    void destruct(DynamicArray* instance) {
        delete instance;
    }
}

dynamic_array.cpp#

#include "dynamic_array.h"

DynamicArray::DynamicArray(const int capacity): capacity(capacity), size(0) {
    this->items = new int[this->capacity];
}
DynamicArray::DynamicArray(const int* items, const int capacity): capacity(capacity) {
    this->items = new int[this->capacity];
    for (int i = 0; i < this->capacity; i++) {
        this->items[i] = items[i];
    }
    this->size = this->capacity;
}

void DynamicArray::add(const int item) {
    if (this->size == this->capacity) {
        this->resize();
    }
    this->items[this->size] = item;
    this->size++;
}
int DynamicArray::get(const int index) const {
    if (index < 0 || index >= this->size) {
        throw "Index out of bounds";
    }
    return this->items[index];
}
void DynamicArray::resize() {
    int* new_items = new int[this->capacity * 2];
    for (int i = 0; i < this->capacity; i++) {
        new_items[i] = this->items[i];
    }
    delete[] this->items;
    this->items = new_items;
    this->capacity *= 2;
}

DynamicArray::~DynamicArray() {
    delete[] this->items;
}

В Python ще използваме само C интерфейса.

!cd "C++" && cmake . && make
import ctypes
import os

class DynamicArray(ctypes.Structure):
    pass

def setup_lib(path) -> ctypes.CDLL:
    lib = ctypes.CDLL(path)

    lib.create.argtypes = [ctypes.c_int]
    lib.create.restype = ctypes.POINTER(DynamicArray)

    lib.create_from_raw.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
    lib.create_from_raw.restype = ctypes.POINTER(DynamicArray)

    lib.add.argtypes = [ctypes.POINTER(DynamicArray), ctypes.c_int]

    lib.get.argtypes = [ctypes.POINTER(DynamicArray), ctypes.c_int]
    lib.get.restype = ctypes.c_int

    lib.resize.argtypes = [ctypes.POINTER(DynamicArray)]

    lib.destruct.argtypes = [ctypes.POINTER(DynamicArray)]

    return lib

lib_path = os.path.join("C++", "libDynamicArray.so")

lib = setup_lib(lib_path)

# Dynamic array from C
array = lib.create(5)

for i in range(10):
    lib.add(array, i * 2)

for i in range(10):
    print(lib.get(array, i), end=' ')

print('')

lib.destruct(array)


IntArray = ctypes.c_int * 10
c_arr = IntArray(7, 8, 2, 3, -3, 12, 14, 9, 0, 1)

arr = lib.create_from_raw(c_arr, 10)

for i in range(10):
    print(lib.get(arr, i), end=' ')

lib.destruct(arr)