Sol2: Function expects shared_ptr, receives derived class and fails

Created on 26 Mar 2018  路  13Comments  路  Source: ThePhD/sol2

I am having trouble calling a function that expects a base shared_ptr and receives a derived shared_ptr object. World::Render() expects a shared_ptr(Buffer) and is instead given a shared_ptr(Context), which should still work, since Context is derived from Buffer.

Here are the relevant C++ classes and functions:

class World
{
     void Render(shared_ptr<Buffer> buffer);
};
class Buffer {};
class Context : public Buffer {};
extern shared_ptr<Context> CreateContext();
extern shared_ptr<World> CreateWorld();

Here is the binding code:

lua.new_usertype<World>("World", "Render", &World::Render);
lua.new_usertype<Buffer>("Buffer");
lua.new_usertype<Context>("Context", sol::base_classes, sol::bases<Buffer>);
lua.set_function("CreateContext", CreateContext);
lua.set_function("CreateWorld", CreateWorld);

And here is the Lua script:

local context = CreateContext()
local world = CreateWorld()
world:Render(context)

This results in the following error:

Lua Error: sol: runtime error: [string "print("Starting...")..."]:17: stack index 2, expected userdata, received sol.sol::detail::unique_usertype: unrecognized userdata (not pushed by sol?) (bad argument into 'void(std::shared_ptr)')
stack traceback:
[C]: in function 'Render'
[string "print("Starting...")..."]:17: in main chunk

Feature.Can Do Feature.Shiny

Most helpful comment

I'm going to leave this issue open to remind me to do this. I might also ramble about things that do and don't work here.

All 13 comments

I dont think sol supports such (yet).
However if you used void Render(Buffer& buffer); for example, it would work.

Here is a complete working example for reference:

#include "sol.hpp"

#include <iostream>
#include <memory>

class Buffer {};
class World
{
public:
    void Render(Buffer& buffer) { std::cout << "rendered" << std::endl; };
};
class Context : public Buffer {};
std::shared_ptr<Context> CreateContext() { return std::make_shared<Context>(); };
std::shared_ptr<World> CreateWorld() { return std::make_shared<World>(); };

int main()
{
    sol::state lua;
    lua.new_usertype<World>("World", "Render", &World::Render);
    lua.new_usertype<Buffer>("Buffer");
    lua.new_usertype<Context>("Context", sol::base_classes, sol::bases<Buffer>());
    lua.set_function("CreateContext", CreateContext);
    lua.set_function("CreateWorld", CreateWorld);

    lua.script(
        "local context = CreateContext();"
        "local world = CreateWorld();"
        "world:Render(context);"
    );
    return 0;
}

That won't work. We have lots of things like this:

C++:

class Entity {
    void SetParent( shared_ptr<Entity> parent);
};
class Model : public Entity {};
class Sprite: public Entity {};

Lua:

local model = CreateBox()
local sprite = CreateSprite()
sprite:SetParent(model)-- expects a shared_ptr(Entity), not a shared_ptr(Model)

It is next-to impossible for me to see through shared_ptr<Derived> and tell accurately that it can be built into a shared_ptr<Base>. There has been a long discussion on exactly why this is hard in the first place:

https://github.com/ThePhD/sol2/issues/505#issuecomment-327033671

I can try to take a crack at this again. I certainly have no expectations I'll come up with anything performant or sane anytime soon, though.

I don't understand the details of this, but if it is just a matter of determining a type from a void* this can be solved easily. In our engine, all objects exposed to Lua are derived from a base Object class. The Object class has two methods:

class Object {
    std::string GetClassName();// human-friendly
    int GetClass();// used internally when performance matters
};

In our Lua debugger I just cast the void* to an Object* and I can figure out the type from there. Or in sol, if you know every userdata type is guaranteed to have a function GetClassName() then you can rely on that. If you want to avoid adding an extraneous requirement/convention to users like this, you could just have a user-defined callback that accepts a (void*) and returns some value indicating the type.

We only expose raw objects (Vec3() etc.) and shared pointers, nothing else. I have done some tests with sol2 and our new API based on shared pointers, and it's a dream come true, so I would really like to implement this library in our next major version.

What do you think?

It's unfortunately a lot more complicated than that. I can know something is std::shared_ptr<Derived>. The problem, as I spoke of in the linked issue and the comment trail, is that I have a much harder time identifying that something is just a plain shared_ptr or unique_ptr, and how to do the conversion from Derived to Base at-runtime. It is going to require either some hackery or finesse.

Note that base classes do work, it's not a problem of figuring out if a class is an Object or not. The problem is knowing that something is a shared_ptr, ripping out the type _inside_ the shared ptr, and performing a proper base-derived shared_ptr conversion.

An object stored in a shared_ptr is going to be created and destroyed infrequently. As a game runs, creating a new entity, material, or other object with some persistent payload is an infrequent event. Adding extra overhead when one of these objects is first exposed to Lua or when it is collected will have no significant impact on performance.

A copy-able object not stored in a shared_ptr, like a Vec3 or Mat4 is something that will be created and destroyed all the time as the game runs. Adding any overhead to these objects would have a significant impact on performance.

Therefore, it would be okay to enter pointers into a map to indicate they are a shared pointer:

shared_pointers_map[*void] = true;

And then you could check this table later to see if the object is a shared pointer. Remove the map value when the object is collected from Lua.

If I just don't understand how this works, that is one thing, but from a performance perspective adding additional overhead to shared_ptrs, by some means, does not matter because creation and destruction of these things is typically infrequent. To me at least, unique_ptrs are of no concern because we only expose shared_ptr for expensive classes and objects for cheap ones.

I do not believe this approach is scalable, nor does it help me with the intrinsic problem of properly converting a chunk of memory without violating aliasing rules. The map idea may make sense in your architecture but it likely does not make sense in the general case.

Based on an earlier suggestion I tried something new.

Our class hierarchy is like below. Simple classes are passed as objects and all derived from Object, while complex objects are always passed around as shared pointers and are all derived from a SharedObject class (which is also derived from Object):

  • Object

    • Vec3

    • Mat4

    • SharedObject

    • World

    • Buffer



      • Context



    • Entity



      • Model



The SharedObject class has a special function to get a shared_ptr for itself. This is possible because the class has multiple inheritance and is also derived from enable_shared_from_this\

class SharedObject : public Object, public enable_shared_from_this<SharedObject>
{
    shared_ptr<SharedObject> Self()
    {
        return shared_from_this();
    }
};

Here is our original World::Render() function:
void World::Render(shared_ptr<Buffer> buffer)

And now I added a new overload:

void World::Render(Buffer& buffer)
{
    //Get a shared_ptr and call the real function
    Render(dynamic_pointer_cast<Buffer>(buffer.Self()));
}

Amazingly, it seems to work. I think this should be safe to use, right?

This'll work while I figure other things out.

I'm going to leave this issue open to remind me to do this. I might also ramble about things that do and don't work here.

The code to fix this is in sol3, but isn't active just yet. Thanks for bringing attention to this so long ago.

This is now officially fixed and part of sol3, woo! #654 馃帀

Thanks! I love you!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shohnwal picture shohnwal  路  4Comments

Koncord picture Koncord  路  4Comments

mrgreywater picture mrgreywater  路  4Comments

Erwsaym picture Erwsaym  路  4Comments

TheMaverickProgrammer picture TheMaverickProgrammer  路  6Comments