On this page:
1.4.1 Reliable Release of Resources
1.4.2 Pointers and GC-Managed Allocation

1.4 Memory Management🔗ℹ

A big difference between Rhombus and C is that memory allocation is generally automatic in Rhombus and explicit in C. This difference creates two challenges for a Rhombus binding to C functions:

  • When a foreign function allocates an object, often the object needs to be freed by a matching call to another foreign function.

  • When a foreign function is given Rhombus-allocated arguments, and when the Rhombus memory manager runs later, then Rhombus-allocated objects might get freed or moved in memory. In that case, references held by foreign code become invalid. This is a problem only when foreign code retains a reference across calls or when it invokes a callback that returns to Rhombus.

1.4.1 Reliable Release of Resources🔗ℹ

We are being sloppy with our calls to cairo_create. As the function name suggests, cairo_create allocates a new drawing context, and we are never deallocating it. The Rhombus pointer object that refers to a cairo_t* value is reclaimed, but not the memory (and other resources) of the cairo_t itself.

A good first step is to define cairo_destroy and apply it to any drawing context that we no longer need, but what if we forget, or what if an error occurs before we can reach a cairo_destroy call? The ffi library supports finalization on an object to associate a clean-up action with a Rhombus object when the object would otherwise be garbage-collected. Explicit deallocation is generally better than relying on finalization, but finalization can be appropriate in some cases and a good back-up in many cases.

The ffi/finalize library further wraps finalization support to make it easy to pair an allocator with a deallocator. It supplies finalize.allocator and finalize.deallocator constructors that are meant to be used with ~wrap in foreign.fun and similar forms.

import:

  ffi/finalize

cairo.fun cairo_destroy(cairo_t*) :: void_t:

  ~wrap: finalize.deallocator()

cairo.fun cairo_create(cairo_surface_t*) :: cairo_t*:

  ~wrap: finalize.allocator(cairo_destroy)

We define cairo_destroy first so that it can be referenced by the definition of cairo_create. The ~wrap: finalize.deallocator() part of the definition of cairo_destroy identifies it as a deallocator, which will unregister finalization (if any) for its argument. The ~wrap: finalize.allocator(cairo_destroy) part of the definition of cairo_create identifies it as an allocator whose result can be finalized by calling the given deallocator.

1.4.2 Pointers and GC-Managed Allocation🔗ℹ

Going back to the callback example, suppose we decide to use the closure_data argument, after all, to pass along a pointer to an integer that is updated on each callback invocation:

def the_counter = new int_t

the_counter[0] := 0

def png_out = Port.Output.open_file("/tmp/img.png", ~exists: #'truncate)

// CAUTION: maybe do not run, because this may crash!

cairo_surface_write_to_png_stream(

  cast (cairo_surface_t*) bt_surface,

  fun (counter :~ foreign.type int_t*, data, len):

    def png_data = Bytes.make(len)

    memcpy(

      cast ~from (bytes_ptr_t) ~to (ptr_t) png_data,

      data,

      len

    )

    Port.Output.write_bytes(png_out, png_data)

    counter[0] := counter[0] + 1

    0,

  the_counter

)

> the_counter[0]

12

This may crash, or the counter may not increment correctly. Adding a memory.gc() call just before or after Port.Output.write_bytes(png_out, png_data) greatly increases the chance of a failing counter increment.

The problem is that a garbage collection during the callback is likely to move the object allocated by new for the_counter, but cairo_surface_write_to_png_stream will continue to provide the address that it was originally given as its third argument.

One solution is to follow C allocation conventions and use manual memory management for the_counter. Use the ~manual allocation mode for new, instead of the default ~gcable mode, and release the counter with free when it is no longer needed.

def the_counter = new ~manual int_t

// ...

def end_count = the_counter[0]

free(the_counter)

To avoid the drawbacks of manual memory management, another strategy is to use the ~immobile allocation mode, which allocates an object with automatic memory management, but the memory manager is not allowed to relocate the object as long as it is live.

def the_counter = new ~immobile int_t

The constructor created by foreign.struct uses new, and it similarly accepts an allocation mode, as does the list_t type constructor. In all cases, the default is ~gcable allocation, because most foreign functions use their arguments and return without calling back to Rhombus. Beware of the new used for conversion by string_t and bytes_t, which is always in ~gcable mode; those convenience types may not be suitable for some foreign functions.