We’ve occasionally used the
defctype macro in previous sections
as a kind of documentation, much what you’d use
typedef for in
C. We also tried one special kind of type definition, the
defcenum type. See defcstruct, for a definition macro that
may come in handy if you need to use C
structs as data.
However, all of these are mostly sugar for the powerful underlying
foreign type interface called type translators. You can easily
define new translators for any simple named foreign type. Since we’ve
defined the new type
curl-code to use as the return type for
libcurl functions, we can use that to directly convert
cURL errors to Lisp errors.
defctype’s purpose is to define simple
aliases. In order to use type translators we must use the
define-foreign-type macro. So let’s redefine
(define-foreign-type curl-code-type () () (:actual-type :int) (:simple-parser curl-code))
define-foreign-type is a thin wrapper around
For now, all you need to know in the context of this example is that
it does what
(defctype curl-code :int) would do and,
additionally, defines a new class
curl-code-type which we will
take advantage of shortly.
CURLcode enumeration seems to follow the typical error code
convention of ‘0’ meaning all is well, and each non-zero integer
indicating a different kind of error. We can apply that trivially to
differentiate between normal exits and error exits.
(define-condition curl-code-error (error) (($code :initarg :curl-code :reader curl-error-code)) (:report (lambda (c stream) (format stream "libcurl function returned error ~A" (curl-error-code c)))) (:documentation "Signalled when a libcurl function answers a code other than CURLE_OK.")) (defmethod translate-from-foreign (value (type curl-code-type)) "Raise a CURL-CODE-ERROR if VALUE, a curl-code, is non-zero." (if (zerop value) :curle-ok (error 'curl-code-error :curl-code value)))
The heart of this translator is new method
translate-from-foreign. By specializing the type
curl-code-type, we immediately modify the behavior
of every function that returns a
curl-code to pass the result
through this new method.
To see the translator in action, try invoking a function that returns
curl-code. You need to reevaluate the respective
defcfun form so that it picks up the new
CFFI-USER> (set-curl-option-nosignal *easy-handle* 1) ⇒ :CURLE-OK
As the result was ‘0’, the new method returned
just as specified.10 I will leave disjoining the separate
CURLcodes into condition types and improving the
function as an exercise for you.
The creation of
*easy-handle-errorbuffers* as properties of
is a kluge. What we really want is a Lisp structure that stores these
properties along with the C pointer. Unfortunately,
easy-handle is currently just a fancy name for the foreign type
:pointer; the actual pointer object varies from Common Lisp
implementation to implementation, needing only to satisfy
pointerp and be returned from
make-pointer and friends.
One solution that would allow us to define a new Lisp structure to
easy-handles would be to write a wrapper around every
function that currently takes an
easy-handle; the wrapper would
extract the pointer and pass it to the foreign function. However, we
can use type translators to more elegantly integrate this
“translation” into the foreign function calling framework, using
(defclass easy-handle () ((pointer :initform (curl-easy-init) :documentation "Foreign pointer from curl_easy_init") (error-buffer :initform (foreign-alloc :char :count *curl-error-size* :initial-element 0) :documentation "C string describing last error") (c-strings :initform '() :documentation "C strings set as options")) (:documentation "I am a parameterization you may pass to curl-easy-perform to perform a cURL network protocol request.")) (defmethod initialize-instance :after ((self easy-handle) &key) (set-curl-option-errorbuffer self (slot-value self 'error-buffer))) (defun add-curl-handle-cstring (handle cstring) "Add CSTRING to be freed when HANDLE is, answering CSTRING." (car (push cstring (slot-value handle 'c-strings)))) (defun get-easy-handle-error (handle) "Answer a string containing HANDLE's current error message." (foreign-string-to-lisp (slot-value handle 'error-buffer))) (defun free-easy-handle (handle) "Free CURL easy interface HANDLE and any C strings created to be its options." (with-slots (pointer error-buffer c-strings) handle (curl-easy-cleanup pointer) (foreign-free error-buffer) (mapc #'foreign-string-free c-strings))) (define-foreign-type easy-handle-type () () (:actual-type :pointer) (:simple-parser easy-handle)) (defmethod translate-to-foreign (handle (type easy-handle-type)) "Extract the pointer from an easy-HANDLE." (slot-value handle 'pointer))
While we changed some of the Lisp functions defined earlier to use CLOS slots rather than hash tables, the foreign functions work just as well as they did before.
The greatest strength, and the greatest limitation, of the type translator comes from its generalized interface. As stated previously, we could define all foreign function calls in terms of the primitive foreign types provided by CFFI. The type translator interface allows us to cleanly specify the relationship between Lisp and C data, independent of where it appears in a function call. This independence comes at a price; for example, it cannot be used to modify translation semantics based on other arguments to a function call. In these cases, you should rely on other features of Lisp, rather than the powerful, yet domain-specific, type translator interface.
It might be better to return
:curle-ok in real code, but this is good