1.3 Composite Foreign Data
Many C functions work with simple forms of data—
1.3.1 Array Arguments
Let’s look at the “dash” example from Cairo’s samples page:
double dashes[] = {50.0, 10.0, 10.0, 10.0}; |
int ndash = sizeof(dashes)/sizeof(dashes[0]); |
double offset = -50.0; |
|
cairo_set_dash (cr, dashes, ndash, offset); |
cairo_set_line_width (cr, 10.0); |
|
cairo_move_to (cr, 128.0, 25.6); |
cairo_line_to (cr, 230.4, 230.4); |
cairo_rel_line_to (cr, -102.4, 0.0); |
cairo_curve_to (cr, 51.2, 230.4, 51.2, 128.0, 128.0, 128.0); |
|
cairo_stroke (cr); |
Two of the new functions are straightforward to define:
cairo.fun cairo_rel_line_to(cairo_t*, double_t, double_t) :: void_t
cairo.fun cairo_curve_to(cairo_t*,
The most interesting function is cairo_set_dash, which takes an array argument. The C type signature for cairo_set_dash is
void cairo_set_dash (cairo_t *cr, |
const double *dashes, |
int num_dashes, |
double offset); |
where the num_dashes argument tells cairo_set_dash the length of the dashes array.
With ffi, we set up the array argument by allocating memory using new and then filling the memory with assignments. The allocated memory is then passed to cairo_set_dash as a pointer. The following approach defines cairo_set_dash_raw as a direct reference to the C function that expects a pointer, and then it defines a Rhombus-friendly wrapper that converts a list into an allocated array.
// Low-level binding: takes a pointer and length
cairo.fun cairo_set_dash_raw(cairo_t*, ptr_t, int_t, double_t) :: void_t:
~c_id: cairo_set_dash
// Rhombus-friendly wrapper that takes a list
fun cairo_set_dash(ctx, dashes :: List.of(Flonum), offset :: Flonum):
def n = List.length(dashes)
arr[i] := dashes[i]
free(arr)
> cairo_set_dash(cr, [50.0, 10.0, 10.0, 10.0], -50.0)
This wrapper approach is fine, but the conversion from a Rhombus list to a C array is also something that can be handled by a new converting type. In fact, the ffi library provides a list_t type constructor to implement that conversion, where list_t takes the element type as an argument. At the same time, we need to pass the length of the list/array to cairo_set_dash, and that’s a separate argument. To deal with that kind of interaction among arguments, when you define a function type with ->, you can give names to arguments and refer to them in later expressions that supply automatic arguments.
cairo.fun cairo_set_dash(
cairo_t*,
int_t = List.length(lst),
The lst :: part of this definition gives a name to the value supplied as the second argument to cairo_set_dash. The list_t(double_t) part insists that the argument is a list of floating-point numbers, and it converts that list to an allocated array. For the third argument, meanwhile, the = List.length(lst) part computes the argument automatically (i.e., it’s not merely optional, but never provided by a caller). The end result is that cairo_set_dash behaves the same as the wrapper cairo_set_dash function defined above.
cairo_set_dash(cr, dashes, offset)
cairo_set_line_width(cr, 10.0)
cairo_move_to(cr, 128.0, 25.6)
cairo_line_to(cr, 230.4, 230.4)
cairo_rel_line_to(cr, -102.4, 0.0)
cairo_curve_to(cr, 51.2, 230.4, 51.2, 128.0, 128.0, 128.0)
cairo_stroke(cr)
> show(bt)
1.3.2 C Structs
For a more advanced example, let’s measure text to scale it into our bitmap. The relevant Cairo function is cairo_text_extents:
void cairo_text_extents (cairo_t *cr, |
const char *utf8, |
cairo_text_extents_t *extents); |
where cairo_text_extents_t is a struct:
typedef struct { |
double x_bearing; |
double y_bearing; |
double width; |
double height; |
double x_advance; |
double y_advance; |
} cairo_text_extents_t; |
We can define a cairo_text_extents_t type for Rhombus using foreign.struct:
foreign.struct cairo_text_extents_t(
)
This single declaration automatically creates several bindings:
cairo_text_extents_t: works both as a type and as a type for use with new to allocate a cairo_text_extents_t instance.
cairo_text_extents_t*: a type for a pointer to a cairo_text_extents_t instance.
cairo_text_extents_t: an annotation for pointers to cairo_text_extents_t instances.
cairo_text_extents_t as a veneer: field accessors like .width and .x_bearing that take a pointer to a cairo_text_extents_t instance and extract a corresponding field value, converting it from C to Rhombus as needed.
Using this definition, you can construct instances directly:
def extents = new cairo_text_extents_t(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
Or allocate one uninitialized:
We can define a “raw” version of cairo_text_extents that will write into a struct pointer that the caller provides:
cairo.fun cairo_text_extents_raw(
~c_id: cairo_text_extents
A string_t type for the second argument lets a caller provide a Rhombus string that is converted into a UTF-8-encoded, null-terminated representation of the string for the C side.
Note that the third argument to cairo_text_extents_raw is cairo_text_extents_t* (ending with *), not cairo_text_extents_t (no *). The C function accepts a pointer argument and not an immediate struct argument. On the Rhombus side, cairo_text_extents_t* and cairo_text_extents_t have the same representation, but the distinction is crucial on the C side, because functions accept and return struct values differently than pointers.
To use cairo_text_extents_raw, a caller must allocate their own cairo_text_extents_t instance:
> cairo_text_extents_raw(cr, "hello world", extents)
> extents.width
54.0
Similar to the way we made cairo_set_dash allocate an array, we can improve cairo_text_extents by having it automatically allocate a cairo_text_extents_t instance. A body after the result type in a foreign.fun definition provides a result expression to substitute for the C function’s result. An extra :: foreign.type cairo_text_extents_t* provides a Rhombus result annotation, as opposed to the C function’s result.
The result could also be written as foreign.type followed by cairo_text_extents_t* instead of just cairo_text_extents_t. The Racket representation is the same for cairo_text_extents_t and cairo_text_extents_t*, but using the * operator for a type would mean that we must use foreign.type to bring the Rhombus annotation and C type contexts, while cairo_text_extents_t is defined for both contexts.
cairo.fun cairo_text_extents(
cairo_t*,
exts
> cairo_text_extents(cr, "hello world").width
54.0
To round out the example, let’s implement a function that draws text scaled to fit the bitmap width:
1.3.3 Callbacks from C to Rhombus
To save a Cairo-based drawing to a PNG file, we could use Rhombus’s drawing utilities. Let’s instead directly use Cairo’s cairo_surface_write_to_png_stream to write bytes to a file. The C signature is
cairo_status_t |
cairo_surface_write_to_png_stream (cairo_surface_t *surface, |
cairo_write_func_t write_func, |
void *closure_data); |
where cairo_write_func_t is a function type:
cairo_status_t |
(*cairo_write_func_t) (void *closure_data, |
const unsigned char *data, |
unsigned int length); |
The closure_data pointer as the third argument to cairo_surface_write_to_png_stream is passed along as the first argument to write_func, which is C’s manual way of forming closures. We’ll let Rhombus take care of closures automatically for us, so we’ll just use a null pointer as closure_data. The interesting part is the function pointer passed as write_func.
For the type of the function argument to cairo_surface_write_to_png_stream, we can nest a -> type for the argument inside a -> type for the function. We’ll also handle the result status by raising an exception if the status is not 0.
cairo.fun cairo_surface_write_to_png_stream(
cairo_surface_t*,
Then, when we pass a Rhombus function as the second argument to cairo_surface_write_to_png_stream, it will be called to write out bytes of the PNG encoding:
def png_out = Port.Output.open_file("/tmp/img.png", ~exists: #'truncate)
cairo_surface_write_to_png_stream(
fun (ignored, 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)
// return 0 to mean "success"
0,
)
Port.Output.close(png_out)
This example illustrates a technique for converting a pointer to bytes into a byte string. The Port.Output.write_bytes function wants a byte string, not a C-level pointer to bytes, so we copy from the data pointer into a freshly allocated byte string. The copy operation needs two pointers, so we cast a byte string to a pointer using bytes_ptr_t. Note that casting from bytes_t, instead of bytes_ptr_t, would make a copy of the byte string, which would not work.
One catch here is that a callback from a foreign function is always in atomic mode. In this case, the callback writes to a port png_out that is not used from any other thread, so atomic mode causes no problems; writing will not get stuck trying to acquire a lock.
Another subtlety is that the callback must remain live as long as it might be called. The Rhombus procedure supplied as a callback will not be garbage collected while it is being called, of course, but the C representation of the callback is a wrapper that refers to the Rhombus procedure, not the other way around. So, the callback could potentially trigger a garbage collector that reclaims the callback wrapper. In this case, the callback is kept live by virtue of being an argument to the foreign function, and the callback is not used after cairo_surface_write_to_png_stream returns. When a callback is registered for future use, then more care must be taken to retain the pointer representing a callback.

