Skip to content

Handles

IllidanS4 edited this page Apr 12, 2019 · 5 revisions

Several objects introduced in PawnPlus take advantage of garbage collection to automatically manage their lifetime, removing the need of explicit destruction. However, other objects do not share this behaviour, and instead must be explicitly destroyed when they are no longer needed.

Handles are the shared_ptr of PawnPlus. Any object may be wrapped in a handle which then takes care of managing the object's lifetime. Once the garbage collector deems the handle collectible, it automatically deletes the object stored inside as well.

Usage

Handles are manipulated in a way similar to variants:

new List:l = list_new();
new Handle:h = handle_new(l);

When a handle is constructed, it calls acquire on the object (if present). When a handle is destroyed in any way, it calls release or delete_deep (different effects of the same operation) on the object.

new List:l = list_new();
new Handle:h = handle_new(l);
wait_ms(1);
assert(!list_valid(l));

Here, the list is protected by the handle which, once the code is paused, is still located in the local pool. Therefore, the garbage collector destroys the handle and as a result, the list is destroyed as well.

Paired explicit calls to handle_acquire and handle_release will temporarily increase the reference count in the handle object, preventing it from being collected:

new List:l = list_new();
new Handle:h = handle_new(l);
handle_acquire(h);
wait_ms(1);
handle_release(h);
assert(list_valid(l));
wait_ms(1);
assert(!list_valid(l));

The two calls can be replaced by a single call to pawn_guard, protecting the value for the duration of the context:

public Task(Handle:h)
{
    new List:l;
    if(!handle_get_safe(h, l)) return;
    pawn_guard(h);
    
    task_await(...); // any asynchronous operation which preserves the context
    assert(list_valid(l));
}

Assuming other code doesn't explicitly delete the handle or the actual list, the list is guaranteed to be present for the whole duration of the function, regardless of any asynchronous operations that may pause it. This is the preferred solution for sharing objects, since in case of errors, the explicit destruction cannot be called.

The contents of a handle cannot be modified, and the object it contains should not be deleted explicitly.

Lifetime control

Handles can monitor the lifetime of several types of objects, and can be used to check if an object is alive or not. Lists, linked lists, maps, and tasks cooperate with handles in a special way – they offer a "lifetime monitor object" to the handle object that can be used to check the state of the object. Once it is deleted, it will be reported as dead. Handles (and guards) use this information and do not attempt to delete these objects if they are reported as dead. handle_linked can be used to query this information.

This code illustrate the necessity of this behaviour:

new List:l = list_new();
assert(list_valid(l));
list_delete(l);
assert(!list_valid(l));
list_new();
assert(!list_valid(l));

The last line may succeed or not, because the list created at the line before it could be assigned the same identifier. Handles don't depend on the identifier alone, and so if the object is already dead, there is no risk that a wrong list is deleted.

This is considerably useful when "binding" a task to a player. Imagine the following code:

new Task:t = task_ms(1000);
amx_forked()
{
    wait_ms(500 + random(1000)); // simulating player disconnect
    print("Cancelling");
    task_delete(t);
}
print("Awaiting");
task_await(t);
print("Done");

If the random wait finishes before t, "Done" is never printed. However, if task_delete is called after the wait, the task will not exist anymore (or the identifier might be assigned to an unrelated task). Using a handle solves this:

new Handle:h = handle_new(t);
handle_acquire(h);
wait_ms(500 + random(1000));
print("Cancelling");
handle_release(h);

Weak handles

When an object needs to outlive its handle, a weak handle can be used. This kind of a handle doesn't automatially destroy its object when deleted, but can be still used to track its lifetime.

new List:l = list_new();
new Handle:h = handle_new(l, .weak=true);
handle_delete(h);
assert(list_valid(l));
new List:l = list_new();
new Handle:h = handle_new(l, .weak=true);
assert(handle_linked(h));
list_delete(l);
assert(!handle_linked(h));

Generic handles

Handles can be generic, behaving similarly to iterators. The example above can be rewritten like this:

public Task(Handle<List>:h)
{
    new List:l = handle_get<List>(h);
    pawn_guard(h);
    
    task_await(...);
    assert(list_valid(l));
}

Examples

Proctection of user-defined tags

forward File_release(File:arg);
public File_release(File:arg)
{
    fclose(arg);
}

main()
{
    tag_set_op(tag_uid((tagof(File:))), tag_op_release, #File_release);
}

stock NewFile()
{
    new File:f = ftemp();
    new Handle<File>:h = handle_new<File>(f);
    // h can be passed to any function, even returned, and the file will be always closed eventually
}
Clone this wiki locally