2025, Oct 06 11:32

pybind11 में C++ अमूर्त क्लासों की Python ओवरराइड: लाइफटाइम जाल, smart_holder और trampoline self life support

pybind11 में C++ अमूर्त क्लासों की Python ओवरराइड में pure virtual call त्रुटि क्यों होती है, और smart_holder व trampoline self life support से लाइफटाइम फिक्स करें.

pybind11 के साथ C++ की अमूर्त बेस क्लासों को Python से जोड़ते समय लाइफटाइम से जुड़ा जाल आसानी से लग सकता है। लक्षण उलझाने वाले होते हैं: आप Python से व्युत्पन्न क्लास बनाते हैं, C++ से ओवरराइड किए गए मेथड को बुलाते हैं, और अंत में C++ बेस पर प्योअर वर्चुअल कॉल ट्रिगर हो जाती है। बाइंडिंग कोड नहीं टूटता—त्रुटि रनटाइम पर ठीक वहीं होती है, जहाँ आपको Python का ओवरराइड चलने की उम्मीद थी।

समस्या को दोहराने वाला न्यूनतम उदाहरण

नीचे दिया गया कोड एक C++ मॉड्यूल दिखाता है जो अमूर्त Actor और ActorFactory को एक्सपोज़ करता है, साथ में एक छोटा ऑर्केस्ट्रेटर जो actor बनाता है और उसे चलाता है। Python में ओवरराइड मौजूद है, फिर भी जब C++ वापस कॉल करता है तो प्योअर वर्चुअल कॉल हो जाती है।

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <memory>
#include <string>
#include <iostream>
// Python में लागू की जाने वाली अमूर्त क्लास
struct Actor {
    virtual ~Actor() = default;
    virtual void setup() = 0;
};
// Python में लागू की जाने वाली अमूर्त फ़ैक्टरी
struct ActorFactory {
    virtual ~ActorFactory() = default;
    virtual std::shared_ptr<Actor> fetchActor(const std::string& type) = 0;
};
// फ़ैक्टरी का उपयोग कर actor प्राप्त करता है और फिर उसे कॉल करता है
class Orchestrator {
private:
    std::shared_ptr<Actor> actorPtr;
    std::shared_ptr<ActorFactory> makerPtr;
public:
    Orchestrator(std::shared_ptr<ActorFactory> fact) : makerPtr(fact) {}
    void execute(const std::string& name) {
        actorPtr = makerPtr->fetchActor(name);
        actorPtr->setup(); // Python ओवरराइड के बजाय यहाँ प्योअर वर्चुअल कॉल होती है
    }
};
// ------------------- ट्राम्पोलिन्स -------------------
namespace py = pybind11;
struct PyActorShim : public Actor {
    using Actor::Actor;
    void setup() override {
        PYBIND11_OVERRIDE_PURE(
            void,
            Actor,
            setup
        );
    }
};
struct PyActorFactoryShim : public ActorFactory {
    using ActorFactory::ActorFactory;
    std::shared_ptr<Actor> fetchActor(const std::string& type) override {
        PYBIND11_OVERRIDE_PURE(
            std::shared_ptr<Actor>,
            ActorFactory,
            fetchActor,
            type
        );
    }
};
// ------------------- बाइंडिंग्स -------------------
PYBIND11_MODULE(CoreBridge, m) {
    py::class_<Actor, PyActorShim, std::shared_ptr<Actor>>(m, "Actor")
        .def(py::init<>())
        .def("setup", &Actor::setup);
    py::class_<ActorFactory, PyActorFactoryShim, std::shared_ptr<ActorFactory>>(m, "ActorFactory")
        .def(py::init<>())
        .def("fetchActor", &ActorFactory::fetchActor);
    py::class_<Orchestrator>(m, "Orchestrator")
        .def(py::init<std::shared_ptr<ActorFactory>>())
        .def("execute", &Orchestrator::execute);
}

और इसे चलाने वाला Python पक्ष:

import CoreBridge as cb
class CustomFactory(cb.ActorFactory):
    def __init__(self):
        super().__init__()
        self.creators = {}
    def add_actor(self, key, fn):
        self.creators[key] = fn
    def fetchActor(self, tag):
        return self.creators[tag]()
class CustomActor(cb.Actor):
    def __init__(self):
        super().__init__()
    def setup(self):
        print("Hello, world!")
fac = CustomFactory()
fac.add_actor("ActorA", lambda: CustomActor())
runner = cb.Orchestrator(fac)
runner.execute("ActorA")

असल में हो क्या रहा है

मूल कारण है होल्डर टाइप। जब Python में लागू की गई ऑब्जेक्ट्स के लिए होल्डर के रूप में std::shared_ptr इस्तेमाल होता है, तो Python और C++ के लाइफटाइम सममित रूप से जुड़े नहीं रहते। Python ऑब्जेक्ट C++ इंस्टेंस को जिंदा रखता है, लेकिन उल्टा सुनिश्चित नहीं होता। अगर Python रेफ़रेंस न रहने पर Python इंस्टेंस हट गया, तो बाद में C++ से जो कॉल Python ओवरराइड तक जानी चाहिए थी, वह C++ की प्योअर वर्चुअल फ़ंक्शन तक पहुँच जाती है, और रनटाइम पर प्योअर वर्चुअल कॉल हो जाती है।

