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 struct
s 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
various libcurl
functions, we can use that to directly convert
cURL errors to Lisp errors.
defctype
's purpose is to define simple typedef
-like
aliases. In order to use type translators we must use the
define-foreign-type
macro. So let's redefine curl-code
using it.
(define-foreign-type curl-code-type ()
()
(:actual-type :int)
(:simple-parser curl-code))
define-foreign-type
is a thin wrapper around defclass
.
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.
The 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
parameter on 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
a curl-code
. You need to reevaluate the respective
defcfun
form so that it picks up the new curl-code
definition.
cffi-user> (set-curl-option-nosignal *easy-handle* 1)
=> :CURLE-OK
As the result was ‘0’, the new method returned :curle-ok
,
just as specified.1 I will leave disjoining the separate
CURLcode
s into condition types and improving the :report
function as an exercise for you.
The creation of *easy-handle-cstrings*
and
*easy-handle-errorbuffers*
as properties of easy-handle
s
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
represent easy-handle
s 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
translate-to-foreign
.
(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.
[1] It might be better to return
(values)
than :curle-ok
in real code, but this is good
for illustration.