1.2 Foreign-Function Basics
To use the FFI, you must have in mind
A particular shared library with functions you want to call: A shared library corresponds to a file with a suffix such as ".dll", ".so", or ".dylib" (depending on the platform), or it might be a library within a ".framework" directory on Mac OS.
A particular set of names exported by the library: Shared libraries can export names for any kind of C value, but they mostly export functions. The exported names generally match the names as used from C.
The C-level type of the exported name: This is typically a function type that involves a sequence of argument types and a result type.
As a running example, let’s try callings functions exported by the Cairo graphics library. Specifically, we’ll start out by imitating the “multi segment caps” C sample code on Cairo’s samples page:
cairo_move_to (cr, 50.0, 75.0); |
cairo_line_to (cr, 200.0, 75.0); |
|
cairo_move_to (cr, 50.0, 125.0); |
cairo_line_to (cr, 200.0, 125.0); |
|
cairo_move_to (cr, 50.0, 175.0); |
cairo_line_to (cr, 200.0, 175.0); |
|
cairo_set_line_width (cr, 30.0); |
cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND); |
cairo_stroke (cr); |
The cr variable here is a Cairo drawing context, which renders commands to a drawing surface. As it happens, Racket bitmaps are implemented using Cairo, and we can get a drawing surface that targets a bitmap using its get-handle method. That method returns a pointer representation compatible with ffi/unsafe, so we must convert for ffi2 it using cpointer->ptr_t.
(require racket/draw) (define bt (make-bitmap 256 256)) (define bt-surface (cpointer->ptr_t (send bt get-handle)))
1.2.1 Loading C Libraries
The ffi2-lib function gets a handle to a library by searching the filesystem. That search that is based on the library file’s base name (without a platform-specific suffix for shared libraries) and a list of versions to try (where #f tries omitting the version):
(require ffi2) (define cairo-lib (ffi2-lib "libcairo" '("2" #f)))
Knowing the library’s name and/or path is often the trickiest part of using the FFI. Sometimes, when using a library name without a path prefix or file suffix, the library file can be located automatically, especially on Unix.
In the case of Cairo, the problem is simplified by the fact that the
library is included with a Racket distribution for Windows or Mac OS.
Using the base file name "libcairo" with version "2"
is likely to find the library as bundled with Racket or as supplied by
the operating system—
On Windows, if you are not running in DrRacket, then "libcairo-2.dll" will be found, but loading it may fail because its dependencies cannot be found; the DLLs bundled with Racket are not in the operating system’s search path. The easy solution is to require racket/draw/unsafe/cairo-lib, which will load all dependencies.
1.2.2 Finding C Functions
Assuming that cairo-lib is successful defined for the loaded Cairo shared library, we can use it to extract the addresses of functions from the library. Let’s start with cairo_create, which accepts a surface (like the one we have from a Racket bitmap) to create a drawing context.
cairo_t * cairo_create (cairo_surface_t *target); |
We can get a pointer to this function using ffi2-lib-ref:
> (ffi2-lib-ref cairo-lib 'cairo_create) #<ptr_t>
To call this function, we will need to give that pointer a type, casting via ffi2-cast to convert it to a Racket function. We could give the function the simplest possible type, which is a function type using -> that expects a generic pointer argument and returns a generic pointer argument: The -> form is used here with Racket’s “infix dot” notation. Surrounding the -> with space-separated dots causes the -> to be moved to the front of the parenthesized form.
(define cairo_create (ffi2-cast (ffi2-lib-ref cairo-lib 'cairo_create) #:to (ptr_t . -> . ptr_t)))
At this point, calling cairo_create on bt-surface would work:
> (cairo_create bt-surface) #<ptr_t>
1.2.3 Increasing Safety with Tagged Pointers
Using a generic pointer type is especially dangerous, because there will be many different pointer types that are used with Cairo functions. It’s better to give each pointer type a specific name. Named pointer types not only improve readability, they enable checks that help prevent using the wrong kind of pointer as a function argument.
(define-ffi2-type cairo_t* ptr_t) (define-ffi2-type cairo_surface_t* ptr_t)
The type cairo_surface_t* is defined here as an extension of
ptr_t, which means that the C representation is a pointer. It
also means that a Racket representation for cairo_surface_t*
will be accepted as a Racket representation of ptr_t—
(define cairo_create (ffi2-cast (ffi2-lib-ref cairo-lib 'cairo_create) #:to (cairo_surface_t* . -> . cairo_t*)))
> (cairo_create bt-surface) procedure: foreign-procedure argument does not match type
argument: #<generic>
argument type: cairo_surface_t*
In this case, we are sure that bt-surface really is a surface pointer, so we can cast it using ffi2-cast:
> (cairo_surface_t*? bt-surface) #f
> (ffi2-cast bt-surface #:to cairo_surface_t*) #<cairo_surface_t*>
> (cairo_surface_t*? (ffi2-cast bt-surface #:to cairo_surface_t*)) #t
> (cairo_create (ffi2-cast bt-surface #:to cairo_surface_t*)) #<cairo_t*>
1.2.4 Reducing Boilerplate for Function Definitions
Instead of finding a function pointer and then separately casting it to the right function type, combine those two steps using the define-ffi2-procedure form:
(define-ffi2-procedure cairo_create (cairo_surface_t* . -> . cairo_t*) #:lib cairo-lib)
In this definition, cairo_create is used both as the name to define and the name to locate in the C library. As we will see in later examples, a #:c-id option can separately specify the name to find in the C library, if needed.
Since we will define many functions from cairo-lib, it’s even better to use define-ffi2-definer to define a new form that’s like define-ffi2-procedure, except that it that has the #:lib part built in:
(define-ffi2-definer define-cairo #:lib cairo-lib) (define-cairo cairo_create (cairo_surface_t* . -> . cairo_t*)) (define-cairo cairo_move_to (cairo_t* double_t double_t . -> . void_t)) (define-cairo cairo_line_to (cairo_t* double_t double_t . -> . void_t)) (define-cairo cairo_set_line_width (cairo_t* double_t . -> . void_t)) (define-cairo cairo_stroke (cairo_t* . -> . void_t))
You may notice that the C type names double and void are represented here by names that end in _t: double_t and void_t. The ffi2 convention is to use a _t suffix for all type and type-constructor names, even for traditional C types like int, double, and void.
At this point, we have enough functions to draw a line on the bitmap.
(require pict)
(define (show bt) (linewidth 2 (frame (bitmap bt))))
> (define cr (cairo_create (ffi2-cast bt-surface #:to cairo_surface_t*))) > (cairo_move_to cr 50.0 50.0) > (cairo_line_to cr 206.0 206.0) > (cairo_set_line_width cr 5.0) > (cairo_stroke cr) > (show bt)
1.2.5 Defining a Type Conversion
To match the original Cairo example, we’ll need to call cairo_set_line_cap. The cairo_set_line_cap function takes a cairo_line_cap_t argument, where cairo_line_cap_t is defined in C using enum. The most natural translation to Racket is to use a symbol for each variant, instead of integer constants. We can define a new type cairo_line_cap_t using define-ffi2-type, and we can supply #:predicate, #:racket->c, and #:c->racket functions to translate between symbols and integers:
(require racket/list) (define line-cap-symbols '(butt round square))
(define-ffi2-type cairo_line_cap_t int_t #:predicate (lambda (v) (memq v line-cap-symbols)) #:racket->c (lambda (sym) (index-of line-cap-symbols sym)) #:c->racket (lambda (i) (list-ref line-cap-symbols i))) (define-cairo cairo_set_line_cap (cairo_t* cairo_line_cap_t . -> . void_t)) (define-cairo cairo_get_line_cap (cairo_t* . -> . cairo_line_cap_t))
> (cairo_set_line_cap cr 'buzz) cairo_set_line_cap: foreign-procedure argument does not
match type
argument: 'buzz
argument type: cairo_line_cap_t
> (cairo_set_line_cap cr 'round) > (cairo_get_line_cap cr) 'round
the #:predicate function checks whether a Racket value is suitable as an argument to #:racket->c. It needs to be provided here to replace the check that would otherwise be inherited from int_t. Separating the predicate from the conversion function (instead of building a check into the conversion function) supports better error checking and enables fine-grained control over whether checks are improved. Checks are omitted with a module that declares (#%declare #:unsafe), for example.
Mapping between symbols and integers is common enough that ffi2 provides define-ffi-enum, which achieves the same result for defining cairo_line_cap_t:
(define-ffi2-enum cairo_line_cap_t int_t butt round square)
1.2.6 Putting it All Together
We can now implement the original Cairo example. Here’s the complete code and the result that DrRacket shows:
#lang racket
(require ffi2 racket/draw pict) (define cairo-lib (ffi2-lib "libcairo" '("2" #f))) (define-ffi2-definer define-cairo #:lib cairo-lib) (define-ffi2-type cairo_t* void_t*) (define-ffi2-type cairo_surface_t* void_t*)
(define-ffi2-enum cairo_line_cap_t int_t butt round square) (define-cairo cairo_create (cairo_surface_t* . -> . cairo_t*)) (define-cairo cairo_move_to (cairo_t* double_t double_t . -> . void_t)) (define-cairo cairo_line_to (cairo_t* double_t double_t . -> . void_t)) (define-cairo cairo_set_line_width (cairo_t* double_t . -> . void_t)) (define-cairo cairo_stroke (cairo_t* . -> . void_t)) (define-cairo cairo_set_line_cap (cairo_t* cairo_line_cap_t . -> . void_t))
(define (make) (define bt (make-bitmap 256 256)) (define bt-surface (cpointer->ptr_t (send bt get-handle))) (values bt (cairo_create (ffi2-cast bt-surface #:to cairo_surface_t*))))
(define (show bt) (linewidth 2 (frame (bitmap bt)))) (define-values (bt cr) (make)) (cairo_move_to cr 50.0 75.0) (cairo_line_to cr 200.0 75.0) (cairo_move_to cr 50.0 125.0) (cairo_line_to cr 200.0 125.0) (cairo_move_to cr 50.0 175.0) (cairo_line_to cr 200.0 175.0) (cairo_set_line_width cr 30.0) (cairo_set_line_cap cr 'round) (cairo_stroke cr) (show bt)

