簡體   English   中英

如何使用 pybind11 和 NumPy 公開不透明類型的數組

[英]How to expose an array of opaque type with pybind11 and NumPy

TL;博士

使用pybind11 ,如何使用Z3B7F949B2343F9E5390 E29F6EF5E1778Z 公開一個 POD 結構數組,同時讓它們在用戶看來是不錯的 Python 對象?

問題

我正在使用pybind11C 樣式 API暴露給Python 有一些類型,在 C 中實現為簡單的 POD 結構,在 Python 中作為不透明對象更有意義。 pybind11允許我這樣做並定義 object 在 Python 中的樣子。

我還想公開一個動態分配的數組。 使用pybind11NumPy這樣做是可能的,但我還沒有找到一種與我已經公開類型本身的方式兼容的方法。

我最終得到了兩種不同的 Python 類型,它們彼此不兼容,即使底層 C 類型相同。

約束

我正在尋找一種不涉及不必要副本的解決方案。 由於所有數據都是 POD,我認為應該可以將數據重新解釋為 C 端的結構或 Python 端的不透明對象。

C API 是固定的,但我可以自由選擇如何設計 Python ZDB974238714CA8DE6347ACE。

實施細節

在 C/C++ 端,類型如下所示:

struct apiprefix_opaque_type
{
    int inner_value;
};

使用pybind11 ,我將結構暴露為不透明的 object。 不暴露inner_value並不重要,但它對用戶沒有太大的價值,擁有更高級別的類型更有意義。

namespace py = pybind11;

void bindings(py::module_& m)
{
    py::class_<apiprefix_opaque_type>(m, "opaque_type")
        .def(py::init([]() {
            apiprefix_opaque_type x;
            x.inner_value = -1;
            return x;
        }))
        .def("is_set", [](const apiprefix_opaque_type& x) -> bool { return x.inner_value != -1; });

    m.def("create_some_opaque", []() -> apiprefix_opaque_type {
        apiprefix_opaque_type x;
        x.inner_value = 42;
        return x;
    });
}

有了這個,在 Python 方面,我有我想要的 API 行為。

>>> a = apitest.opaque_type()
>>> a.inner_value # Demonstrating that inner_value is not exposed.
AttributeError: 'apitest.opaque_type' object has no attribute 'inner_value'
>>> a.is_set()
False
>>> b = apitest.create_some_opaque()
>>> b.is_set()
True

在 API 的其他地方,我有一個包含這些數組的結構,作為指針和計數對。 為了簡單起見,讓我們假設它是一個全局變量(盡管實際上它是另一個動態分配對象的成員)。

struct apiprefix_state
{
    apiprefix_opaque_type* things;
    int num_things;
};

apiprefix_state g_state = { nullptr, 0 };

這個數組足夠大,我關心性能。 因此,我限制避免不必要的副本。

從 Python 開始,我希望能夠讀取數組、修改數組或完全替換數組。 我認為如果最后設置數組的人保留對它的所有權會更有意義,但我並不完全確定。

這是我目前使用NumPy公開數組的嘗試。

void more_bindings(py::module_& m)
{
    py::class_<apiprefix_state>(m, "state")
        .def(py::init([]() {
            return g_state;
        }))
        .def("create_things",
             [](apiprefix_state&, int size) -> py::array {
                 auto arr = py::array_t<apiprefix_opaque_type>(size);
                 return std::move(arr);
             })
        .def_property(
            "things",
            [](apiprefix_state& state) {
                auto base = py::array_t<apiprefix_opaque_type>();
                return py::array_t<apiprefix_opaque_type>(state.num_things, state.things, base);
            },
            [](apiprefix_state& state, py::array_t<apiprefix_opaque_type> things) {
                state.things = nullptr;
                state.num_things = 0;
                if (things.size() > 0)
                {
                    state.num_things = things.size();
                    state.things = (apiprefix_opaque_type*)things.request().ptr;
                }
            });
}

鑒於我對 Python 中的 memory 管理的初步了解,我強烈懷疑所有權沒有正確實施。

但是這個問題的問題是,NumPy 不明白apiprefix_opaque_type是什么。

>>> state = apitest.state()
>>> state.things
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: NumPy type info missing for struct apiprefix_opaque_type
>>>

如果我添加一個 dtype 聲明......

    PYBIND11_NUMPY_DTYPE(apiprefix_opaque_type, inner_value);

...現在 NumPy 理解它,但現在有兩個不兼容的 Python 類型指的是相同的 C 類型。 此外,還公開了實現細節inner_value

>>> state = apitest.state()
>>> state.things
array([], dtype=[('inner_value', '<i4')])
>>> state.things = state.create_things(10)
>>> a = apitest.opaque_type()
>>> a
<apitest.opaque_type object at 0x000001BABE6E72B0>
>>> state.things[0] = a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'apitest.opaque_type'
>>>

如何公開我的不透明對象數組?

如果您只想公開一系列事物,那么您可以做這樣的事情

apiprefix_opaque_type& apiprefix_state_get(apiprefix_state& s, size_t j)
{

    return s.things[j];
}

void apiprefix_state_set(apiprefix_state& s, size_t j, const apiprefix_opaque_type& o)
{
    s.things[j] = o;
}

py::class_<apiprefix_state>(m, "state")
    // ...
    .def("__getitem__", &apiprefix_state_get)
    .def("__setitem__", &apiprefix_state_set)

添加范圍檢查顯然是個好主意。 (你可以使用 lambdas,我只是發現顯式函數更具可讀性)。

當您將things包裝在 numpy 數組中時,您將其公開為緩沖區,並且結構化 dtype 僅提供有關應將哪些偏移量處的哪些字節解釋為int s的信息。 因此,您實際上可以在上面編寫state.things[0] = 42 (更一般地,對於具有多個成員的結構,您可以分配一個元組)。 但它不知道如何從apiprefix_opaque_type中提取一個int以將其分配給 dtype 定義的字段。

如果您想將things公開為 numpy 陣列,那么正如您所指出的,所有權是一個重要問題。 如上所述,python 將擁有由create_things創建的任何 arrays 並管理底層 memory。 但是,您的二傳手有幾個問題。 第一的

state.things = nullptr;
state.num_things = 0;

如果 state.things 所指向的state.things不是由 Z23EEEB7347BDD25DFCA936 管理,則可能是 memory 泄漏。 其次在這一行

state.things = (apiprefix_opaque_type*)things.request().ptr;

you are referencing memory managed by python without reference counting , so there is a chance the apiprefix_state will be left with things pointing to memory that python has garbage collected.

看起來您可能想要公開可能由 C++ 管理的全局g_state 在這種情況下,一種可能的方法是

pybind11::capsule nogc(values, [](void *f) {});
return pybind11::array_t<apiprefix_opaque_type>(
    { g_state.num_things },
    { sizeof(apiprefix_opaque_type) },
    g_state.things,
    nogc
);

或者,您可以直接使用緩沖協議或 memory 視圖。

如果您確實想始終引用全局 state 則從初始化程序返回它是不尋常的

.def(py::init([]() { return g_state; }))

通常它會像

.def_static("get_instance", ... )

但請注意,這並不完全符合您的要求,因為它會復制g_state

暫無
暫無

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

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