tags:

views:

422

answers:

3

I'm lead dev for Bitfighter, a game primarily written in C++, but using Lua to script robot players. We're using Lunar (a variant of Luna) to glue the bits together.

I'm now wrestling with how our Lua scripts can know that an object they have a reference to has been deleted by the C++ code.

Here is some sample robot code (in Lua):

if needTarget then                           -- needTarget => global(?) boolean
    ship = findClosest(findItems(ShipType))  -- ship => global lightUserData obj
end

if ship ~= nil then
    bot:setAngleToPoint(ship:getLoc())
    bot:fire()
end

Notice that ship is only set when needTarget is true, otherwise the value from a previous iteration is used. It is quite possible (likely, even, if the bot has been doing it's job :-) that the ship will have been killed (and its object deleted by C++) since the variable was last set. If so, C++ will have a fit when we call ship:getLoc(), and will usually crash.

So the question is how to most elegantly handle the situation and limit the damage if (when) a programmer makes a mistake.

I have some ideas. First, we could create some sort of Lua function that the C++ code can call when a ship or other item dies:

function itemDied(deaditem)
    if deaditem == ship then
        ship = nil
        needTarget = true
    end
end

Second, we could implement some sort of reference counting smart pointer to "magically" fix the problem. But I would have no idea where to start with this.

Third, we can have some sort of deadness detector (not sure how that would work) that bots could call like so:

if !isAlive(ship) then
    needTarget = true
    ship = nil             -- superfluous, but here for clarity in this example
end

if needTarget then                           -- needTarget => global(?) boolean
    ship = findClosest(findItems(ShipType))  -- ship => global lightUserData obj
end

<...as before...>

Fourth, I could retain only the ID of the ship, rather than a reference, and use that to acquire the ship object each cycle, like this:

local ship = getShip(shipID)                 -- shipID => global ID

if ship == nil then
    needTarget = true
end

if needTarget then                           -- needTarget => global(?) boolean
    ship = findClosest(findItems(ShipType))  -- ship => global lightUserData obj
    shipID = ship:getID()
end

<...as before...>

My ideal situation would also throw errors intelligently. If I ran the getLoc() method on a dead ship, I'd like to trigger error handling code to either give the bot a chance to recover, or at least allow the system to kill the robot and log the problem, hopefully cuing me to be more careful in how I code my bot.

Those are my ideas. I'm leaning towards #1, but it feels clunky (and might involve lots of back and forth because we've got lots of short-lifecycle objects like bullets to contend with, most of which we won't be tracking). It might be easy to forget to implement the itemDied() function. #2 is appealing, because I like magic, but have no idea how it would work. #3 & #4 are very easy to understand, and I could limit my deadness detection only to the few objects that are interesting over the span of several game cycles (most likely a single ship).

This has to be a common problem. What do you think of these ideas, and are there any better ones out there?

Thanks!


Here's my current best solution:

In C++, my ship object is called Ship, whose lifecycle is controlled by C++. For each Ship, I create a proxy object, called a LuaShip, which contains a pointer to the Ship, and Ship contains a pointer to the LuaShip. In the Ship's destructor, I set the LuaShip's Ship pointer to NULL, which I use as an indicator that the ship has been destroyed.

My Lua code only has a reference to the LuaShip, and so (theoretically, at least, as this part is still not working properly) Lua will control the lifecycle of the LuaShip once the corresponding Ship object is gone. So Lua will always have a valid handle, even after the Ship object is gone, and I can write proxy methods for the Ship methods that check for Ship being NULL.

So now my task is to better understand how Luna/Lunar manages the lifecycle of pointers, and make sure that my LuaShips do not get deleted when their partner Ships get deleted if there is still some Lua code pointing at them. That should be very doable.


Actually, it turned out not to be doable (at least not by me). What did seem to work was to decouple the Ship and the LuaShip objects a little. Now, when the Lua script requests a LuaShip object, I create a new one and hand it off to Lua, and let Lua delete it when it's done with it. The LuaShip uses a smart pointer to refer to the Ship, so when the Ship dies, that pointer gets set to NULL, which the LuaShip object can detect.

It is up to the Lua coder to check that the Ship is still valid before using it. If they do not, I can trap the sitation and throw out an stern error message, rather than having the whole game crash (as was happening before).

Now Lua has total control over the lifecyle of the LuaShip, C++ can delete Ships without causing problems, and everything seems to work smoothly. The only drawback is that I'm potentially creating a lot of LuaShip objects, but it's really not that bad.


If you are interested in this topic, please see the mailing list thread I posted about a related concept, that ends in some suggestions for refining the above:

http://lua-users.org/lists/lua-l/2009-07/msg00076.html

+2  A: 

Our company went with solution number four, and it worked well for us. I recommend it. However, in the interests of completeness:

Number 1 is solid. Let the ship's destructor invoke some Lunar code (or mark that it should be invoked, at any rate), and then complain if you can't find it. Doing things this way means that you'll have to be incredibly careful, and maybe hack the Lua runtime a bit, if you ever want to run the game engine and the robots in separate threads.

Number 2 isn't as hard as you think: write or borrow a reference-counting pointer on the C++ side, and if your Lua/C++ glue is accustomed to dealing with C++ pointers it'll probably work without further intervention, unless you're generating bindings by inspecting symbol tables at runtime or something. The trouble is, it'll force a pretty profound change in your design; if you're using reference-counted pointers to refer to ships, you have to use them everywhere - the risks inherent in referring to ships with a mixture of bare pointers and smart ones should be obvious. So I wouldn't go that route, not as late in the project as you seem to be.

Number 3 is tricky. You need a way to determine whether a given ship object is alive or dead even after the memory representing it has been freed. All the solutions I can think of for that problem basically devolve into number 4: you can let dead ships leave behind some kind of token that's copied into the Lua object and can be used to detect deadness (you'd keep dead objects in a std::set or something similar), but then why not just refer to ships by their tokens?