समाधान: py::smart_holder और ट्राम्पोलिन self life support

pybind11 v3 से, होल्डर के रूप में py::smart_holder का उपयोग किया जा सकता है। यह सुनिश्चित करता है कि जब तक C++ पक्ष के पास रेफ़रेंस है, Python ऑब्जेक्ट जीवित रहे—यानी लाइफटाइम्स एक-सी लाइन में हों और वर्चुअल डिस्पैच Python ओवरराइड तक पहुँचे। ट्राम्पोलिन क्लासों को py::trampoline_self_life_support से विरासत लेनी चाहिए। यह संयोजन लाइफटाइम असंगति को ठीक करता है और C++ से Python में सही वर्चुअल डिस्पैच बनाए रखता है।

नीचे सुधारा गया C++ बाइंडिंग है, जिसमें क्लास बाइंडिंग्स में py::smart_holder और ट्राम्पोलिन में py::trampoline_self_life_support का उपयोग किया गया है। Orchestrator का वह कंस्ट्रक्टर जो std::shared_ptr लेता है, वैध रहता है, क्योंकि ज़रूरत पड़ने पर py::smart_holder को std::shared_ptr में कास्ट किया जा सकता है।

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <memory>
#include <string>
#include <iostream>
struct Actor {
    virtual ~Actor() = default;
    virtual void setup() = 0;
};
struct ActorFactory {
    virtual ~ActorFactory() = default;
    virtual std::shared_ptr<Actor> fetchActor(const std::string& type) = 0;
};
class Orchestrator {
private:
    std::shared_ptr<Actor> actorPtr;
    std::shared_ptr<ActorFactory> makerPtr;
public:
    Orchestrator(std::shared_ptr<ActorFactory> fact) : makerPtr(fact) {}
    void execute(const std::string& name) {
        actorPtr = makerPtr->fetchActor(name);
        actorPtr->setup();
    }
};
namespace py = pybind11;
struct PyActorShim : public Actor, py::trampoline_self_life_support {
    using Actor::Actor;
    void setup() override {
        PYBIND11_OVERRIDE_PURE(
            void,
            Actor,
            setup
        );
    }
};
struct PyActorFactoryShim : public ActorFactory, py::trampoline_self_life_support {
    using ActorFactory::ActorFactory;
    std::shared_ptr<Actor> fetchActor(const std::string& type) override {
        PYBIND11_OVERRIDE_PURE(
            std::shared_ptr<Actor>,
            ActorFactory,
            fetchActor,
            type
        );
    }
};
PYBIND11_MODULE(CoreBridge, m) {
    py::class_<Actor, PyActorShim, py::smart_holder>(m, "Actor")
        .def(py::init<>())
        .def("setup", &Actor::setup);
    py::class_<ActorFactory, PyActorFactoryShim, py::smart_holder>(m, "ActorFactory")
        .def(py::init<>())
        .def("fetchActor", &ActorFactory::fetchActor);
    py::class_<Orchestrator>(m, "Orchestrator")
        .def(py::init<std::shared_ptr<ActorFactory>>())
        .def("execute", &Orchestrator::execute);
}

मॉड्यूल का उपयोग करने वाले Python कोड में कोई बदलाव की आवश्यकता नहीं है। ओवरराइड जैसा है वैसा ही रहेगा, और C++ से आने वाली कॉल बिना प्योअर वर्चुअल पर टकराए सही ढंग से Python तक जाएगी।

यह बारीकी क्यों मायने रखती है

फ़ैक्ट्रियाँ और प्लगइन-स्टाइल आर्किटेक्चर Python-over-C++ पैटर्न के साथ स्वाभाविक रूप से बैठते हैं, लेकिन वे लाइफटाइम से जुड़ी उलझनों को भी बढ़ा देते हैं। Python में बनाए गए और C++ से उपभोग किए जाने वाले ऑब्जेक्ट्स स्वामित्व सीमाएँ पार करते हैं, इसलिए गलत होल्डर चुनने से लाइफटाइम चुपचाप अलग हो जाते हैं—और यहीं से बेहूदे प्योअर वर्चुअल कॉल्स घुस आती हैं। py::smart_holder का उपयोग और ट्राम्पोलिन में self life support सक्षम करना दोनों पक्षों को ऑब्जेक्ट के अस्तित्व पर सहमत रखता है, जिससे वर्चुअल डिस्पैच भरोसेमंद ढंग से काम करता है।

मुख्य बातें

यदि C++ से कॉल होने पर Python का ओवरराइड चलने की जगह प्योअर वर्चुअल कॉल दिख रही है, तो सबसे पहले होल्डर टाइप की जाँच करें। pybind11 v3 में, Python में लागू और C++ से कॉल होने वाली क्लासों के लिए py::smart_holder को तरजीह दें, और ट्राम्पोलिन क्लासों को py::trampoline_self_life_support से इनहेरिट कराएँ। इससे लाइफटाइम्स एकसाथ चलते हैं और ऊपरी स्तर की डिज़ाइन बदले बिना सही व्यवहार बना रहता है।

यह लेख StackOverflow पर प्रश्न (लेखक: Arturo 550) और Ahmed AEK के उत्तर पर आधारित है।