This video assumes intermediate knowledge with Fusion. Beginner friendly tutorials can be found in the official Fusion documentation. Below is a transcript with slides.
If you’re getting up to date on Fusion, you might have seen the new possiblyOutlives
warning. At first, it might seem mystical, but in reality, it’s perfectly understandable, so in this video, I’ll show you why this warning exists, where it comes from, and how you can fix it.
Object oriented programming
Let’s start by talking about how most Luau libraries implement object oriented programming. To create an object, you call a constructor function. To destroy an object, you call it’s :destroy()
method. The object starts living when you construct it, and stops living when you destroy it.
Now, let’s talk about how Fusion implements object oriented programming. As before, to create an object, you call a constructor function. However, to destroy an object, you don’t call a :destroy()
method. Instead, you append the object to a list of objects you’ve made so far. When you’re done with this list of objects, you can use the doCleanup
function, which will destroy everything in the list. These lists are called scopes, and let you destroy groups of objects all at once.
For your convenience, Fusion appends every newly created object for you. All you need to do is pass the scope into the constructor. This is why every constructor requires you to pass in a scope.
Destruction order
Now that we understand how Fusion generally deals with objects, let’s talk about destruction order.
Suppose I create a value object, and a computed object that depends on the value object. For this piece of code to work correctly at all times, the value object must ‘stay alive’ while the computed object is using it.
It can only be destroyed once the computed has been destroyed, because if the value object is destroyed while the computed object is still alive, then the computed object is depending on an object that no longer exists, which will raise a useAfterDestroy
error, indicating that the required object is no longer available, because they were destroyed in the wrong order.
Because of this, we can say that the computed object must not outlive the value object. If the computed object is alive while the value object is dead, it will raise a useAfterDestroy
error.
To speak more generally, you create newer objects using older objects: in this example, we create a computed using a value object we made earlier. For this reason, objects should be destroyed from newest to oldest, to ensure that the newer objects are destroyed, before the older objects that they depend on.
Destruction order
Now that we understand that, let’s talk about how scopes manage destruction order.
Because new objects are appended to the scope, we know that the oldest objects will appear at the start of the list, while the newest objects will appear at the end. This is why the doCleanup()
function works backwards from the end of the list - doing that means it’s destroying the newest objects first. This is the most simple way in which Fusion prevents useAfterDestroy
errors.
However, it’s still possible to use things in the wrong order, especially if your program is highly dynamic. Consider this example where we have two value objects inside of each other, and a computed object trying to access the innermost value. As written, the code is fine; both value objects are created before the computed object.
However, if we later create a new value object to replace the innermost value object, the computed object is now depending on a value object that’s newer, which is dangerous because it can raise a useAfterDestroy
error.
This is where the possiblyOutlives
message comes from. When one thing depends on another thing in Fusion, the library will look at the scopes to figure out the ordering of the objects. If it detects a case where an older object depends on a newer object, you will see the possiblyOutlives
message. When you see this message, it means you’ve introduced one of these subtle ordering problems, where an object can depend on something that no longer exists, causing a useAfterDestroy
error. So if you see this warning, you will need to change your code.
So how can we fix this problem? Generally, the solution is to make sure your objects appear in the right order. This can be as simple as swapping lines of code around. For example, if we moved this new value object before the computed, the error would go away, because the computed will always be destroyed before anything that it depends upon.
However, this isn’t always possible. If you have to create this new value object at a later point in time, then we’ll need to use some other features of scopes, to ensure that the destruction order is correct.
Inner scopes
A nice feature of scopes is that they can contain more than just objects. They can contain other things, like functions to run, instances to destroy, or event connections to disconnect. These are all very useful for when you’re writing custom destruction logic.
But of particular interest to us, scopes can contain other scopes. When the doCleanup()
method encounters an ‘inner’ scope, it will start cleaning up that inner scope from the end. Once it’s done, it continues progressing through the ‘outer’ scope as usual.
This means, if we add objects to the inner scope, we can destroy objects at that point, rather than appending them to the end of the whole outer scope. We can use this fact to model the destruction ordering that we want.
So let’s return to the example with the nested value objects. To make this work, we’ll use the innerScope
function, so that we can add objects at that point in the destruction order. Then, when we’re creating our innermost state objects, we’ll create them using that inner scope, which means they’re destroyed at that older point in time. However, we’ll continue to use the outer scope for the computed object and outermost value object, because those objects depend on the objects we’re creating inside of the inner scope, so we want to make sure the computed and outermost value are destroyed first.
With that, the code now works perfectly. No warnings are emitted, and the objects are destroyed in the correct order.
Addendum - dynamic destruction
Now, there’s something I’d like to note about this whole example. Given the way that we’ve written this code, it’s impossible to individually destroy our innermost value objects. They’ll only be destroyed once the program finishes running. This might be fine for your use case, but if you wanted to destroy the innermost value objects after they get replaced, then we’ll need to extend the code a little bit.
A nice feature of inner scopes is that you can clean them up at any time, and when you do, the inner scope removes itself from the outer scope. In this way, you can create temporary scopes, made exclusively to hold objects that only exist for a short period of time.
So let’s expand our code. I’m going to move the construction logic for the innermost value objects into a new function, and from now on, I’m going to use this function whenever I want to replace the innermost object.
We’re going to use our existing inner scope to permanently mark that point in time. We’ll store our temporary scopes inside of it, to make sure they’re destroyed in the right order if the entire outer scope is destroyed, just like before.
So to create our temporary scope, we call innerScope()
on that existing permanent scope. In the temporary scope, we create our temporary value object. Instead of just storing the innermost value object, we’ll store both the value object and the temporary scope. Finally, just before overwriting the value, we’ll read out the previous temporary scope, and finish the function by destroying that previous scope just before we return.
With that, we have a complete implementation of a dynamic list of objects, that’s 100% absent of errors.
So hopefully, now you know a little bit more about those possiblyOutlives
warning messages, and how you can use Fusion to guarantee that your code is memory safe.