In general, you can't detect whether a particular C++ pointer points to an object that's been deleted, so there's no easy magical way to solve your problem. Trapping the error of calling ship:getLoc() on a deleted ship is possible only if you take special action in the destructor. There's no perfect solution to this problem, so good luck.

David Seiler
My current solution (posted at the end of my question) does run up against the threading concern you raised. However, at this time, I don't think threading makes much sense, as my scripts run comfortably at close to 60 frames/sec on an old laptop, and threading would add tons of complexity.I am using a type of smart pointer for the Ship<=>LuaShip connection, so hopefully I'll see the benefits there without having to retrofit the entire game, which would be prohibitive.
Watusimoto
+3  A: 

I don't think you have a probelm on your Lua side, and you should not be solving it there.

Your C++ code is deleting objects that are still being referenced. No matter how they're referenced, that's bad.

The simple solution may be to let Lunar clean up all your objects. It already knows which objects must be kept alive because the script is using them, and it seems feasible to let it also do GC for random C++ objects (assuming smart pointers on the C++ side, of course - each smart pointer adds to Lunars reference count)

MSalters
Yes, you are right. I think the problem is in how Luna/Lunar does its binding. I'm still trying to understand the details, but it appears to use weak tables in its referencing, which I believe do not inhibit gc from deleting objects. Hopefully I can tweak it a bit and tell it not to do that, at least for selected objects (such as the LuaShip I described in my solution above).
Watusimoto
A: 

I agree with MSalters, I really don't think you should be freeing the memory from the C++ side. Lua userdata supports the ___gc metamethod to give you a chance to clean things up. If the gc is not agressive enough you can tweak it a bit, or run it manually with a small step size, more often. The lua gc is not deterministic, so if you need to have resources released then you will need to have a function that you can call to release those resources (which will also be called by __gc, with appropriate checks).

You might also want to look into using weak tables for your ship references so that you don't have to assign EVERY reference to nil to get it freed. Have one strong reference (say, in a list of all active ships) then all the others are weak references. When a ship is destroyed, set a flag on the ship that marks it as such, then set the reference to nil in the active ships table. Then, when the other ship wants to interact your logic is the same except you check for:

if ship==nil or ship.destroyed then
  ship = findClosest(findItems(ShipType))
end
Dolphin