C/C++ из Python (CFFI, pybind11)
- пятница, 20 сентября 2019 г. в 00:35:55
Продолжаем тему как вызывать C/C++ из Python3. Теперь используем библиотеки cffi, pybind11. Способ через ctypes был рассмотрен в предыдущей статье.
Тестовая библиотека для демонстрации работы с глобальными переменными, структурами и функциями с аргументами различных типов.
test.h
typedef struct test_st_s test_st_t;
extern int a;
extern double b;
extern char c;
int func_ret_int(int val);
double func_ret_double(double val);
char *func_ret_str(char *val);
char func_many_args(int val1, double val2, char val3, short val4);
test_st_t *func_ret_struct(test_st_t *test_st);
struct test_st_s {
int val1;
double val2;
char val3;
};
test.c
#include <stdio.h>
#include <stdlib.h>
#include "test.h"
int a = 5;
double b = 5.12345;
char c = 'X';
int
func_ret_int(int val) {
printf("C get func_ret_int: %d\n", val);
return val;
}
double
func_ret_double(double val) {
printf("C get func_ret_double: %f\n", val);
return val;
}
char *
func_ret_str(char *val) {
printf("C get func_ret_str: %s\n", val);
return val;
}
char
func_many_args(int val1, double val2, char val3, short val4) {
printf("C get func_many_args: int - %d, double - %f, char - %c, short - %d\n", val1, val2, val3, val4);
return val3;
}
test_st_t *
func_ret_struct(test_st_t *test_st) {
if (test_st) {
printf("C get test_st: val1 - %d, val2 - %f, val3 - %c\n", test_st->val1, test_st->val2, test_st->val3);
}
return test_st;
}
Библиотека точно такая же как в статье про ctypes.
Это библиотека для работы исключительно с C. Из описания этой библиотеки:
Interact with almost any C code from Python
Какую-то часть от этого почти удалось найти.
Для эксперимента использовалась версия 1.12.3, почитать про неё можно здесь.
Немного об этой библиотеки в 2-х словах, CFFI генерирует поверх нашей библиотеки свою обвязку и компилирует её в библиотеку с которой мы и будем работать.
pip3 install cffi
Скрипт сборки, который будет собирать обвязку вокруг нашей библиотеки.
build.py
import os
import cffi
if __name__ == "__main__":
ffi = cffi.FFI()
# Путь расположение скрипта
PATH = os.getcwd()
# test.h заголовочный файл нашей библиотеки
# указываем путь до него относительно build.py
with open(os.path.join(PATH, "src/c/test.h")) as f:
ffi.cdef(f.read())
ffi.set_source("_test", # имя библиотеки собранной cffi, добавляем префикс _
# Подключаем test.h, указываем путь относительно собираемой _test
'#include "../src/c/test.h"',
# Где находится libtest.so (Исходная собранная библиотека)
# относительно _test.cpython-36m-x86_64-linux-gnu.so (создается CFFI)
libraries=[os.path.join(PATH, "lib/test"), "./test"],
library_dirs=[PATH, 'objs/'],
)
# компилируем _test в папку lib
ffi.compile(tmpdir='./lib')
Пример работы с C из Python через CFFI:
from cffi import FFI
import sys
import time
# пути до модуля _test
sys.path.append('.')
sys.path.append('lib/')
sys.path.append('../../lib/')
# подключаем модуль
import _test
###
## C
###
print("CFFI\n")
print("C\n")
start_time = time.time()
##
# Работа с функциями
##
print('Работа с функциями:')
print('ret func_ret_int: ', _test.lib.func_ret_int(101))
print('ret func_ret_double: ', _test.lib.func_ret_double(12.123456789))
# Необходимо строку привести из cdata к массиву байтов, и массив байтов к строке.
print('ret func_ret_str: ', _test.ffi.string(_test.lib.func_ret_str('Hello!'.encode('utf-8'))).decode("utf-8"))
print('ret func_many_args: ', _test.lib.func_many_args(15, 18.1617, 'X'.encode('utf-8'), 32000).decode("utf-8"))
##
# Работа с переменными
##
print('\nРабота с переменными:')
print('ret a: ', _test.lib.a)
# Изменяем значение переменной.
_test.lib.a = 22
print('new a: ', _test.lib.a)
print('ret b: ', _test.lib.b)
print('ret c: ', _test.lib.c.decode("utf-8"))
##
# Работа со структурами
##
print('\nРабота со структурами:')
# Создаем структуру и заполняем её
test_st = _test.ffi.new("test_st_t *")
test_st.val1 = 5
test_st.val2 = 5.1234567
test_st.val3 = 'Z'.encode('utf-8')
ret = _test.lib.func_ret_struct(test_st)
# Полученные данные из C
print('ret val1 = {}\nret val2 = {}\nret val3 = {}'.format(ret.val1, ret.val2, ret.val3.decode("utf-8")))
# Время работы
print("--- %s seconds ---" % (time.time() - start_time))
Для работы с C++ кодом, нужно написать C обвязку для него. В статье про способ через ctypes описано как это сделать. Ссылка внизу.
Плюсы:
Минусы:
#ifdef __cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif
#ifndef _TEST_H_
#define _TEST_H_
...
#endif /* _TEST_H_ */
pybind11 напротив разработана специально для работы с C++. Для эксперимента использовалась версия 2.3.0, почитать про неё можно здесь.
pip3 install pybind11
Нужно написать скрипт сборки нашей библиотеки.
build.py
import pybind11
from distutils.core import setup, Extension
ext_modules = [
Extension(
'_test', # имя библиотеки собранной pybind11
['src/c/test.cpp'], # Тестовый файлик который компилируем
include_dirs=[pybind11.get_include()], # не забываем добавить инклюды pybind11
language='c++', # Указываем язык
extra_compile_args=['-std=c++11'], # флаг с++11
),
]
setup(
name='_test', # имя библиотеки собранной pybind11
version='1.0.0',
author='djvu',
author_email='djvu@inbox.ru',
description='pybind11 extension',
ext_modules=ext_modules,
requires=['pybind11'], # Указываем зависимость от pybind11
package_dir = {'': 'lib'}
)
Выполняем его:
python3 setup.py build --build-lib=./lib
В исходники библиотеки нужно добавить:
#include <pybind11/pybind11.h>
PYBIND11_MODULE(_test, m)
namespace py = pybind11;
// _test имя нашего модуля
PYBIND11_MODULE(_test, m) {
/*
* Функции библиотеки
*/
m.def("func_ret_int", &func_ret_int);
m.def("func_ret_double", &func_ret_double);
m.def("func_ret_str", &func_ret_str);
m.def("func_many_args", &func_many_args);
m.def("func_ret_struct", &func_ret_struct);
/*
* Глобальные переменные библиотеки
*/
m.attr("a") = a;
m.attr("b") = b;
m.attr("c") = c;
/*
* Структуры
*/
py::class_<test_st_t>(m, "test_st_t")
.def(py::init()) // Указываем конструктор. Которого у структуры нет, но он нужен Python
.def_readwrite("val1", &test_st_t::val1) // переменные структуры
.def_readwrite("val2", &test_st_t::val2)
.def_readwrite("val3", &test_st_t::val3);
};
Пример работы с C из Python через pybind11:
import sys
import time
# Пути до модуля _test
sys.path.append('lib/')
# подключаем модуль
import _test
###
## C
###
print("pybind11\n")
print("C\n")
start_time = time.time()
##
# Работа с функциями
##
print('Работа с функциями:')
print('ret func_ret_int: ', _test.func_ret_int(101))
print('ret func_ret_double: ', _test.func_ret_double(12.123456789))
# Необходимо строку привести из cdata к массиву байтов, и массив байтов к строке.
print('ret func_ret_str: ', _test.func_ret_str('Hello!'.encode('utf-8')))
print('ret func_many_args: ', _test.func_many_args(15, 18.1617, 'X'.encode('utf-8'), 32000))
##
# Работа с переменными
##
print('\nРабота с переменными:')
print('ret a: ', _test.a)
# Изменяем значение переменной.
_test.a = 22
print('new a: ', _test.a)
print('ret b: ', _test.b)
print('ret c: ', _test.c)
##
# Работа со структурами
##
print('\nРабота со структурами:')
# Создаем структуру и заполняем её
_test_st = _test.test_st_t()
#print(dir(_test_st))
_test_st.val1 = 5
_test_st.val2 = 5.1234567
_test_st.val3 = 'Z'.encode('utf-8')
ret = _test.func_ret_struct(_test_st)
# Полученные данные из C
print('ret val1 = {}\nret val2 = {}\nret val3 = {}'.format(ret.val1, ret.val2, ret.val3))
# Время работы
print("--- %s seconds ---" % (time.time() - start_time))
Плюсы:
Минусы: