Search code examples
cpythonpybind11

Pass along a Python type to C++ that inherits C++


I have a C++ class RuleSet with a bunch of std::shared_ptr to some rules for a game, most notably to the victory condition, a std::shared_ptr<VictoryRule>. VictoryRule is a abstract C++ type, with one function winners to return the winners of a game. (team_t is a enum, but for simplicity consider it an alias to int.)

class VictoryRule {
 public:
  //! @brief Check if game is won by a team.
  //! @return the teamno of the team that won or team_t::no_team if nobody won.
  virtual std::vector<team_t> winners(const Game &game) = 0;
  //! @brief Default virtual destructor.
  virtual ~VictoryRule() = default;
};

I exposed this class to python with pybind11 to be able to subclass it in python, with a trampoline class.

class PyVictoryRule : public VictoryRule {
 public:
  std::vector<team_t> winners(const Game &game) override {
    PYBIND11_OVERRIDE_PURE(std::vector<team_t>, VictoryRule, winners, game);
  }
};

And it's registered with:

  pybind11::class_<VictoryRule, PyVictoryRule, std::shared_ptr<VictoryRule>>(
      m, "VictoryRule")
      .def(pybind11::init())
      .def("winners", &VictoryRule::winners, arg("game"));

And I'v reimplemented the Rule in python

class CustomVictoryRule(VictoryRule):
    def __init__(self):
        VictoryRule.__init__(self)

    def winners(self, game):
        return [1]

I can instantiate the class in python and call the winners method without any issue.

cvr = CustomVictoryRule()
cvr.winners() # Returns [1]

But I don't want to use the rule directly, I want to store it in the C++ Ruleset.

class RuleSet {
 public:
  std::shared_ptr<VictoryRule> get_victory_rule();
  void register_victory_rule(std::shared_ptr<VictoryRule> victory_rule);

 private:
  std::unordered_map<CasePawn::Type, std::shared_ptr<Rule>> per_pawn_type_rule_;
  std::shared_ptr<VictoryRule> victory_rule_;
};

All methods of RuleSet are registered to pybind11 as-is.

  pybind11::class_<RuleSet, std::shared_ptr<RuleSet>>(m, "RuleSet")
      .def(pybind11::init())
      .def("get_victory_rule", &RuleSet::get_victory_rule)
      .def("register_victory_rule", &RuleSet::register_victory_rule,
           arg("victory_rule"));

But when I pass along a CustomVictoryRule to RuleSet.register_victory_rule, it loses its dynamic type, and Receive an error for trying to call pure virtual function VictoryRule::winners...

ruleset = RuleSet()
ruleset.register_victory_rule(CustomVictoryRule())
ruleset.get_victory_rule().winners() #fails with RuntimeError: `Tried to call pure virtual function "VictoryRule::winners"`

What should I do so that ruleset.get_victory_rule() return a victory rule with the correct type CustomVictoryRule, so that CustomVictoryRule.winners() is called in that last line of code ?


Solution

  • I found the issue. Instead of doing:

    ruleset.register_victory_rule(CustomVictoryRule())
    

    I do:

    cvr = CustomVictoryRule()
    ruleset.register_victory_rule(cvr)
    

    Then it worked ! So somehow python keeps alive the instance when it's stored into a local variable that's kept alive as long as the ruleset.

    Thus, I need to tell pybind11 to keep temporary python objects passed as arguments. Luckily, there's a pybind11::keep_alive decorator to functions to do exactly that. So, binding RuleSet from c++ to python with the following fixes the issue !

      pybind11::class_<RuleSet, std::shared_ptr<RuleSet>>(m, "RuleSet")
          .def(pybind11::init())
          .def("victory_rule", &RuleSet::get_victory_rule)
          .def("register_victory_rule", &RuleSet::register_victory_rule,
               arg("victory_rule"), keep_alive<1, 2>());