IS4Code / PawnPlus

A SA-MP plugin enhancing the capabilities of the Pawn programming language
MIT License
102 stars 17 forks source link

"Ref-counting" in the global pool #11

Closed IS4Code closed 5 years ago

IS4Code commented 5 years ago

The issue

Currently, functions like str_to_global and str_to_local simply move the string between the two pools. However, let's have a situation like this:

stock Func(String:str, delay)
{
    amx_forked(fork_data, .use_data = false)
    {
        str_to_global(str);
        wait_ms(delay);
        str_to_local(str);
        print_s(str);
    }
}

Moving the string to the global pool is necessary, because if the string was local, the GC would delete it before the code is resumed. However, this causes issues when the same string is used:

new String:str = str_new_static("string");
Func(str, 1000);
Func(str, 1500);

Since both functions are called at the same time, by the time the second one is resumed, the string has been already collected, because during the wait, it is moved back to the local pool.

This problem cannot be solved without storing additional information about the string, but this makes the code more complicated.

The proposal

I propose a breaking change to how the global pool works. Instead of simple operations like to_local and to_global, new operations would be introduced: use and release. The functions would manipulate the internal "reference count" every GC-object has. When the count is increased from 0, the object would be moved to the global pool. When the count is decreased from 1, the object would be moved to the local pool. Decreasing the count from 0 would produce a warning.

The GC will behave in the same way: objects in the local pool will be collected when the current supercontext is released (i.e. at the end of the top-level callback).

Together with this change, the global versions of the tags will be removed as well, since every individual function can handle the ownership of the string on its own, if it needs to. As a result, deleting a string will be always almost unneeded, because if the counts of use and release are balanced, the GC will automatically delete it eventually.

Guards

Guards would be modified to use the use and release operations by default, instead of delete. Thanks to this, the function above could be rewritten like this:

stock Func(String:str, delay)
{
    amx_forked(fork_data, .use_data = false)
    {
        pawn_guard(str);
        wait_ms(delay);
        print_s(str);
    }
}

pawn_guard will call use on str when used, increasing the count by 1. Since the guard will remain alive during the whole forked context, the string cannot be collected. Once the context ands, the guard will be destroyed and as a result, release is called on str. If nothing else owns the string, it will be collected.

Collections

At the moment, storing dynamic strings in collections is simple: move it to the global pool and add it to the collection. Calling list_delete_deep and similar functions will destroy the string stored inside.

I think list_delete_deep should instead call release on the string, decreasing the reference count. This makes sharing the same string in multiple containers very easy, since you only need to call str_use (as you would have used str_to_global previously) before storing the string in a container, and list_delete_deep would make it be deleted eventually.

The only slight issue is with variant references stored inside containers. Previously, the function would have called var_delete_deep on the variant (recursively deleting its contents), but since it should behave the same way for any GC-object, it would call release on it, and the GC will then delete the variant without deleting its contents.

Since variants should be only used for passing dynamic values around, I guess this is not such a large issue for most users. In addition, there might be another type of GC-object added in the future that would solve this.

The impact

Since this is quite a significant change to how a part of this plugin works (albeit one I consider beneficial), I have decided to first introduce it in this manner, for others to discuss and add their own insights.