Всем привет. Решил несколько дополнить статью C/C++ из Python.


Передача стандартных типов, таких как int, bool, float и так далее довольно проста, но мало необходима. С такими данными быстро справится и сам python, и вряд ли у кого-то возникнет необходимость вынесения части такого кода в библиотеку C/C++.


А вот передача больших массивов данных, или еще лучше двумерных массивов данных, или даже двумерных массивов объектов.


Тут уже все не так очевидно, и есть ряд вещей, которые думаю можно осветить для тех кто хочет существенно ускорить трудные для интерпретатора python участки кода.


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


Сразу приведу исходный код файлов библиотеки.


py_list2c_array.h


#ifndef _PY_LIST_2_C_ARRAY_H_
#define _PY_LIST_2_C_ARRAY_H_

#include <stdio.h>

typedef struct {
    int value;
    wchar_t* name;
} Item;

extern "C" __declspec(dllexport)
int sum_diagonal(Item** field, size_t size);

#endif

py_list2c_array.cpp


Тут также все стандартно, разве что отмечу использование функции wprintf() для печати строк типа wchar_t*.


// py_list2c_array.cpp: определяет экспортированные функции для приложения DLL.
//

#include "stdafx.h"
#include "py_list2c_array.h"

extern "C" __declspec(dllexport)
int sum_diagonal(Item** field, size_t size) {
    int result = 0;
    for(size_t i=0; i<size;++i) {
        for(size_t j=0; j<size; ++j) {
            if(i == j) {
                result += field[i][j].value;
                wprintf(L"%s\n", field[i][j].name);
            }
        }
    }
    return result;
}

py_list2c_array.py


А теперь самое главное. Приведу исходный код python скрипта с описанием важных моментов.


import ctypes

class PyItem:
    def __init__(self, value, name):
        self.value = value
        self.name = name

class CItem(ctypes.Structure):
    _fields_ = [
                 ('value', ctypes.c_int),
                 ('name', ctypes.c_wchar_p)
                ]

def create_list(size):
    return [[PyItem(int(str(i+1)+str(j+1)), 'item{}{}'.format(i+1, j+1)) for j in range(size)] for i in range(size)]

def py_list2c_array(py_list, size):
    rowType = CItem * size
    resultType = ctypes.POINTER(CItem) * size
    result = resultType()
    for i in range(size):
        row = rowType()
        for j in range(size):
            row[j] = CItem()
            row[j].value = py_list[i][j].value
            row[j].name = ctypes.c_wchar_p(py_list[i][j].name)
        result[i] = ctypes.cast(row, ctypes.POINTER(CItem))
    return ctypes.cast(result, ctypes.POINTER(ctypes.POINTER(CItem)))

if __name__ == '__main__':
    sLib = ctypes.cdll.LoadLibrary('./py_list2c_array.dll')

    size = 4
    py_list = create_list(size)
    c_array = py_list2c_array(py_list, size)

    sLib.sum_diagonal.argtypes = [ctypes.POINTER(ctypes.POINTER(CItem)), ctypes.c_size_t]
    sLib.sum_diagonal.restype = ctypes.c_int
    result = sLib.sum_diagonal(c_array, ctypes.c_size_t(size))
    print('Результат: {}'.format(result))

Детали


Рассмотрим некоторые особенности создания массивов ctypes. Для этого подробнее разберем функцию преобразования списка в массив py_list2c_array. Сначала необходимо указать типы.


Тип каждой строки массива определяется как тип элемента, умноженный на количество элементов.


rowType = CItem * size


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


Чуть ниже поясню про ctypes.POINTER().resultType = ctypes.POINTER(CItem) * size


Далее создаем результирующий массив.result = resultType()


А в цикле создаем каждую строку, как одномерный массив.row = rowType()


Далее во вложенном цикле создаем каждый элемент массива и присваиваем значения структуре из списка объектов python.row[j] = CItem()


row[j].value = py_list[i][j].value
row[j].name = ctypes.c_wchar_p(py_list[i][j].name)


Затем каждую созданную строку с элементами следует преобразовать к типу указателя на массив объектов и присвоить в ячейку результирующего массива.


Про функцию ctypes.cast() напишу чуть ниже.result[i] = ctypes.cast(row, ctypes.POINTER(CItem))
Ну и конечно преобразовать весь массив к указателю.return ctypes.cast(result, ctypes.POINTER(ctypes.POINTER(CItem)))


ctypes.POINTER


В ctypes есть ctypes.POINTER() — указывает, что используется указатель. Например:ctypes.POINTER(CItem) указывает, что это указатель на структуру CItem().
Соответственно, строкой :ctypes.POINTER(ctypes.POINTER(CItem)) мы можем указать, что это указатель на указатель на структуру CItem, или в C++ CItem** А есть ctypes.pointer(). Данная функция возвращает указатель на объект. Например :item = CItem()


pointer = ctypes.pointer(item)


Не следует их путать, так как смысл у них совершенно разный.


ctypes.cast()


А теперь рассмотрим очень важную функцию ctypes.cast()Данная функция чем-то схожа со static_cast() из C++.


Она позволяет сделать очень важные приведения.


При создании типа массива, например:rowType = CItem * 4


row = rowType()


В данном случае row является областью памяти из 4 элементов структур CItem.


Конечно в таком виде мы никак не сможем использовать эти данные. А вот если мы на них используем функцию приведения :array_pointer = ctypes.cast(row, ctypes.POINTER(CItem))


В данном случае array_pointer уже является указателем на область памяти с 4 структурами CItem.


Первым параметром задается созданная область памяти с элементами массива, а вторым параметром надо указать к какому типу надо привести данную область.Ну вот вроде осветил основные моменты при передачи с помощью ctypes массивов данных.


Надеюсь данная статья поможет более быстро и полно разобраться с замечательной библиотекой ctypes.

Комментарии (11)


  1. rmuskovets
    07.09.2019 18:50

    У вашего кода нет форматирования! Пожалуйста, поправьте это.


    1. DollaR84 Автор
      08.09.2019 22:03

      Спасибо, поправил.


  1. ser-mk
    08.09.2019 00:14

    Спасибо, очень ценной дополнение!
    Дополню, что для создания простых структур можно использовать модуль struct


    1. DollaR84 Автор
      08.09.2019 08:51

      спасибо, как-то не приходилось работать с данным модулем, посмотрю его детальнее.


  1. iroln
    08.09.2019 01:03

    А вот передача больших массивов данных, или еще лучше двумерных массивов данных, или даже двумерных массивов объектов.

    А может быть просто взять numpy?
    https://docs.scipy.org/doc/numpy/reference/c-api.html


    Если ваш код должен работать с большими многомерными массивами, то numpy — это то, что нужно.


    1. DollaR84 Автор
      08.09.2019 09:00

      Да про numpy знаю, однако считаю, что он нужен для реально огромных массивов данных, например для анализа статистических данных или еще чего подобного. Когда речь идет о нескольких десятков объектов, у меня например было около шести десятков, то передавать их по одному абсолютно неадекватная задача, но и пристыковывать к проекту большую и тяжелую библиотеку только ради одной задачи передачи этих объектов в dll, без использования еще где либо — считаю слишком не рационально. Тот пример, что я привел, должен заполнить как раз ту середину, между минимумом элементов, которые можно передать отдельными параметрами и теми огромными массивами, где без numpy уже не обойтись.


  1. Jessy_James
    09.09.2019 16:43
    +1

    Посмотрел Ваш материал, что бы дополнить свой. Только пришлось помучиться, как получить структуру обратно…


    1. DollaR84 Автор
      09.09.2019 17:02
      +1

      Ну для этого есть два варианта.
      1. Создать структуру в python и передать указатель на нее одним из аргументов.
      2. Создать структуру в коде C++ dll, и возвращать указатель на нее, но в таком случае надо не забывать и делать вторую функцию для удаления созданной структуры, чтобы не возникало утечек с памятью. В вашей статье вы возвращали указатель на созданный через new класс, но нигде потом не вызвали удаление данного класса из памяти. Но так как это в том контексте не особо важно было, я не стал указывать на это, но если вы хотите делать возвращение структуры по второму варианту, то надо не забывать освобождать выделенную память.


      1. Jessy_James
        09.09.2019 17:16
        +1

        Ну да, поскольку делал как пример, то и забил на free. Надо бы дополнить.


        1. DollaR84 Автор
          09.09.2019 19:46

          В вашей статье привел в комментарии пример по типу вашего кода, как можно работать с созданными структурами в C, по указателю из python, надеюсь поможет


          1. Jessy_James
            09.09.2019 19:48

            Благодарю, я уже почитал. Посмотрю подробнее в ближайшее время, про contents как раз и не знал )), вот и помучился.