pybind / pybind/pybind11

[BUG]: Recursive dispatch fails to find python function override.

Open
#3,357 10 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
C++
Stars
18k
Forks
2.3k
Avg merge
5d 17h
Merged PRs (30d)
10

Description

Required prerequisites
Problem description

Consider a simple C++ visitor pattern:

class Data{};
using DataVisitor = std::function<void (const Data&)>;

class Adder {
public:
    // interface to add two data objects, the result is passes to data visitor
    virtual void operator()(const Data& first, const Data& second, const DataVisitor& visitor) const = 0;
};

void add(const Data& first, const Data& second,  const Adder& adder, const DataVisitor& visitor) {
    adder(first, second, visitor);
}

void add3(const Data& first, const Data& second, const Data& third,  const Adder& adder, const DataVisitor& visitor) {
    adder(first, second, [&] (const Data& first_plus_second) {
        adder(first_plus_second, third, visitor);
    });
}

In this pattern, Data and Adder are abstract classes which can be used in two algorithms add and add3 which, as the name suggest, add two or three objects of the type Data and send the result to a visitor. This pattern is intended to eliminate allocating memory for Data pointer in case we need to store temporary results, e.g., see add3 algorithm.

Next, we define the corresponding python bindings:

class PyAdder : public Adder {
public:
    void operator()(const Data& first, const Data& second, const DataVisitor& visitor) const override {
        PYBIND11_OVERRIDE_PURE_NAME(void, Adder, "__call__", operator(), first, second, visitor);
    }
};

class PyData : public Data {
public:
    PyData() = default;
};

PYBIND11_MODULE(module, m) {
    pybind11::class_<Adder, PyAdder>(m, "Adder")
        .def(pybind11::init<>())
        .def("__call__", &Adder::operator());

    m.def("add", &add);
    m.def("add3", &add3);

    pybind11::class_<Data, PyData>(m, "Data")
        .def(pybind11::init<>());
}

Finally, let's test it in Python. Firs,t some boilerplate:

class Adder(module.Adder):
    def __call__(self, first, second, visitor):
        visitor(Data(first.value + second.value))

class Data(module.Data):
    def __init__(self, value):
        super().__init__()
        self.value = value

def print_result(data):
        print(data.value)

Testing function add yields the expected result

module.add(Data(1), Data(2), Adder(), print_result) # prints 3

However, testing add3 causes failure

module.add3(Data(1), Data(2), Data(3), Adder(), print_result) # raises RuntimeError

The error reads

Traceback (most recent call last):
  File "*****/test_add.py", line 39, in test_add3_int
    module.add3(Data(1), Data(2), Data(3), Adder(), print_result)
  File "*****/test_add.py", line 8, in __call__
    visitor(Data(first.value + second.value))
RuntimeError: Tried to call pure virtual function "Adder::__call__"

Chaising the problem through pybind11 sources, I found a possible culprit. In function get_type_override, we check if dispatch code is invoked from overridden function.

    /* Don't call dispatch code if invoked from overridden function.
       Unfortunately this doesn't work on PyPy. */
#if !defined(PYPY_VERSION)
    PyFrameObject *frame = PyThreadState_Get()->frame;
    if (frame != nullptr && (std::string) str(frame->f_code->co_name) == name
        && frame->f_code->co_argcount > 0) {
        PyFrame_FastToLocals(frame);
        PyObject *self_caller = dict_getitem(
            frame->f_locals, PyTuple_GET_ITEM(frame->f_code->co_varnames, 0));
        if (self_caller == self.ptr())
            return function();
    }
#else

It returns an empty function if the second override is called from the same frame as the first override. Commenting this check renders a working example. I consider this as a bug, but please let consider proving me wrong.

My questions are:

  • In which case it is important to prevent the second dispatch from overridden function?
  • Is there a good way to fix things related to Visitor (recursive) or Double Dispatch patterns?

The link for reproducible and extended examples: https://bitbucket.org/yershov/recursive-dispatch/src/main/

Reproducible example code
https://bitbucket.org/yershov/recursive-dispatch/src/main/

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with get_type_override and the recursive-dispatch example linked in the issue. Reproduce the add3 failure and inspect the frame check that returns an empty function during nested overrides. Done means establishing the intended dispatch behavior and adding coverage showing that the recursive visitor example no longer raises the pure-virtual-function error.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.