Използване на C код в Python#
План на лекцията:
Как работят C библиотеките ?
Съпоставяне на типове от C в Python
Простичка функция
Структури
Масиви и указатели
C++ код
Как работят C библиотеките ?#
Съществуват два вида C библиотеки:
Статични библиотеки
Динамични библиотеки
Статичните библиотеки (с разширение .a
или .lib
) се свързват към програмата по време на компилация на кода. Това означава, че кодът на библиотеката се “копира” в програмата. Това е по-бързо, но не е възможно да се променя кода на библиотеката без да се компилира отново програмата.
Динамичните библиотеки (с разширение .so
, .dll
или .dylib
) се зареждат по време на изпълнение на програмата. Това означава, че кодът на библиотеката се зарежда в паметта по време на изпълнение на програмата. Това е по-бавно, но е възможно да се променя кода на библиотеката без да се компилира отново програмата.
В тази лекция ще използваме динамични библиотеки.
Съпоставяне на типовете от C в Python#
Основната библиотека, чрез която ще реализираме връзката между C код и Python, е ctypes
. В нея е всичко необходимо за използването на външен C код в Python.
Както знаем, типовете в Python не са същите както в C. Python дефинира типове, имащи за цел да представят типовете в C. Те са разделени на три категории:
Прости (fundamental) типове
Сложни (structural) типове
Масиви и указатели
Прости типове#
Простите типове са:
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)