Next: , Previous: Tutorial-Completion, Up: Tutorial

4.11 Defining new types

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 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)
        (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 CURLcodes 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-handles 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-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 translate-to-foreign.

  (defclass easy-handle ()
    ((pointer :initform (curl-easy-init)
              :documentation "Foreign pointer from curl_easy_init")
      :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."
     (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.