C/C++ из Python (ctypes)

  • Tutorial
main

Про то как вызывать Python из C написал в прошлой статье, теперь поговорим как делать наоборот и вызывать C/C++ из Python3. Раз начал писать об этом, то раскроем всю тему до конца. Тем более, что ни чего сложного здесь нет тоже.


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)]

# Указываем, что функция возвращает test_st_t *
test.func_ret_struct.restype = ctypes.POINTER(test_st_t)
# Указываем, что функция принимает аргумент 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")))

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) 

Плюсы и минусы ctypes


Плюсы:


  • Можно подключать любую уже скомпилированную C библиотеку

Минусы:


  • В Python нужно описывать, что C что функции возвращают и принимают в качестве аргументов.

Код постарался закомментировать понятно, что бы здесь писать поменьше )


Надеюсь будет полезно.


Благодарность


DollaR84 за его помощь.


Ссылки


Исходные коды примеров
Предыдущая статья Python из C

Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 31

    0
    Совсем недавно для одного своего проекта на python делал динамическую библиотеку, чтобы вынести в нее нагруженный двойной цикл, который сильно тормозил python, будучи написанным в нем, и отрабатывающий практически мгновенно в библиотеке. Так вот, чтобы не возиться с decode и encode для строк, можно использовать в плюсах тип wchar_t, а в ctypes python тип c_wchar_p, для указателя на строку wchar_t. К такому типу третий python автоматически приводит строки utf-8.
      0
      Попробую в понедельник.
        0
        А вы не пробовали для этих целей использовать Numba. С простыми двойными циклами она должна справляться очень хорошо.
          0
          Ну я бы не сказал, что он был простым, нужно было работать с двумерным списком, плюс еще два одномерных списка, плюс вызов несколько вспомогательных функций. Также я реализовал был даже многопоточность для этого цикла, с помощью пула multiprocessing? но, к сожалению, там распараллеливание не помогало, сам python сильно долго обходил цикл. А вот передача всех данных в dll позволило мгновенно обработать все данные, даже несмотря на необходимость преобразования классов объектов в структуры ctypes. Даже была идея написать сюда маленькую статейку как создавать двухмерные списки структур для передачи в dll? но подумал что специалисты с хабра и так это знают и напишут зачем я публикую очевидное из документации. Хотя я сам прилично успел голову поломать, когда разбирался со всеми особенностями этого действия.
            +1
            Мне было бы интересно поглядеть на реализацию. Со своей стороны я бы попробовал это реализовать на cython и numba, потом оценить насколько все это хорошо работает и насколько красивое решение получилось.
              +1
              Хорошо, я попробую выдрать этот кусок кода из проекта и организовать его отдельное функционирование. Но мне понадобиться некоторое время, так как там используется множество различных объектов. Напишу обертки генераторы всего, что нужно для работы цикла. Как будет готово, я вам отпишусь.
      +2
      За статью спасибо, хотелось иметь по теме прямо такую справку: простую, понятную, но не упрощенную.
      П. С. А то везде пишут, что это просто, но нигде толком не пишут КАК. Мне еще это долго не понадобится, но теперь есть уверенность, что в случае чего — все произойдет быстро
        0
        Мне самому это год назад понадобилось и не думаю, что в скором времени нужно будет ). Но решил написать такие readme на будущее, не только для себя.
        +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

          0
          Хотел про nm писать, но не стал. Когда makefile писал, то накосячил и libtestpp.so собирал из test.c. С помощью nm разбирался, почему не находит вызываемые функции.

          Про то что без extern прокатит писать не стал, а вы не поленились ). Спасибо.
          +1
          А почему бы не использовать Boost.Python?
            0
            Ни когда бустом не пользовался. Может плохо, может хорошо, может пора и на него посмотреть )
              +3

              Лучше pybind11

                0
                Спасибо интересно!
                0
                А чем он лучше для вызова обычных C ф-ций?
                Мне кажется проще Ctypes уже нет
                  +1

                  Для обычных C функций лучше CFFI.
                  https://qr.ae/TWy0op


                  Boost.Python я вообще не рекомендую использовать ни для чего. Монстроузный и неудобный (удобнее, конечно, чем голый CPython API, но значительно менее удобный чем более современные штуки).

                +3
                Думаю, не плохо было б добавить описание работы со structure и union, кроме базовых типов.
                  +1
                  В своей статье: Передача двумерных списков из python в DLL
                  несколько показал работу со Structure
                    0
                    Моё упущение, постараюсь сделать.
                      0
                      Сделал структуру, передача и получение. С получением помучился…
                        0
                        Как-то вы довольно сложно сделали работу со структурами и ее возвращение. Я как-то делал гораздо проще. Посмотрю свои исходники, попробую привести пример.
                          0
                          Как полученные данные скопировать в 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 на удаление.
                          Постарался максимально приблизить к вашему примеру, но если где есть неточности, думаю поправите.
                        +1

                        Всё это здорово, конечно, но на самом деле это уже давно не нужно, потому что есть pybind11 и CFFI. Эти инструменты разительно упрощают embedding/extending для Python без возни с медленным ctypes.

                          0
                          Спасибо за статью.

                          А как же cython? там вообще без танцев с бубном и ctypes, и синтаксис почти питоновский.
                          Я тут чуток затронул эту тему в своей статье про автотестирование.
                            0
                            Мне это понадобилось когда проект был почти готов, поэтому смысла что-то еще делать не было. Шел по легкому пути.
                              0
                              А, ну тогда ок
                            +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
                            

                            И можно наткнуться на очень неприятные баги)
                              +1
                              Спасибо, мы как раз с автором этой статьи переписываемся. Он показал как делать. Попозже у себя поправлю.
                              0
                              А где освобождается память выделнная в ф-ии test_ret_str(test *test, char *val) ???
                                0
                                Надо написать ;)

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое