передача списка строк от python до C через pybind11 - PullRequest
2 голосов
/ 05 февраля 2020

После этого сообщения я хочу знать, как я могу передать список строк от Python до C (т. Е. Используя C заголовки и синтаксис, а не C ++) через Pybind11 , Я полностью осознаю тот факт, что Pybind11 - это библиотека C ++, и коды должны быть скомпилированы компилятором C ++ в любом случае. Однако мне трудно понять реализации C ++, например здесь и здесь .

Здесь Я пытался передать список python строк по указателям, представленным как целые числа, а затем получить их long* в C, но это не сработало .

Код на C / C ++ должен выглядеть примерно так:

// example.cpp
#include <stdio.h>
#include <stdlib.h>

#include <pybind11/pybind11.h>

int run(/*<pure C or pybind11 datatypes> args*/){

    // if pybind11 data types are used convert them to pure C :
    // int argc = length of args
    // char* argv[] =  array of pointers to the strings in args, possible malloc

    for (int i = 0; i < argc; ++i) {
        printf("%s\n", argv[i]);
    } 

    // possible free

    return 0;
}

PYBIND11_MODULE(example, m) {

    m.def("run", &run, "runs the example");
}

Также предоставляется простой CMakeLists.txt пример здесь . и код Python может выглядеть примерно так:

#example.py
import example

print(example.run(["Lorem", "ipsum", "dolor", "sit", "amet"]))

Чтобы избежать недоразумений, таких как this , учтите следующие моменты:

  • Это не так вопрос XY, так как предполагаемая проблема Y уже была решена правильным / каноническим способом с использованием заголовков / стандартных библиотек C ++ и синтаксиса (ссылки выше). Цель этого вопроса - чистое любопытство. Решение проблемы в синтаксисе, с которым я знаком, поможет мне понять природу типов данных и функциональности pybind11. Пожалуйста, не пытайтесь найти проблему Y и решить ее.
  • Я полностью осознаю, что pybind11 - это библиотека C ++, и код должен быть скомпилирован с помощью компилятора C ++.
  • Я был бы признателен, если бы вы проконсультировались со мной в комментариях о необходимых изменениях моего вопроса, а не делали это самостоятельно. Я знаю, что вы хотите помочь, но я постарался сформулировать свой вопрос как можно лучше, чтобы избежать путаницы.
  • Я был бы признателен, если бы вы как можно больше меняли некомментированные части моих кодов C / C ++ и python.
  • Я знаю, что использую термин " C / C ++ "не так. Я использую этот термин для обозначения кода C ++, написанного в синтаксисе C и использующего заголовки C. Мне жаль, что я не знаю лучшего способа для его вызова.
  • Как показывают закомментированные части файла example.cpp, можно использовать типы данных pybind11, а затем преобразовать их в C , Но я подозреваю, что возможно и чистое решение C. Например, см. эту попытку .

1 Ответ

2 голосов
/ 05 февраля 2020

Ниже я переформатировал код предыдущего примера , где я использовал конструкции C ++, чтобы использовать только C и pybind11.

#include <pybind11/pybind11.h>
#include <stdio.h>

#if PY_VERSION_HEX < 0x03000000
#define MyPyText_AsString PyString_AsString
#else
#define MyPyText_AsString PyUnicode_AsUTF8
#endif

namespace py = pybind11;

int run(py::object pyargv11) {
int argc = 0;
char** argv = NULL;

PyObject* pyargv = pyargv11.ptr();
if (PySequence_Check(pyargv)) {
    Py_ssize_t sz = PySequence_Size(pyargv);
    argc = (int)sz;

    argv = (char**)malloc(sz * sizeof(char*));
    for (Py_ssize_t i = 0; i < sz; ++i) {
        PyObject* item = PySequence_GetItem(pyargv, i);
        argv[i] = (char*)MyPyText_AsString(item);
        Py_DECREF(item);
        if (!argv[i] || PyErr_Occurred()) {
            free(argv);
            argv = nullptr;
            break;
        }
    }
}

if (!argv) {
    //fprintf(stderr,  "argument is not a sequence of strings\n");
    //return;

    if (!PyErr_Occurred())
        PyErr_SetString(PyExc_TypeError, "could not convert input to argv");
    throw py::error_already_set();
}

for (int i = 0; i < argc; ++i)
    fprintf(stderr, "%s\n", argv[i]);

free(argv);

return 0;
}

PYBIND11_MODULE(example, m) {
m.def("run", &run, "runs the example");
}

Ниже я подробно прокомментирую это, чтобы объяснить, что я делаю и почему.

В Python2 строковые объекты основаны на char*, в Python3 они основаны на Unicode. Отсюда следующий макрос MyPyText_AsString, который меняет поведение в зависимости от версии Python, так как нам нужно получить C стиль "char *".

#if PY_VERSION_HEX < 0x03000000
#define MyPyText_AsString PyString_AsString
#else
#define MyPyText_AsString PyUnicode_AsUTF8
#endif

pyargv11 py::object является тонким дескриптор объекта Python C -API; поскольку в следующем коде используется Python C -API, проще иметь дело с базовым PyObject* напрямую.

void closed_func_wrap(py::object pyargv11) {
    int argc = 0;            // the length that we'll pass
    char** argv = NULL;      // array of pointers to the strings

    // convert input list to C/C++ argc/argv :

    PyObject* pyargv = pyargv11.ptr();

Код будет принимать только контейнеры, которые реализуют протокол последовательности и Таким образом, может быть зациклено. Это охватывает два наиболее важных из них PyTuple и PyList одновременно (хотя и немного медленнее, чем проверка этих типов напрямую, но это сделает код более компактным). Чтобы быть полностью обобщенным c, этот код должен также проверять протокол итератора (например, для генераторов и, вероятно, отклонять объекты str, но оба они маловероятны.

    if (PySequence_Check(pyargv)) {

Хорошо, у нас есть последовательность; теперь получаем его размер. (Этот шаг является причиной, по которой для диапазонов вам нужно использовать протокол итератора Python, поскольку их размер обычно неизвестен (хотя вы можете запросить подсказку).)

        Py_ssize_t sz = PySequence_Size(pyargv);

Одна часть, размер задан, сохраните его в переменной, которую можно передать другим функциям.

        argc = (int)sz;

Теперь выделите массив указателей для char* (технически const char*, но это важно не здесь, как мы его отбросим).

        argv = (char**)malloc(sz * sizeof(char*));

Далее l oop по последовательности для извлечения отдельных элементов.

        for (Py_ssize_t i = 0; i < sz; ++i) {

Это получает один элемент из последовательность. Вызов GetItem эквивалентен Pythons «[i]» или getitem call.

            PyObject* item = PySequence_GetItem(pyargv, i);

В Python2 строковые объекты основаны на char *, в Python3, они основаны на Unicode. Отсюда следующий макрос "MyPyText_AsStr ing ", который меняет поведение в зависимости от версии Python, так как нам нужно перейти к C -типу" char * ".

Приведение от const char* к char* здесь в принципе безопасно, но содержимое argv[i] НЕ должно быть изменено другими функциями. То же самое верно для argv аргумента main(), поэтому я предполагаю, что это так.

Обратите внимание, что строка C НЕ копируется. Причина в том, что в Py2 вы просто получаете доступ к базовым данным, а в Py3 преобразованная строка сохраняется как элемент данных объекта Unicode, и Python выполняет управление памятью. В обоих случаях мы гарантируем, что их время жизни будет, по крайней мере, таким же, как и время жизни объекта ввода Python (pyargv11), то есть, по крайней мере, на время вызова этой функции. Если другие функции решат сохранить указатели, понадобятся копии.

            argv[i] = (char*)MyPyText_AsString(item);

Результатом PySequence_GetItem стала новая ссылка, так что теперь, когда мы закончили с этим, отбросьте ее:

            Py_DECREF(item);

Возможно, входной массив не содержал только Python str объектов. В этом случае преобразование завершится неудачно, и мы должны проверить этот случай, или «closed_function» может вызвать ошибку.

            if (!argv[i] || PyErr_Occurred()) {

Очистить ранее выделенную память.

                free(argv);

Установить argv to NULL для последующей проверки успеха:

                argv = nullptr;

Откажитесь от l oop:

                break;

Если данный объект не был последовательностью или если один элементов последовательности не было строкой, то у нас нет argv, и поэтому мы выручаем:

    if (!argv) {

Следующее немного лениво, но, вероятно, лучше понять, если все вы хочу посмотреть код C.

        fprintf(stderr,  "argument is not a sequence of strings\n");
        return;

Что вы действительно должны сделать, это проверить, была ли уже установлена ​​ошибка (например, b / c проблемы преобразования), и установить ее, если нет , Затем сообщите об этом pybind11. Это даст вам чистое исключение Python на стороне вызывающего. Это выглядит так:

        if (!PyErr_Occurred())
            PyErr_SetString(PyExc_TypeError, "could not convert input to argv");
        throw py::error_already_set();       // by pybind11 convention.

Хорошо, если мы доберемся сюда, у нас есть argc и argv, так что теперь мы можем использовать их:

    for (int i = 0; i < argc; ++i)
        fprintf(stderr, "%s\n", argv[i]);

Наконец очистите выделенную память.

    free(argv);

Примечания:

  • Я бы все равно выступил за использование по крайней мере std::unique_ptr, так как в этом случае жизнь будет намного проще создаются исключения C ++ (из пользовательских преобразователей любого входного объекта).
  • Изначально я ожидал, что смогу заменить весь код однострочным std::vector<char*> pv{pyargv.cast<std::vector<char*>>()}; после #include <pybind11/stl.h>, но я обнаружил, что это не работает (даже если он компилируется). Ни один из них не использовал std::vector<std::string> (также компилируется, но не работает во время выполнения).

Просто спросите, все ли что-то неясно.

РЕДАКТИРОВАТЬ : Если вы действительно хотите иметь PyListObject, просто вызовите PyList_Check(pyargv11.ptr()) и, если true, приведите результат: PyListObject* pylist = (PyListObject*)pyargv11.ptr(). Теперь, если вы хотите работать с py::list, вы также можете использовать следующий код:

#include <pybind11/pybind11.h>
#include <stdio.h>

#if PY_VERSION_HEX < 0x03000000
#define MyPyText_AsString PyString_AsString
#else
#define MyPyText_AsString PyUnicode_AsUTF8
#endif

namespace py = pybind11;

int run(py::list inlist) {
    int argc = (int)inlist.size();
    char** argv = (char**)malloc(argc * sizeof(char*));

    for (int i = 0; i < argc; ++i)
        argv[i] = (char*)MyPyText_AsString(inlist[i].ptr());

    for (int i = 0; i < argc; ++i)
        fprintf(stderr, "%s\n", argv[i]);

    free(argv);

    return 0;
}

PYBIND11_MODULE(example, m) {
    m.def("run", &run, "runs the example");
}

Этот код короче только b / c, он имеет меньше функций: он принимает только списки и его также является более неуклюжим в обработке ошибок (например, он утечет, если передан в списке целых чисел из-за выброса исключения pybind11; чтобы исправить это, используйте unique_ptr, как в самом первом примере кода, так что argv освобождается при исключении).

...