1.5 Foreign Unions and Pointer Arithmetic
At the Racket level, when a representation involves a choice between different shapes, then a predicate associated with each shape easily distinguishes the cases. At the C level, different shapes can be merged with a union type to superimpose representations while relying on some explicit indicator of which choice applies at any given point.
This level of representation detail and punning is closely related to pointer arithmetic, which also ends up being used in similar places. As in C, the ffi2 library can provides a convenient veneer for certain forms of pointer arithmetic by viewing pointers as references to arrays.
1.5.1 Declaring Union Types
Let’s work with Cairo path objects, which encode vector-graphics drawing inputs. A path in Cairo is defined as a cairo_path_t:
typedef struct { |
cairo_status_t status; |
cairo_path_data_t *data; |
int num_data; |
} cairo_path_t; |
The data field is a pointer, but not to just one cairo_path_data_t; it is a pointer to an array of cairo_path_data_ts, where num_data indicates the length of that array.
Individual elements of the array are defined by a union:
union _cairo_path_data_t { |
struct { |
cairo_path_data_type_t type; |
int length; |
} header; |
struct { |
double x, y; |
} point; |
}; |
That is, each element is ether a header or a point. A header is followed by length-1 points. The cairo_path_data_type_t within a header is an integer that indicates whether encodes a move-to, line-to, etc., operation.
For the Racket version of these types, it will be helpful to pull out path_header_t and path_point_t into their own definitions before combining them with union_t.
(define-ffi2-enum cairo_path_data_type_t int_t move-to line-to curve-to close-path)
(define-ffi2-type path_header_t (struct_t [type cairo_path_data_type_t] [length int_t]))
(define-ffi2-type path_point_t (struct_t [x double_t] [y double_t]))
(define-ffi2-type cairo_path_data_t (union_t [header path_header_t] [point path_point_t])) (define-ffi2-type cairo_status_t int_t)
(define-ffi2-type cairo_path_t (struct_t [status cairo_status_t] [data cairo_path_data_t*] [num_data int_t]))
(define-cairo cairo_path_destroy (cairo_path_t* . -> . void_t) #:wrap (deallocator))
(define-cairo cairo_copy_path (cairo_t* . -> . cairo_path_t*) #:wrap (allocator cairo_path_destroy))
Let’s create a fresh context, add path elements in it, and get the accumulated path before it is used by cairo_stroke:
(define-values (bt cr) (make)) (cairo_move_to cr 50.0 50.0) (cairo_line_to cr 206.0 206.0) (cairo_move_to cr 50.0 206.0) (cairo_line_to cr 115.0 115.0) (define a-path (cairo_copy_path cr)) (cairo_stroke cr) (cairo_destroy cr)
The path should be value with status 0 (success), and it should have 8 components:
> (cairo_path_t-status a-path) 0
> (cairo_path_t-num_data a-path) 8
> (define data (cairo_path_t-data a-path)) > data #<cairo_path_data_t*>
To access an individual element of the data array, the
definition of cairo_path_data_t also defined
cairo_path_data_t*-ref, which takes a pointer to a
cairo_path_data_t array and returns a particular element of the
array. Since cairo_path_data_t values are represented on the
Racket side by a pointer, we get a cairo_path_data_t* back—
> (cairo_path_data_t*-ref data 0) #<cairo_path_data_t*>
A cairo_path_data_t can be either the header case or the point case. Defining cairo_path_data_t as a union_t type gave us cairo_path_data_t-header and cairo_path_data_t-point to recast the Racket representation as either of those. We know that that first element of data must be the header case.
> (define head1 (cairo_path_data_t-header (cairo_path_data_t*-ref data 0))) > (path_header_t-type head1) 'move-to
> (path_header_t-length head1) 2
A 'move-to command has a single point, so we know that the second element of data is the point case.
> (define pt1 (cairo_path_data_t-point (cairo_path_data_t*-ref data 1))) > (path_point_t-x pt1) 50.0
> (path_point_t-y pt1) 50.0
Next is 'line-to, and so on:
> (define head2 (cairo_path_data_t-header (cairo_path_data_t*-ref data 2))) > (path_header_t-type head2) 'line-to
> (define pt2 (cairo_path_data_t-point (cairo_path_data_t*-ref data 3))) > (path_point_t-x pt2) 206.0
> (path_point_t-y pt2) 206.0
Now that we’re done with this experiment, let’s be good citizens by cleaning up, although finalization will clean up after us if it must.
> (cairo_path_destroy a-path)
Operations like cairo_path_data_t*-ref and field accessors like cairo_path_data_t-header (or, more generally, accessors that access compound types within other compound types) ultimately perform a kind of pointer arithmetic. In case you ever need to take control of pointer arithmetic yourself, ffi2 provides ffi2-add.
1.5.2 Hiding Pointers through Conversion
The Cairo path example illustrates how to traverse a complex structure, but users of a set of Cairo bindings likely will not want to deal with all of that complexity. Let’s define a new type that converts the C representation on demand by wrapping it as a sequence that’s compatible with for.
To make a sequence, we need a new Racket structure type that implements prop:sequence. We’ll define auto_cairo_path_t as a type that wraps a pointer as a cairo-path instance.
(define-ffi2-type auto_cairo_path_t cairo_path_t* #:predicate (lambda (v) (cairo-path? v)) #:c->racket (lambda (p) (cairo-path p)) #:racket->c (lambda (rkt) (cairo-path-ptr rkt)))
(struct cairo-path (ptr) #:property prop:sequence (lambda (p) (in-cairo-path p)))
The reference to in-cairo-path is the doorway to the main conversion, which iterates through Cairo path pointers:
(define (in-cairo-path path) (define pp (cairo-path-ptr path)) ; Read the path struct fields (define path-struct (cairo-path-ptr path)) (define array-ptr (cairo_path_t-data path-struct)) (define len (cairo_path_t-num_data path-struct)) ; Generate a sequence (make-do-sequence (lambda () (values ; pos->element: extract one path command at a given position (lambda (pos) (define header-elem (cairo_path_data_t*-ref array-ptr pos)) (define header (cairo_path_data_t-header header-elem)) (define type (path_header_t-type header)) (define count (sub1 (path_header_t-length header))) (define points (for/list ([i (in-range count)]) (define pt-elem (cairo_path_data_t*-ref array-ptr (+ pos 1 i))) (define pt (cairo_path_data_t-point pt-elem)) (list (path_point_t-x pt) (path_point_t-y pt)))) (cons type points)) ; next-pos: advance past this element's header + data (lambda (pos) (define header-elem (cairo_path_data_t*-ref array-ptr pos)) (define header (cairo_path_data_t-header header-elem)) (+ pos (path_header_t-length header))) ; initial position 0 ; continue? (lambda (pos) (< pos len)) ; no other guards needed #f #f))))
Let’s redefine cairo_copy_path and cairo_path_destroy and try the earlier example again.
(define-cairo cairo_path_destroy (auto_cairo_path_t . -> . void_t) #:wrap (deallocator))
(define-cairo cairo_copy_path (cairo_t* . -> . auto_cairo_path_t) #:wrap (allocator cairo_path_destroy)) (define-values (bt cr) (make)) (cairo_move_to cr 50.0 50.0) (cairo_line_to cr 206.0 206.0) (cairo_move_to cr 50.0 206.0) (cairo_line_to cr 115.0 115.0) (define auto-path (cairo_copy_path cr)) (cairo_stroke cr) (cairo_destroy cr)
> (for ([elem auto-path]) (writeln elem))
(move-to (50.0 50.0))
(line-to (206.0 206.0))
(move-to (50.0 206.0))
(line-to (115.0 115.0))
> (cairo_path_destroy auto-path)