9

I've got an API (a specific GUI library) that relies on std::shared_ptr a lot, i.e. they are often used as function parameters and stored within other objects. For example, container widgets, such as splitters and boxes will store their child widgets in shared_ptrs. Now I would like to map this API to Lua via luabind. In an ideal world, Luabind would create new objects in shared_ptrs and allow me to pass those directly to functions taking shared_ptr parameters. This seems to work for single classes, e.g.:

luabind::class_<Button, std::shared_ptr<Button>>("Button")

While I declare it like that, I can expose and use functions like void foo(std::shared_ptr<Button> const&).

Now the luabind manual mentions that in order to use a hierarchy of classes, I'd have to use the same shared_ptr template-instance for all classes in the hierarchy, e.g.

luabind::class_<BaseWidget, std::shared_ptr<BaseWidget>>("BaseWidget"),
luabind::class_<Button, BaseWidget, std::shared_ptr<BaseWidget>>("Button")

Now I can no longer call foo - it will fail to find the function from Lua. Can I somehow get luabind to still support passing buttons in shared_ptrs? Also, I would like to know why luabind mandates that you use the same smart pointer for all classes in the hierarchy instead of them just being convertible to base class pointers.

ildjarn
  • 62,044
  • 9
  • 127
  • 211
ltjax
  • 15,837
  • 3
  • 39
  • 62

1 Answers1

7

I think for that to work you have to bind your derived class like this:

luabind::class_<Button, BaseWidget, std::shared_ptr<Button>> ("Button")

For example:

class BaseWidget
{
public:
    static void Bind2Lua(lua_State* l)
    {
        luabind::module(l)
        [
            luabind::class_<BaseWidget, std::shared_ptr<BaseWidget>> ("BaseWidget")
            .def(luabind::constructor<>())
        ];
    }

    virtual ~BaseWidget()
    {

    }
};

class Button : public BaseWidget
{
public:
    static void Bind2Lua(lua_State* l)
    {
        luabind::module(l)
        [
            luabind::class_<Button, BaseWidget, std::shared_ptr<Button>> ("Button")
            .def(luabind::constructor<>())
            .def("Click", &Button::Click)
        ];
    }

    void Click()
    {
        std::cout << "Button::Click" << std::endl;
    }
};

Now you can use it with shared_ptr:

class Action
{
public:
    void DoClick(const std::shared_ptr<Button>& b)
    {
        // perform click action
        b->Click();
    }

    static void Bind2Lua(lua_State* l)
    {
        luabind::module(l)
        [
            luabind::class_<Action> ("Action")
            .def(luabind::constructor<>())
            .def("DoClick", &Action::DoClick)
        ];
    }
};

In lua:

b = Button()

a = Action()

a:DoClick(b)

The reason is that luabind uses type-id system with integers (more precisely std::size_t as defined in inheritance.hpp).
You can obtain the type-id of any registered type with the function:

luabind::detail::static_class_id<T>(nullptr);

Where T is the registered class.
In my demo program they are:

  • BaseWidget = 3
  • std::shared_ptr< BaseWidget > = 6
  • Button = 4
  • std::shared_ptr< Button > = 7
  • Action = 5

So when you call DoClick from lua, it will call the get member of the template class pointer_holder in instance_holder.hpp:

std::pair<void*, int> get(class_id target) const
{
    if (target == registered_class<P>::id)
        return std::pair<void*, int>(&this->p, 0);

    void* naked_ptr = const_cast<void*>(static_cast<void const*>(
        weak ? weak : get_pointer(p)));

    if (!naked_ptr)
        return std::pair<void*, int>((void*)0, 0);

    return get_class()->casts().cast(
        naked_ptr
      , static_class_id(false ? get_pointer(p) : 0)
      , target
      , dynamic_id
      , dynamic_ptr
    );
}

As you can see, if the target class is not the same as the one registered, it will try to do a cast.
This is where things get interesting. If you declared the Button class as

luabind::class_<Button, BaseWidget,std::shared_ptr<BaseWidget>>("Button")

then the instance will be held as a shared_ptr to BaseWidget, thus the cast function will try to cast from BaseWidget (3) to std::shared_ptr< Button > (7) and that fails. It could work if luabind supported base-to-derived conversion, which it doesn't seem to.

If however you declared the Button class as

luabind::class_<Button, BaseWidget, std::shared_ptr<Button>> ("Button")

then the instance will be held as as a shared_ptr to Button and then the target id will match the registered type. The get function will branch on the first return, never to bother with the cast.

You can also find the self contained program I used here at pastebin.

And here is a list of interesting break points you can set to see what's happening (luabind version 900):

  • line 94 in instance_holder.hpp (first line of pointer_holder::get)
  • line 143 in instance.cpp (first line of cast_graph::impl::cast)
BenMorel
  • 34,448
  • 50
  • 182
  • 322
Julien Lebot
  • 3,092
  • 20
  • 32
  • I actually added those overloads for get_pointer. It does definitely pass the smart_pointer (I can tell because the reference counts are right if I call those functions multiple times) – ltjax Jun 27 '12 at 16:06
  • Also, passing the raw pointer is even more clumsy since I want to store the pointers, not just execute functions on them. For that work at all, I'd have to modify the GUI object hierarchy to derive from enable_shared_from_this. – ltjax Jun 27 '12 at 16:09
  • But with your current solution, you ignore that the luabind manual mandates to use the base-class shared_ptr type: see http://www.rasterbar.com/products/luabind/docs.html#smart-pointers, last paragraph. If this "just works" I'd like to know why... – ltjax Jun 29 '12 at 14:25
  • I think it is a mistake made by the luabind documentation. I will file a bug report and detail why it works like that in my answer. I did some pretty extensive debugging to figure this out ! – Julien Lebot Jun 29 '12 at 16:57
  • The way I see it, the only problem that could possibly happen there is if luabind tries to assign a raw pointer to another smart pointer, so that the instance is held by two different reference-counting bodies... Could this maybe happen if you try to pass a button to a function that takes a std::shared_ptr? – ltjax Jul 02 '12 at 18:49
  • Actually, I can not call a function that takes a std::shared_ptr with this.... maybe that's what the documentation was about? – ltjax Jul 02 '12 at 20:20
  • Probably, but I think it would be wrong, because Derived->Base should be supported by default, but Base->Derived should be done with a dynamic_ptr_cast. Thus holding the instance by its base class shared_ptr would prevent you from calling any function taking a derived class directly. If that's the case then the documentation is fine and I stand corrected. Otherwise I would keep it the way I put it, and implement an explicit conversion path (which should exist). – Julien Lebot Jul 03 '12 at 08:50
  • Actually, just tried it and it seems that luabind extracts the shared_ptr *before* calling the cast function; thus it does not try to cast between shared_ptr< Button > and shared_ptr< BaseWidget > but between Button* and shared_ptr< BaseWidget > which hopefully fails since as you pointed out that would be undef behavior. It seems it would be safe in that case to have a function receive a BaseWidget* argument except in the case you want to hold onto that pointer. – Julien Lebot Jul 03 '12 at 09:59