Про то как вызывать Python из C написал в прошлой статье, теперь поговорим как делать наоборот и вызывать C/C++ из Python. Раз начал писать об этом, то раскроем всю тему до конца. Тем более, что ни чего сложного здесь нет тоже.
C
Здесь все просто, python умеет вызывать C функции без каких либо проблем.
test.c:
#include "test.h"
int a = 5;
double b = 5.12345;
char c = 'X';
int
func_ret_int(int val) {
printf("get func_ret_int: %d\n", val);
return val;
}
double
func_ret_double(double val) {
printf("get func_ret_double: %f\n", val);
return val;
}
char *
func_ret_str(char *val) {
printf("get func_ret_str: %s\n", val);
return val;
}
char
func_many_args(int val1, double val2, char val3, short val4) {
printf("get func_many_args: int - %d, double - %f, char - %c, short - %d\n", val1, val2, val3, val4);
return val3;
}
test.h:
#ifndef _TEST_H_
#define _TEST_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
#include <string.h>
#include <unistd.h>
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)ж
#ifdef __cplusplus
}
#endif
#endif /* _TEST_H_ */
Как компилировать :
gcc -fPIC -shared -o libtest.so test.c
Исходник компилируется в динамическую библиотеку и готов к бою.
Переходим к python. В примере показывается как передать аргументы функции, получить результат работы от функции, а так же как получить и изменить значения глобальных переменных.
main.py:
#!/usr/bin/python3
#-*- coding: utf-8 -*-
import ctypes
# Загрузка библиотеки
test = ctypes.CDLL('./objs/libtest.so')
##
# Работа с функциями
##
# Указываем, что функция возвращает int
test.func_ret_int.restype = ctypes.c_int
# Указываем, что функция принимает аргумент int
test.func_ret_int.argtypes = [ctypes.c_int, ]
# Указываем, что функция возвращает double
test.func_ret_double.restype = ctypes.c_double
# Указываем, что функция принимает аргумент double
test.func_ret_double.argtypes = [ctypes.c_double]
# Указываем, что функция возвращает char *
test.func_ret_str.restype = ctypes.c_char_p
# Указываем, что функция принимает аргумент char *
test.func_ret_str.argtypes = [ctypes.POINTER(ctypes.c_char), ]
# Указываем, что функция возвращает char
test.func_many_args.restype = ctypes.c_char
# Указываем, что функция принимает аргументы int, double. char, short
test.func_many_args.argtypes = [ctypes.c_int, ctypes.c_double, ctypes.c_char, ctypes.c_short]
print('ret func_ret_int: ', test.func_ret_int(101))
print('ret func_ret_double: ', test.func_ret_double(12.123456789))
# Необходимо строку привести к массиву байтов, затем полученный массив байтов приводим к строке.
print('ret func_ret_str: ', test.func_ret_str('Hello!'.encode('utf-8')).decode("utf-8") )
print('ret func_many_args: ', test.func_many_args(15, 18.1617, 'X'.encode('utf-8'), 32000).decode("utf-8"))
print()
##
# Работа с переменными
##
# Указываем, что переменная типа int
a = ctypes.c_int.in_dll(test, "a")
print('ret a: ', a.value)
# Изменяем значение переменной.
a.value = 22
a = ctypes.c_int.in_dll(test, "a")
print('ret a: ', a.value)
# Указываем, что переменная типа double
b = ctypes.c_double.in_dll(test, "b")
print('ret b: ', b.value)
# Указываем, что переменная типа char
c = ctypes.c_char.in_dll(test, "c")
print('ret c: ', c.value.decode("utf-8"))
Все возможные типы данных и их обозначения можно посмотреть в документации python.
Работа со структурами
C — объявление структуры в test.h:
typedef struct test_st_s test_st_t;
struct test_st_s {
int val1;
double val2;
char 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;
}
Python:
import sys
import struct
# Объявляем структуру в Python аналогичную в C
class test_st_t(ctypes.Structure):
_fields_ = [('val1', ctypes.c_int),
('val2', ctypes.c_double),
('val3', ctypes.c_char)]
# Указываем, что функция возвращает void *
test.func_ret_struct.restype = ctypes.c_void_p
# Указываем, что функция принимает аргумент void *
test.func_ret_struct.argtypes = [ctypes.c_void_p]
# Создаем структуру
test_st = test_st_t(19, 3.5, 'Z'.encode('utf-8'))
# Python None == Null C
ret = test.func_ret_struct(None)
print('ret func_ret_struct: ', ret) # Если передали None, то его и получим назад
ret = test.func_ret_struct(ctypes.byref(test_st))
# Полученные данные из C
print('ret val1 = {}\nret val2 = {}\nret val3 = {}'.format(ret.contents.val1, ret.contents.val2, ret.contents.val3.decode("utf-8")))
Как полученные данные скопировать в python структуру напрямую не додумался, кто знает напишите.
C++
Здесь немного сложнее, т.к. ctypes может только работать с C функциями. Это для нас не проблема, просто C обвяжем код C++.
Методы класса C++ и обвязка на C:
#include "test.hpp"
/*
* Методы класса
*/
std::string test::ret_str(std::string val) {
std::cout << "get ret_str: " << val << std::endl;
return val;
}
int test::ret_int(int val) {
std::cout << "get ret_int: " << val << std::endl;
return val;
}
double test::ret_double(double val) {
std::cout << "get ret_double: " << val << std::endl;
return val;
}
/*
* Обвязка C для методов класса C++
*/
// Создаем класс test, и получаем указатель на него.
test *test_new() {
return new test();
}
// Удаляем класс test.
void test_del(test *test) {
delete test;
}
/*
* Вызов методов класса.
*/
// Обертка над методом ret_str
char *test_ret_str(test *test, char *val) {
// char * к std::string
std::string str = test->ret_str(std::string(val));
// std::string к char *
char *ret = new char[str.length() + 1];
strcpy(ret, str.c_str());
return ret;
}
// Обертка над методом ret_int
int test_ret_int(test *test, int val) {
return test->ret_int(val);
}
// Обертка над методом ret_double
double test_ret_double(test *test, double val) {
return test->ret_double(val);
}
/*
* Получение переменных класса.
*/
// Обертка для получения a
int test_get_a(test *test) {
return test->a;
}
// Обертка для получения b
double test_get_b(test *test) {
return test->b;
}
// Обертка для получения c
char test_get_c(test *test) {
return test->c;
}
Но есть один нюанс, обвязку надо объявить как extern C. Чтобы ++ компилятор не перегрузил имена функций обвязки. Если он это сделает, то мы не сможем через ctypes работать с нашими функциями.
test.hpp:
#include <iostream>
#include <string.h>
class test {
public:
int a = 5;
double b = 5.12345;
char c = 'X';
std::string ret_str(std::string val);
int ret_int(int val);
double ret_double(double val);
};
#ifdef __cplusplus
extern "C" {
#endif
test *test_new();
void test_del(test *test);
char *test_ret_str(test *test, char *val);
int test_ret_int(test *test, int val);
double test_ret_double(test *test, double val);
int test_get_a(test *test);
double test_get_b(test *test);
char test_get_c(test *test);
#ifdef __cplusplus
}
#endif
Как компилировать:
g++ -fPIC -shared -o libtestpp.so test.cpp
С python все так же просто.
# Загрузка библиотеки
testpp = ctypes.CDLL('./objs/libtestpp.so')
# Указываем, что функция возвращает указатель
testpp.test_new.restype = ctypes.c_void_p
# Создание класса test
test = testpp.test_new()
##
# Работа с методами
##
# Указываем, что функция возвращает char *
testpp.test_ret_str.restype = ctypes.c_char_p
# Указываем, что функция принимает аргумент void * и char *
testpp.test_ret_str.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
# Указываем, что функция возвращает int
testpp.test_ret_int.restype = ctypes.c_int
# Указываем, что функция принимает аргумент void * и int
testpp.test_ret_int.argtypes = [ctypes.c_void_p, ctypes.c_int]
# Указываем, что функция возвращает double
testpp.test_ret_double.restype = ctypes.c_double
# Указываем, что функция принимает аргумент void * и double
testpp.test_ret_double.argtypes = [ctypes.c_void_p, ctypes.c_double]
print('Работа с методами:')
# В качестве 1-ого аргумента передаем указатель на наш класс
print('ret test_ret_str: ', testpp.test_ret_str(test, 'Hello!'.encode('utf-8')).decode("utf-8"))
print('ret test_ret_int: ', testpp.test_ret_int(test, 123))
print('ret test_ret_double: ', testpp.test_ret_double(test, 9.87654321))
##
# Работа с переменными
##
# Указываем, что функция возвращает int
testpp.test_get_a.restype = ctypes.c_int
# Указываем, что функция возвращает double
testpp.test_get_b.restype = ctypes.c_double
# Указываем, что функция возвращает char
testpp.test_get_c.restype = ctypes.c_char
print('\nРабота с переменными:')
print('ret test_get_a: ', testpp.test_get_a(test))
print('ret test_get_b: ', testpp.test_get_b(test))
print('ret test_get_c: ', testpp.test_get_c(test).decode("utf-8"))
# Удаляем класс
testpp.test_del(test)
Код постарался закомментировать понятно, что бы здесь писать поменьше )
Надеюсь будет полезно.
Благодарность
DollaR84 за его помощь.
Ссылки
Исходные коды примеров
Предыдущая статья Python из C
Комментарии (28)
Palich239
06.09.2019 19:13За статью спасибо, хотелось иметь по теме прямо такую справку: простую, понятную, но не упрощенную.
П. С. А то везде пишут, что это просто, но нигде толком не пишут КАК. Мне еще это долго не понадобится, но теперь есть уверенность, что в случае чего — все произойдет быстроJessy_James Автор
06.09.2019 19:27Мне самому это год назад понадобилось и не думаю, что в скором времени нужно будет ). Но решил написать такие readme на будущее, не только для себя.
ciklop
06.09.2019 21:18+1Через ctypes можно вызывать с++ и без extern C. Но это будет "непереносимый код". Так как name mangling не стандартизован, и каждый компилятор будет генерировать свои экспортируемые имена функций. Если их подсмотреть в библиотеке — их также можно спокойно вызвать.
Под linux посмотреть имена функций можно с помощью nm. Для test.cpp без extern C будет примерно так:
nm -D libtestcpp.so ... 00000000000014cc T _Z12test_ret_intP4testi 00000000000013ab T _Z12test_ret_strP4testPc 00000000000014ee T _Z15test_ret_doubleP4testd 0000000000001370 T _Z8test_newv ...
И вот они, имена функций.
Под Windows можно посмотреть с помощью
link /dump /exports libtestcpp.dll
Jessy_James Автор
06.09.2019 21:22Хотел про nm писать, но не стал. Когда makefile писал, то накосячил и libtestpp.so собирал из test.c. С помощью nm разбирался, почему не находит вызываемые функции.
Про то что без extern прокатит писать не стал, а вы не поленились ). Спасибо.
Closius
07.09.2019 14:13А почему бы не использовать Boost.Python?
Jessy_James Автор
08.09.2019 00:36Ни когда бустом не пользовался. Может плохо, может хорошо, может пора и на него посмотреть )
ser-mk
08.09.2019 01:14А чем он лучше для вызова обычных C ф-ций?
Мне кажется проще Ctypes уже нетiroln
08.09.2019 02:54Для обычных C функций лучше CFFI.
https://qr.ae/TWy0op
Boost.Python я вообще не рекомендую использовать ни для чего. Монстроузный и неудобный (удобнее, конечно, чем голый CPython API, но значительно менее удобный чем более современные штуки).
maxood
07.09.2019 16:19Думаю, не плохо было б добавить описание работы со structure и union, кроме базовых типов.
DollaR84
07.09.2019 22:26+1В своей статье: Передача двумерных списков из python в DLL
несколько показал работу со Structure
Jessy_James Автор
09.09.2019 16:36Сделал структуру, передача и получение. С получением помучился…
DollaR84
09.09.2019 17:12Как-то вы довольно сложно сделали работу со структурами и ее возвращение. Я как-то делал гораздо проще. Посмотрю свои исходники, попробую привести пример.
DollaR84
09.09.2019 19:26Как полученные данные скопировать в python структуру напрямую не додумался, кто знает напишите.
Вот что меня смутило в вашем коде, буфер и копирование данных из C структуры в python/ Зачем лишние буфера, операции копирования и прочее. Так как dll может оперировать со структурами ctypes, созданными в самом python, так же можно оперировать структурами, созданными в dll, из самого python.
Вот вы в своей функции вернули тоже, что получили на вход. Не знаю насколько это нужно, поэтому упрощу вашу функцию. Пусть она на вход ничего не получает, а возвращает структуру, созданную в ней.
Тогда в C:
test_st_t * func_ret_struct(void) { test_st_t *res = new test_st_t; res->val1 = 19; res->val2 = 3.5; res->val3 = 'z'; return res; }
В коде python:
test.func_ret_struct.argtypes = [ctypes.c_void_p] test.func_ret_struct.restype = ctypes.POINTER(test_st_t) ret = test.func_ret_struct() print('val1 = {}\nval2 = {}\nval3 = {}'.format(ret.contents.val1, ret.contents.val2, ret.contents.val3))
Волшебное слово contents позволяет получить прямой доступ к данным структуры по указателю.
Ну и конечно не забыть потом передать этот указатель в dll на удаление.
Постарался максимально приблизить к вашему примеру, но если где есть неточности, думаю поправите.
grafalex
08.09.2019 23:11Спасибо за статью.
А как же cython? там вообще без танцев с бубном и ctypes, и синтаксис почти питоновский.
Я тут чуток затронул эту тему в своей статье про автотестирование.Jessy_James Автор
08.09.2019 23:55Мне это понадобилось когда проект был почти готов, поэтому смысла что-то еще делать не было. Шел по легкому пути.
ser-mk
09.09.2019 20:52+1Как полученные данные скопировать в python структуру напрямую не додумался, кто знает напишите.
Возможно можно сделать по аналогии как здесь через ctypes.cast(). Но пока нет возможности проверить
Стоит наверно еще сказать про выравнивание структур. Не так давно столкнулся с такой особенностью.
class GPIO_InitTypeDef(ctypes.Structure): _fields_ = [ ('GPIO_Pin',ctypes.c_uint16), ('GPIO_Speed',ctypes.c_uint8), ('GPIO_Mode',ctypes.c_uint8)] ctypes.sizeof(GPIO_InitTypeDef)
sizeof возвращает размер 4 байта.
А С++ библиотека собрана с выравниванием структур по 4 байта, т.е. итоговый размер будет 12
typedef struct { uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; }GPIO_InitTypeDef; sizeof(GPIO_InitTypeDef) // 12 bytes
И можно наткнуться на очень неприятные баги)Jessy_James Автор
09.09.2019 20:56+1Спасибо, мы как раз с автором этой статьи переписываемся. Он показал как делать. Попозже у себя поправлю.
DollaR84
Совсем недавно для одного своего проекта на python делал динамическую библиотеку, чтобы вынести в нее нагруженный двойной цикл, который сильно тормозил python, будучи написанным в нем, и отрабатывающий практически мгновенно в библиотеке. Так вот, чтобы не возиться с decode и encode для строк, можно использовать в плюсах тип wchar_t, а в ctypes python тип c_wchar_p, для указателя на строку wchar_t. К такому типу третий python автоматически приводит строки utf-8.
Jessy_James Автор
Попробую в понедельник.
alec_kalinin
А вы не пробовали для этих целей использовать Numba. С простыми двойными циклами она должна справляться очень хорошо.
DollaR84
Ну я бы не сказал, что он был простым, нужно было работать с двумерным списком, плюс еще два одномерных списка, плюс вызов несколько вспомогательных функций. Также я реализовал был даже многопоточность для этого цикла, с помощью пула multiprocessing? но, к сожалению, там распараллеливание не помогало, сам python сильно долго обходил цикл. А вот передача всех данных в dll позволило мгновенно обработать все данные, даже несмотря на необходимость преобразования классов объектов в структуры ctypes. Даже была идея написать сюда маленькую статейку как создавать двухмерные списки структур для передачи в dll? но подумал что специалисты с хабра и так это знают и напишут зачем я публикую очевидное из документации. Хотя я сам прилично успел голову поломать, когда разбирался со всеми особенностями этого действия.
alec_kalinin
Мне было бы интересно поглядеть на реализацию. Со своей стороны я бы попробовал это реализовать на cython и numba, потом оценить насколько все это хорошо работает и насколько красивое решение получилось.
DollaR84
Хорошо, я попробую выдрать этот кусок кода из проекта и организовать его отдельное функционирование. Но мне понадобиться некоторое время, так как там используется множество различных объектов. Напишу обертки генераторы всего, что нужно для работы цикла. Как будет готово, я вам отпишусь.
alec_kalinin
Спасибо!