1.4 Memory Management
A big difference between Racket and C is that memory allocation is generally automatic in Racket and explicit in C. This difference creates two challenges for a Racket 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 Racket-allocated arguments, and when the Racket memory manager runs later, then Racket-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 a foreign code retains a reference across calls or when it invokes a callback that returns to Racket.
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 Racket pointer object that refers to a cairo_t is reclaimed, but not the memory (and other resources) of the cairo_t itself.
A good first step is to define cairo_destory 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_destory call? The ffi2 library supports finalization on an object to associate a clean-up action with a Racket object when the object would otherwise be garbage-collected. Explicit deallocation is generally better that relying on finalization, but finalization can be appropriate in some cases and a good back-up in many cases.
The ffi/unsafe/alloc library further wraps finalization support to make it easy to pair an allocator with a deallocator. It supplies allocator and deallocator constructors that are meant to be used with #:wrap in define-ffi2-procedure and similar forms.
(require ffi/unsafe/alloc)
(define-cairo cairo_destroy (cairo_t* . -> . void_t) #:wrap (deallocator))
(define-cairo cairo_create (cairo_surface_t* . -> . cairo_t*) #:wrap (allocator cairo_destroy))
We define cairo_destroy first so that it can be referenced by the definition of cairo_create. The #:wrap (deallocator) part of the definition of cairo_destroy identifies it as a deallocator, which will unregister finalization (if any) for its argument. The #:wrap (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:
(define the-counter (ffi2-malloc (ffi2-sizeof int_t))) (ffi2-set! the-counter int_t 0) (define png-out (open-output-file "/tmp/img.png" #:exists 'truncate)) ; CAUTION: maybe do not run, because this may crash!
(cairo_surface_write_to_png_stream (ffi2-cast bt-surface #:to cairo_surface_t*) (lambda (counter data len) (define png-data (make-bytes len)) (ffi2-memcpy (ffi2-cast png-data #:from bytes_ptr_t) data len) (write-bytes png-data png-out) (ffi2-set! counter int_t (add1 (ffi2-ref counter int_t))) 0) the-counter) (close-output-port png-out)
> (ffi2-ref the-counter int_t) 12
This may crash, or the counter may not increment correctly. Adding a (collect-garbage) call just before or after (write-bytes png-data png-out) 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 ffi2-malloc 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 ffi2-malloc, instead of the default #:gcable mode, and release the counter with ffi2-free when it is no longer needed.
(define the-counter (ffi2-malloc #:manual (ffi2-sizeof int_t))) .... (define end-count (ffi2-ref the-counter int_t)) (ffi2-free the-counter)
To avoid the drawbacks of manual memory management, another strategy is to use the #:gcable-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.
(define the-counter (ffi2-malloc #:gcable-immobile (ffi2-sizeof int_t)))
The constructor created by define-ffi2-type with a struct_t type uses ffi2-malloc, 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 Racket. Beware of the ffi2-malloc 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.