pixel (pinterface ) wrote,

CFFI Tricks: Automatic Freeing of C Resources

Lisp finalizers, at least the ones provided by trivial-garbage, are not at first glance particularly useful.

You can't reference the object being finalized—on some lisps, the object is already gone by the time you get to the finalizer—so you can't inspect the constituent parts of an object to operate on them. Closing over the parts only works in the event an object cannot be destructively updated—since most objects in CL can be destructively updated, that would require resetting the finalizer on update. Of course, since objects can have multiple finalizers, and you can't selectively clear them, you might accidentally wipe away someone else's finalizer.

In short, finalizers are only good for unique, constant objects. Like, say, your typical usage of a foreign-pointer.

Note (added 2011.11.13): Paul Khuong has pointed out the below is a bad idea akin to trying to stick a finalizer on a fixnum. Whoops!

(defvar *ptr* (gced-foreign-alloc :int))

For this example, we're going to define a wrapper for #'FOREIGN-ALLOC which sets up the finalization of our pointer object. In your code, you'll probably bake the finalization setup into a method on #'TRANSLATE-FROM-FOREIGN which returns a pointer.

(defun gced-foreign-alloc (type &rest rest)
  (let* ((ptr (apply #'foreign-alloc type rest))
         (addr (pointer-address ptr)))
    (tg:finalize ptr
                 (lambda ()
                   (foreign-free (make-pointer addr))))))

Because the object being finalized is not passed to the finalizer function, and because closing over it would result in keeping the pointer around indefinitely, we instead close over just enough information to recreate the pointer: the address.

That's right, we recreate the pointer in order to free it. This feels a little odd, but the restrictions on portable finalizers rather limit our options; so, we cross our fingers and hope for the best.1

If you were expecting a long entry, I'm sorry to disappoint: that's pretty much all there is to it.

(setf *ptr* nil)
(tg:gc :full t)

There are, however, some caveats to this approach.

  1. Because GC runs cannot be guaranteed to occur at any given time, the pointed-to object may be held far longer than you'd prefer.
  2. Recreating the pointer with something like (make-pointer (pointer-address *ptr*)), or even just having a #'POINTER-EQ pointer returned by a C function call, may result in multiple non-EQ objects pointing to the same memory address and too-early freeing.
  3. If the foreign side moves objects around, the address may no longer be correct once the finalizer runs.

One is only an annoyance, and three is liable to cause you problems elsewhere anyway. That second caveat is a biggie, however, and means this technique is unsafe for use when dealing with arbitrary C code—careful consideration of applicability is necessary. For example, it's useful in burgled-batteries to automatically handle decrementing reference counts on untranslatable objects, but I wouldn't use it to free() memory.

Footnotes

  1. Because finalizers operate on a specific object, so long as (eq (make-pointer x) (make-pointer x)) is NIL, there should be no issues. If it's T, some GC implementations could conceivably be tripped up, but I think that's unlikely.

Aside: Greetings Planet Lisp!

This being my first new post since I was added to Planet Lisp, here are some other, more generally applicable, CFFI Tricks you may have missed:

Tags: cffi, lisp
  • Post a new comment

    Error

    default userpic
  • 2 comments