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
GlobalString
, GlobalVariant
, and GlobalIter
tags are removed.
*_to_global
functions removed, *_use
is introduced.
*_to_local
functions removed, *_release
is introduced.
tag_op_delete_deep
(free
in C++) renamed to tag_op_release
. Retains its effect for non-GC-collected objects, but decreases the reference count for GC-objects.
tag_op_use
introduced. Increases the reference count for GC-objects. Does nothing for other objects.
var_delete_deep
removed (to reduce confusion and enforce proper usage). Variant references stored in collections will not be recursively deleted when the collection is recursively deleted.
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.