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:
/** my_exported.h */
#include "exports.h"
typedef enum {
ZERO,
ONE,
TWO
} MyEnum;
MY_API int getAnEnumValue(MyEnum anEnum);
The implementation just gives back the integer value of the function:
int getAnEnumValue(MyEnum anEnum) {
return (int)anEnum;
}
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:
import ctypes as c
from enum import IntEnum
class MyEnum(IntEnum):
ZERO = 0
ONE = 1
TWO = 2
my_dll = c.cdll.LoadLibrary('my_dll')
get_an_enum_value = my_dll.getAnEnumValue
get_an_enum_value.argtypes = [MyEnum]
get_an_enum_value.restype = c.c_int
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. Thefrom_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, actypes
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:
class MyEnum(IntEnum):
ZERO = 0
ONE = 1
TWO = 2
# Option 1: set the _as_parameter value at construction.
def __init__(self, value):
self._as_parameter = int(value)
# Option 2: define the class method `from_param`.
@classmethod
def from_param(cls, obj):
return int(obj)
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. Any Enum
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.
class CtypesEnum(IntEnum):
"""A ctypes-compatible IntEnum superclass."""
@classmethod
def from_param(cls, obj):
return int(obj)
class MyEnum(CtypesEnum):
ZERO = 0
ONE = 1
TWO = 2
Our final (working!) Python code, then, would be:
# Import the standard library dependencies
import ctypes as c
from enum import IntEnum
# Define the types we need.
class CtypesEnum(IntEnum):
"""A ctypes-compatible IntEnum superclass."""
@classmethod
def from_param(cls, obj):
return int(obj)
class MyEnum(CtypesEnum):
ZERO = 0
ONE = 1
TWO = 2
# Load the DLL and configure the function call.
my_dll = c.cdll.LoadLibrary('my_dll')
get_an_enum_value = my_dll.getAnEnumValue
get_an_enum_value.argtypes = [MyEnum]
get_an_enum_value.restype = c.c_int
# Demonstrate that it works.
print(get_an_enum_value(MyEnum.TWO))
The output will be 2
, just as you’d expect!
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.)
Thanks to @oluseyi for being my rubber ducky while I was working this out earlier this week!
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.↩I love the redundancy of “
ctypes
types,” don’t you?↩