main

Про то как вызывать 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)


  1. DollaR84
    06.09.2019 17:05

    Совсем недавно для одного своего проекта на python делал динамическую библиотеку, чтобы вынести в нее нагруженный двойной цикл, который сильно тормозил python, будучи написанным в нем, и отрабатывающий практически мгновенно в библиотеке. Так вот, чтобы не возиться с decode и encode для строк, можно использовать в плюсах тип wchar_t, а в ctypes python тип c_wchar_p, для указателя на строку wchar_t. К такому типу третий python автоматически приводит строки utf-8.


    1. Jessy_James Автор
      06.09.2019 17:10

      Попробую в понедельник.


    1. alec_kalinin
      06.09.2019 21:13

      А вы не пробовали для этих целей использовать Numba. С простыми двойными циклами она должна справляться очень хорошо.


      1. DollaR84
        06.09.2019 22:42

        Ну я бы не сказал, что он был простым, нужно было работать с двумерным списком, плюс еще два одномерных списка, плюс вызов несколько вспомогательных функций. Также я реализовал был даже многопоточность для этого цикла, с помощью пула multiprocessing? но, к сожалению, там распараллеливание не помогало, сам python сильно долго обходил цикл. А вот передача всех данных в dll позволило мгновенно обработать все данные, даже несмотря на необходимость преобразования классов объектов в структуры ctypes. Даже была идея написать сюда маленькую статейку как создавать двухмерные списки структур для передачи в dll? но подумал что специалисты с хабра и так это знают и напишут зачем я публикую очевидное из документации. Хотя я сам прилично успел голову поломать, когда разбирался со всеми особенностями этого действия.


        1. alec_kalinin
          06.09.2019 23:47

          Мне было бы интересно поглядеть на реализацию. Со своей стороны я бы попробовал это реализовать на cython и numba, потом оценить насколько все это хорошо работает и насколько красивое решение получилось.


          1. DollaR84
            07.09.2019 11:12

            Хорошо, я попробую выдрать этот кусок кода из проекта и организовать его отдельное функционирование. Но мне понадобиться некоторое время, так как там используется множество различных объектов. Напишу обертки генераторы всего, что нужно для работы цикла. Как будет готово, я вам отпишусь.


            1. alec_kalinin
              07.09.2019 13:43

              Спасибо!


  1. Palich239
    06.09.2019 19:13

    За статью спасибо, хотелось иметь по теме прямо такую справку: простую, понятную, но не упрощенную.
    П. С. А то везде пишут, что это просто, но нигде толком не пишут КАК. Мне еще это долго не понадобится, но теперь есть уверенность, что в случае чего — все произойдет быстро


    1. Jessy_James Автор
      06.09.2019 19:27

      Мне самому это год назад понадобилось и не думаю, что в скором времени нужно будет ). Но решил написать такие readme на будущее, не только для себя.


  1. 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


    1. Jessy_James Автор
      06.09.2019 21:22

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

      Про то что без extern прокатит писать не стал, а вы не поленились ). Спасибо.


  1. Closius
    07.09.2019 14:13

    А почему бы не использовать Boost.Python?


    1. Jessy_James Автор
      08.09.2019 00:36

      Ни когда бустом не пользовался. Может плохо, может хорошо, может пора и на него посмотреть )


    1. iroln
      08.09.2019 01:09

      Лучше pybind11


    1. ser-mk
      08.09.2019 01:14

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


      1. iroln
        08.09.2019 02:54

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


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


  1. maxood
    07.09.2019 16:19

    Думаю, не плохо было б добавить описание работы со structure и union, кроме базовых типов.


    1. DollaR84
      07.09.2019 22:26
      +1

      В своей статье: Передача двумерных списков из python в DLL
      несколько показал работу со Structure


    1. Jessy_James Автор
      08.09.2019 00:35

      Моё упущение, постараюсь сделать.


    1. Jessy_James Автор
      09.09.2019 16:36

      Сделал структуру, передача и получение. С получением помучился…


      1. DollaR84
        09.09.2019 17:12

        Как-то вы довольно сложно сделали работу со структурами и ее возвращение. Я как-то делал гораздо проще. Посмотрю свои исходники, попробую привести пример.


      1. 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 на удаление.
        Постарался максимально приблизить к вашему примеру, но если где есть неточности, думаю поправите.


  1. iroln
    08.09.2019 01:12

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


  1. grafalex
    08.09.2019 23:11

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

    А как же cython? там вообще без танцев с бубном и ctypes, и синтаксис почти питоновский.
    Я тут чуток затронул эту тему в своей статье про автотестирование.


    1. Jessy_James Автор
      08.09.2019 23:55

      Мне это понадобилось когда проект был почти готов, поэтому смысла что-то еще делать не было. Шел по легкому пути.


      1. grafalex
        09.09.2019 09:38

        А, ну тогда ок


  1. 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
    

    И можно наткнуться на очень неприятные баги)


    1. Jessy_James Автор
      09.09.2019 20:56
      +1

      Спасибо, мы как раз с автором этой статьи переписываемся. Он показал как делать. Попозже у себя поправлю.