Python context managers for CFFI resource management
I have a project at work that involves calling into a third-party C library from Python, using CFFI. Using the library involves working with two different types of "contexts"—things that in an object-oriented language would be classes, but in C are nothing more than structs or void
pointers to opaque memory regions.
In pseudo-Python, working with one of these contexts looks like this:
context = ffi.new("ApplicationSpecificStruct *")
lib.create_context(context)
lib.do_something_with_context(context, some_data)
lib.free_context(context)
I've omitted error handling, but in reality each one of those library calls returns an error code which must be checked and handled after each call (no exceptions in C, remember!).
In order to simplify the process of working with the context, and especially of ensuring that it is still freed if an exception is thrown while it is being used, I have found Python's context management protocol helpful.
Using contextlib.contextmanager
, we can create a simple context manager that encapsulates the process of creating and freeing the C library's context.
@contextmanager
def application_context():
context = ffi.new("ApplicationSpecificStruct *")
lib.create_context(context)
try:
yield context
finally:
lib.free_context(context)
with application_context() as the_context:
lib.do_something_with_context(the_context, some_data)
If an exception is thrown inside the with
-block, the C library's context will still get freed.
Now, I suspect some will argue that this is a cop-out—that the more Pythonic thing to do would be to create proper object-oriented wrappers for the library's context types. There are certain advantages to this approach; principally, it enables cleaner code, in which lib.do_something_with_context(the_context, some_data)
becomes simply context.do_something(some_data)
.
But building fully object-oriented wrappers is both more tedious and more time-consuming, and for what I'm doing the context manager approach is perfectly suitable. Besides, even if I'd implemented fully object-oriented wrappers, I'd still want them to implement the context manager protocol—it's the Pythonic way to ensure that a resource is closed after it's been used, as in this example from the Python documentation:
with open("hello.txt") as f:
for line in f:
print(line)