簡體   English   中英

通過pybind11將字符串列表從python傳遞給C

[英]passing a list of strings from python to C through pybind11

這篇文章之后,我想知道如何通過 Pybind11 將字符串列表從 Python 傳遞到 C(即,使用 C 標頭和語法,而不是 C++)。 我完全知道 Pybind11 是一個 C++ 庫,並且代碼無論如何都必須由 C++ 編譯器編譯。 但是,我很難理解 C++ 實現,例如這里這里

在這里,我嘗試通過指針傳遞一個python字符串列表,表示為整數,然后在C中通過long*接收它們,但是沒有用。

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

為了避免這樣的誤解,請考慮以下幾點:

  • 這不是 XY 問題,因為已經使用 C++ 頭文件/標准庫和語法(上面的鏈接)以正確/規范的方式解決了假定的 Y 問題。 這個問題的目的純粹是好奇。 以我熟悉的語法解決問題將幫助我了解 pybind11 數據類型和功能的基本性質。 請不要試圖找出 Y 問題並解決它。
  • 我完全知道 pybind11 是一個 C++ 庫,無論如何必須使用 C++ 編譯器編譯代碼。
  • 如果您在對我的問題進行必要編輯的評論中咨詢我,而不是自己做,我將不勝感激。 我知道您想提供幫助,但我已嘗試盡可能好地構建我的問題以避免混淆。
  • 如果您盡可能避免更改我的 C/C++ 和 python 代碼中沒有注釋的部分,我將不勝感激。
  • 我知道使用術語“C/C++”是錯誤的。 我使用這個術語來指代用 C 語法編寫並使用 C 頭文件的 C++ 代碼。 很抱歉,我不知道更好的稱呼方式。
  • 正如example.cpp文件的注釋部分所示,可以使用 pybind11 數據類型,然后將它們轉換為 C。但我懷疑純 C 解決方案也可能是可能的。 例如,請參閱此嘗試

下面我重新格式化了之前使用 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();

代碼將只接受實現序列協議的容器,因此可以循環。 這同時涵蓋了兩個最重要的PyTuplePyList (雖然比直接檢查這些類型要慢一點,但這將使代碼更緊湊)。 為了完全通用,這段代碼還應該檢查迭代器協議(例如,對於生成器和可能拒絕 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*));

接下來,循環遍歷序列以檢索單個元素。

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

這從序列中獲取單個元素。 GetItem 調用等效於 Python 的“[i]”或getitem調用。

            PyObject* item = PySequence_GetItem(pyargv, i);

在 Python2 中,字符串對象是基於 char* 的,在 Python3 中,它們是基於 unicode 的。 因此,下面的宏“MyPyText_AsString”會根據 Python 版本改變行為,因為我們需要使用 C 風格的“char*”。

這里從const char*char*原則上是安全的,但argv[i]的內容不能被其他函數修改。 main()argv參數也是如此,所以我假設情況確實如此。

請注意,不復制 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 設置為NULL以便稍后成功檢查:

                argv = nullptr;

放棄循環:

                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.

好吧,如果我們到了這里,那么我們就有了argcargv ,所以現在我們可以使用它們:

    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*>>()};替換所有代碼std::vector<char*> pv{pyargv.cast<std::vector<char*>>()}; #include <pybind11/stl.h> ,但我發現這不起作用(即使它確實編譯)。 也沒有使用std::vector<std::string> (也編譯,但在運行時也失敗)。

只是問是否還有不清楚的地方。

編輯:如果您真的只想擁有一個 PyListObject,只需調用PyList_Check(pyargv11.ptr()) ,如果為真,則轉換結果: 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)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM