Theology, technology, life & art

Python Enums, ctypes.Structures, and DLL exports

Illustrating the Simplest Use of ctypes Structures

For one of my contracts right now, I’m writing a ctypes Python interface to existing C code. I got stuck and confused for quite a while on getting the interface to a given function to build correctly, and along the way had to try to understand the from_param class method. The official docs are… fine… but the examples provided don’t cover the most common/basic use case: defining a simple, non-ctypes data type as an argument to a DLL-exported function.

Let’s say you have a C function exported from a DLL; for convenience we’ll make it something rather silly but easy to understand:

As I said, a very silly example. Note that you don’t technically need the (int) cast there; I’ve just put it in to be explicit about what we’re doing.

How would we use this from Python? Assuming we have a DLL named my_dll which exports the getAnEnumValue function, we’d load it up roughly like this:1

import ctypes as c
my_dll = c.cdll.LoadLibrary('my_dll')

Then, we bind to the function like this:

get_an_enum_value = my_dll.getAnEnumValue

Now, when you do this, you usually also supply the argtypes and restype values for these functions. If you’re like me, you’d think, “Oh, an enum—a perfect opportunity to use the Enum type in Python 3.4+!” and then you’d do something like this:

That seems sensible enough, but as it is, it won’t work: you’ll get an error:

TypeError: item 1 in _argtypes_ has no from_param method

This is because argtypes values have to be either existing ctypes types2 or supply either:

a from_param classmethod, or

an _as_parameter_ attribute

You can use ctypes.Structure subclasses natively that way, because the Structure class supplies its from_param classmethod. The same is not true of our custom enum class, though. As the docs put it:

If you have defined your own classes which you pass to function calls, you have to implement a from_param() class method for them to be able to use them in the argtypes sequence. The from_param() class method receives the Python object passed to the function call, it should do a typecheck or whatever is needed to make sure this object is acceptable, and then return the object itself, its _as_parameter_ attribute, or whatever you want to pass as the C function argument in this case. Again, the result should be an integer, string, bytes, a ctypes instance, or an object with an _as_parameter_ attribute.

So, to make the enum type work, we need to add a from_param class method or an _as_parameter_ attribute to it. Thus, either of these options will work:

In the constructor-based option, the value argument to the constructor is the value of the Enum instance. Since the value of anan IntEnum is always the same as the integer to whcih it is bound, we can just return int(value).

The from_param approach works a little differently, but with the same results. The obj argument to the from_param method is the object instance, in this case the enumerated value itself. AnyEnum with an integer value can be directly cast to int (though it is possible for Enum instances to have other values, so be careful), and since we have an IntEnum here, we can again just return int(obj) directly.

Now, let’s say we want to apply this pattern to more than a single IntEnum class, because our C code defines more than one enumeration. Extracting it to be common functionality is simple enough: just create a class that implements the class method, and inherit from it.

An important note: The type definition we’ve provided here will work for argtypes or restype assignments, but not as one of the members of a custom ctypes.Structure type’s _fields_ value. (Discussing how you’d go about doing that is beyond the scope of this post; the most direct approach is just to use a ctypes.c_int and note that it is intended to be used with a given IntEnum/CtypesEnum type.)

I’m leaving out the part where we build the DLL, and also the part where we locate the DLL, and only using the Windows convention. If you’re on a *nix system, you should use 'my_dll.so' instead, and in any case you need to make sure the DLL is available in the search path.↩