vinniefalco / LuaBridge

A lightweight, dependency-free library for binding Lua to C++
1.63k stars 346 forks source link

Object lifetime management #13

Open vinniefalco opened 12 years ago

vinniefalco commented 12 years ago

JoshEngebretson wrote:

"Vinnie: Right, but what I am saying is Lua being able to control C++ lifetime. I exposed a virtual deletePointer on Userdata and am able to cleanup native, explicitly, from Lua. This is very useful and was easy to modify LuaBridge to handle.

It is pretty common to get back a pointer in client code that you are supposed to explicitly handle the lifetime.

I am mentioning it in case it sparks any ideas, but I am totally happy with how easy it was to wire this in... and the custom container stuff is simply awesome :)"

vinniefalco commented 12 years ago

I am not sure I understand what you mean about Lua being able to control C++ lifetime. If you want Lua to control the lifetime of a class exported with LuaBridge, then either pass the class by value or construct the object from Lua using a registered constructor (that doesn't use a container). The C++ object will "live" inside the userdata. When Lua no longer references the userdata, it will get garbage collected. At that point, the object's destructor will be called. You can still get a pointer to such an object from C++, any registered function that accepts a pointer to the object will receive a pointer to the portion of the userdata which holds the constructed class. I hope this is clear...it is somewhat convoluted to imagine.

JoshEngebretson commented 12 years ago

Say you have a library you're wrapping that has an instance factory. The library expects that you explicitly delete the instance when you're done with it. I am finding that explicitly telling the C++ that I am done is very nice compared to trying to wire up automated ref counting or having to subclass which isn't always an option. Does this make sense?

Edit: Additionally if I explicitly delete the native, I don't have to wait for the GC to decide it is time to delete the native. This can cause problems in itself. Access of a script reference when the backing native is gone, raises an error.

vinniefalco commented 12 years ago

I'm still a little bit confused. It seems to me that what you want for the case of wrapping a native library that creates instances of objects for you, is a non-ref counted container. You would create a simple class that constructs using the pointer to the already-created native library object, and in the destructor you call the library to delete the object (or use operator delete if that works). The result would be the equivalent of "Lua lifetime" except that the object is not constructed in-place in the userdata. When Lua no longer references the object, and it is garbage collected, then the object would be freed.

JoshEngebretson commented 12 years ago
  1. You don't want the native instance to be deleted when the lua object is collected as it may still be valid in C++ and you may get it back again. Needing to hold references script side to keep C++ instances alive is very error prone.
  2. The native C++ instance will only be deleted on GC, you very well may want to delete the instance explicitly to free resources immediately or other behavior... If you accidentally access a script object whose native is deleted, an error is generated so this is safe.

This stuff becomes more clear when wrapping C++ you don't control.

vinniefalco commented 12 years ago

Does LuaBridge offer enough functionality to support this use-case?

JoshEngebretson commented 12 years ago

It requires some form of a deletePointer with plumbing to make it accessible to Lua. This is nearly as common as the needing nil to marshal NULL case. It might be out of scope for LuaBridge 1.x? There is probably enough in LuaBridge to handle most cases where you control the C++ classes. Though, even then, it would be nice to be able to explicitly delete the native side.

merlinblack commented 11 years ago

I have problem which is in similar vain to this - which I managed to get around with luabind, but not with luabridge as of yet. Basically I have a external library with factory style create function and also a destroy function. These are easy enough to get working, however the problem is getting rid of the Lua references once the object has been destroyed. Imagine the following...

    a = CreateObject()
    DoSomethingWithIt( a )
    DestroyObject( a )
    -- You can crash here if you accidentally use 'a'
    a = nil -- safe now.

Basically Lua does not know that the object is dead - until you tell it.

What I did with luabind was make a container template, that could be marked invalid, and from there it would throw an exception. Then it was a simple matter of wrapping the create and destroy functions from the library. To return this container, and on destroy, mark it invalid. Any exception from accessing a dead object got turned into a Lua error.

I've tried the same approach with luabridge, however it does not work as the container's 'get' function is not called for each access (to properties or methods, etc) to the Lua reference. From what I can tell the pointer of the contained object is stored and used.

Any ideas on an alternate method or perhaps a tweak to luabridge? I'm going to get better acquainted with Userdata.h to see what I can find.

greatwolf commented 11 years ago

I see the problem. My initial thought was perhaps a weak table might offer a solution. Like setup some kind of machinery that auto-nils lua references when the object goes out of scope or dies.

But perhaps a better more simple approach is to just handle it the same way as lua file*. Specifically, if you try to read/write on a closed file descriptor lua gives an error attempt to use a closed file.

So if you try to call DoSomethingWithIt( a ) after DestroyObject is called on it then luabridge should issue a lua_error, like attempt to use a dead object or something to that effect.

Thoughts and comments?

merlinblack commented 11 years ago

The trick is knowing when a pointer is valid.

There's a gotchar in there, which I discovered after my 'ward pointers' started causing a race condition with garbage collection.
My ward_ptrs need to have a way to retrieve them from a raw pointer for when they come out the other side of Lua. Also they were hanging around due to garbage collection longer than the contained pointer, which in the mean time could get reused! A brand new object was marked as 'dead' because is had been made at the same address as an old one. Doh!

Another way I thought of is rather than an explicit DestroyObject call - you hook it up to the __gc method, and have no other way to call DestroyObject. This way in order to call DestroyObject you must nil all the references. This works as long as a) The only info you need to call DestroyObject is the object, and b) You can afford to have the object hang around for 'a while'. For what I'm trying to achieve these two points stop me doing this.

greatwolf commented 11 years ago

I guess the question really comes down to who owns the resource. In the simple file * case, lua clearly owns that so file:close() didn't have to worry whether C/C++ was still using it. If the host language (C++) should have full ownship and controls the lifetime of the object then you can just have lua get weak references to that object. Of course that would mean that C++ can yank the object rug from under lua while lua's still using it.

OTOH if ownship is meant to be shared equally between C++ and lua, I guess the goto solution is to use a ref-counted shared pointer.