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.
~wrap: finalize.deallocator()
~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 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(
fun (counter :~ foreign.type int_t*, data, len):
def png_data = Bytes.make(len)
cast ~from (bytes_ptr_t) ~to (ptr_t) png_data,
data,
len
)
Port.Output.write_bytes(png_out, png_data)
0,
the_counter
)
Port.Output.close(png_out)
> 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.
// ...
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.
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.