If you have been reading
curl_easy_setopt(3)
, you should have noticed that some options
accept a function pointer. In particular, we need one function
pointer to set as CURLOPT_WRITEFUNCTION
, to be called by
libcurl
rather than the reverse, in order to receive data as it
is downloaded.
A binding writer without the aid of FFI usually approaches this problem by writing a C function that accepts C data, converts to the language's internal objects, and calls the callback provided by the user, again in a reverse of usual practices.
The CFFI approach to callbacks precisely mirrors its differences
with the non-FFI approach on the “calling C from Lisp”
side, which we have dealt with exclusively up to now. That is, you
define a callback function in Lisp using defcallback
, and
CFFI effectively creates a C function to be passed as a function
pointer.
Implementor's note: This is much trickier than calling C functions from Lisp, as it literally involves somehow generating a new C function that is as good as any created by the compiler. Therefore, not all Lisps support them. See Implementation Support, for information about CFFI support issues in this and other areas. You may want to consider changing to a Lisp that supports callbacks in order to continue with this tutorial.
Defining a callback is very similar to defining a callout; the main
difference is that we must provide some Lisp forms to be evaluated as
part of the callback. Here is the signature for the function the
:writefunction
option takes:
size_t function(void *ptr, size_t size, size_t nmemb, void *stream);
Implementor's note: size_t is almost always an unsigned int. You can get this and many other types using feature tests for your system by using cffi-grovel.
The above signature trivially translates into a CFFI
defcallback
form, as follows.
;;; Alias in case size_t changes. (defctype size :unsigned-int) ;;; To be set as the CURLOPT_WRITEFUNCTION of every easy handle. (defcallback easy-write size ((ptr :pointer) (size size) (nmemb size) (stream :pointer)) (let ((data-size (* size nmemb))) (handler-case ;; We use the dynamically-bound *easy-write-procedure* to ;; call a closure with useful lexical context. (progn (funcall (symbol-value '*easy-write-procedure*) (foreign-string-to-lisp ptr data-size nil)) data-size) ;indicates success ;; The WRITEFUNCTION should return something other than the ;; #bytes available to signal an error. (error () (if (zerop data-size) 1 0)))))
First, note the correlation of the first few forms, used to declare the C function's signature, with the signature in C syntax. We provide a Lisp name for the function, its return type, and a name and type for each argument.
In the body, we call the dynamically-bound
*easy-write-procedure*
with a “finished” translation, of
pulling together the raw data and size into a Lisp string, rather than
deal with the data directly. As part of calling
curl_easy_perform
later, we'll bind that variable to a closure
with more useful lexical bindings than the top-level
defcallback
form.
Finally, we make a halfhearted effort to prevent non-local exits from
unwinding the C stack, covering the most likely case with an
error
handler, which is usually triggered
unexpectedly.1 The reason is that most C code is written to
understand its own idiosyncratic error condition, implemented above in
the case of curl_easy_perform
, and more “undefined behavior”
can result if we just wipe C stack frames without allowing them to
execute whatever cleanup actions as they like.
Using the CURLoption
enumeration in curl.h once more, we
can describe the new option by modifying and reevaluating
define-curl-options
.
(define-curl-options curl-option
(long 0 objectpoint 10000 functionpoint 20000 off-t 30000)
(:noprogress long 43)
(:nosignal long 99)
(:errorbuffer objectpoint 10)
(:url objectpoint 2)
(:writefunction functionpoint 11)) ;new item here
Finally, we can use the defined callback and the new
set-curl-option-writefunction
to finish configuring the easy
handle, using the callback
macro to retrieve a CFFI
:pointer
, which works like a function pointer in C code.
cffi-user> (set-curl-option-writefunction
*easy-handle* (callback easy-write))
=> 0
[1] Unfortunately, we can't protect against
all non-local exits, such as return
s and throw
s,
because unwind-protect
cannot be used to “short-circuit” a
non-local exit in Common Lisp, due to proposal minimal
in
ANSI issue Exit-Extent. Furthermore, binding an
error
handler prevents higher-up code from invoking restarts
that may be provided under the callback's dynamic context. Such is
the way of compromise.