PFL Object Scoping

Tuesday, 20 October 2026 12:49 UTC

When an object has been created using memory allocated from the heap and it has become no longer needed, it is necessary to free its memory allocation. This allows the memory to be reused by the program and prevents memory-leaks.

The PFL framework uses reference-counting to manage object memory allocation lifetimes. That is, each object instance contains a count of its current references. When an object is created, it has a reference count of zero. As references are added or removed, the count is updated. When the count returns to zero after having been decremented, the object is no longer needed and can be terminated. Its termination function is called and the memory is then freed.

This is performed automatically within container objects. In other words, if an object is added to a container object such as PFL_Array for example, the array increments the object's reference count. If the object is then removed from the array or the array itself is terminated, the array decrements the object's reference count. If the object's reference count has then reached zero, it is terminated.

In functions, if an object is created and assigned to a function-scope variable reference, the object must have its reference count maintained manually. It must be incremented when the reference is assigned and decremented before the function completes.

PFL provides some functions and macros to facilitate the reference counting.

The following list of function macros are typically used within container objects to provide the automatic reference counting:-

  • pfl_obj_i_inc_ref(obj)

    Increment an object's reference count.

  • pfl_obj_i_rel(obj)

    Release an object. Decrement its reference count and if the count becomes zero, terminate it.

  • pfl_obj_i_dec_ref(obj)

    Decrement an object's reference count but do not terminate it if it reaches zero.

  • pfl_obj_i_set_ref(ref, val)

    Replace the object in a reference variable with another one. i.e. if the variable already contains an object instance reference, decrement the old object's reference count. Set the variable to be a reference to the new object and increment its reference count.

The following pfl_scope macros are typically used within functions to manage references in variables with function scope. (See the examples below):-

  • pfl_scope(val) and pfl_scope_end

    Used with braces to mark the scope of a single reference value.

  • pfl_scope_list(...) and pfl_scope_end

    Used with braces to mark the scope of multiple reference values e.g. object parameters.

  • pfl_scope_result(val) and pfl_scope_end_result

    Similar to pfl_scope(val) but used to provide a temporary scope to an object as it is being built before returning it as the result of a function. e.g. in a constructor function or a generator. This does not terminate the object at scope end but allows it to be returned as a "new" instance with zero references.

Circular references

A potential problem with reference counting is circular references. This happens when a container object contains itself, directly or indirectly.

For example, if we create a PFL_Array object and append it to itself it creates a circular reference. It increments its own reference count. This means that even if there are no other references to the PFL_Array, the reference count cannot reach zero.

There are two ways to work around this:-

  1. Design

    Generally speaking, it is preferable to avoid circular references by careful design of data-structures such that objects never end up containing themselves. This is ideal and leads to a more readable application.

  2. Weak References

    Sometimes it's expedient to have a 2-way relationship between objects such as a parent / child relationship. That is, the parent object contains a reference to the child but the child also contains a reference back to the parent. In this case, the circular reference can be mitigated by a weak-reference. The child's reference to its parent object need not increment/decrement the parent object's reference count because the child is already within the parent's scope. The child's parent reference becomes a weak reference.

    Objects that are designed to be general purpose containers, such as PFL_Array, manage their child objects reference counts automatically. So in order to allow a PFL_Array to contain itself, a PFL_Weak_Ref container can be used. The PFL_Weak_Ref is a single item container that does not increment its child object's reference count. So a PFL_Array can be legitimately added to itself by wrapping it in a PFL_Weak_Ref instance before adding it. (see the examples below).

    PFL_Weak_Ref objects are transparent to container objects' accessor routines. If a PFL_Array item contains a PFL_Weak_Ref, when that item is requested, the PFL_Array will return the PFL_Weak_Ref's contents.

Memory Tracking

The PFL framework includes a memory tracker. This is enabled by default in development mode and not intended for production use. It can assist in situations where an objects have not been freed, leading to memory leaks. It is similar to tools like Address Sanitizer but explicitly intended for PFL.

The memory tracker maintains a list of all memory allocations created with pfl_malloc and removes them as they are freed. At the end of execution it produces a report showing either a list of un-freed mallocs or a confirmation that there are none.

 * ------------------------------------------------------------
 * No un-freed mallocs.
 * Max mem alloc: 2138
 * ------------------------------------------------------------

Scope

The pfl_scope macros provide a visual indication of function scopes. The are paired in start and end types and include open and close braces within the macro definitions to create C scopes. By convention, explicit braces are also used in the code to assist in readability.

Example 1 - pfl_scope()

pfl_scope(val) combined with pfl_scope_end provides scope for a single object instance.

void 
hello_str_example ()
{
    // Create a new string with a ref count of 0.
    PFL_Str * hello_str = pfl_str_new("Hello");
    
    pfl_scope(hello_str)  // Increment ref count to 1
    {
        // Print the hello_str 
        pfl_println(hello_str);
    }
    pfl_scope_end;        // Decrement ref count to 0 and terminate hello_str    
}

Example 2 - container scope

A container object provides a scope for its contents.

void 
array_example ()
{
    // Allocate a 3 item array
    PFL_Array * array = pfl_array_new(3);  
    
    pfl_scope(array)  // Increment array's ref count to 1
    {
        // Place 3 strings within the array's scope 
        PFL_Str * foo = pfl_array_i_append(array, pfl_str_new("foo"));
        PFL_Str * bar = pfl_array_i_append(array, pfl_str_new("bar"));
        PFL_Str * baz = pfl_array_i_append(array, pfl_str_new("baz"));
        
        // Print the array: 
        // ["foo", "bar", "baz"]
        pfl_println(array);
    }
    pfl_scope_end;    // Terminate array and its contents. 
}

Example 3 - pfl_scope_list()

pfl_scope_list(...) allows multiple instance parameters.

void 
three_strs_example (PFL_Str * foo, PFL_Str * bar, PFL_Str * baz)
{
    // This function provides a scope for its parameters
    
    pfl_scope_list(foo, bar, baz)  // Increment 3 ref counts
    {
        pfl_println(foo);
        pfl_println(bar);
        pfl_println(baz);
    }
    pfl_scope_end;                 // Decrement 3 ref counts  
}

Example 4 - function scope

Functions such as pfl_println() can provide their own scopes. This allows them to receive unreferenced anonymous values such as the result of a contructor function.

void
no_scope_example_1 ()
{
    // Create a new String with no references
    PFL_Str * goodbye = pfl_str_new("Goodbye");
    
    // The pfl_println() function provides its own scope
    // and will terminate an unscoped object.
    pfl_println(goodbye);
    
    // Attempting to print goodbye again would result in an error.
    // pfl_println(goodbye);
}

void
no_scope_example_2 ()
{
    // The pfl_println() function provides its own scope.
    // No memory is leaked.
    pfl_println(pfl_str_new("Goodbye again"));
}

Example 5 - pfl_scope_result()

pfl_scope_result() with pfl_scope_end_result provides a temporary scope without terminating a new object

void *
scope_result_example ()
{
    PFL_Str * res_str = pfl_str_new("Result");
    
    uint16_t ref_count = 0;
    
    pfl_scope_result(res_str)
    {
        // ref_count inside the scope is 1
        ref_count = pfl_obj_i_get_ref_count(res_str);
        
        printf("res_str ref_count within the scope is: %d.\n", ref_count);
    }
    pfl_scope_end_result;
        
    // ref_count outside the scope is 0 - this is a new object instance.
    ref_count = pfl_obj_i_get_ref_count(res_str);
    
    printf("res_str ref_count after the scope is: %d.\n", ref_count);
    
    return res_str;
}