[Tutor] Python C extension - which method?

eryk sun eryksun at gmail.com
Mon May 7 10:01:18 EDT 2018


On Mon, May 7, 2018 at 9:57 AM, Michael C
<mysecretrobotfactory at gmail.com> wrote:
> Sorry I don't understand your suggestion.
>
> Use "ctypes.CDLL" and "ctypes.WinDLL"
>
> this works,
> mydll = cdll.LoadLibrary('test.dll')
> print(mydll.sum(3,2))
>
> and this doens't
> mydll = cdll('test.dll')
> print(mydll.sum(3,2))
>
> What's the syntax of what you suggested?

Loading the library is simply `mydll = ctypes.CDLL('test')`. Note that
CDLL is uppercase. Python is a case-sensitive language.

The base shared library class is CDLL. The constructor takes the
following parameters: name, mode=DEFAULT_MODE, handle=None,
use_errno=False, and use_last_error=False. For the first 3 parameters,
if the shared library `handle` isn't provided, it calls
ctypes._dlopen(mode, name) to load the library.

The CDLL constructor dynamically defines a function-pointer subclass,
with custom _flags_ and _restype_. The _restype_ is c_int (i.e. a
32-bit C integer). The base _flags_  value is _FUNCFLAG_CDECL because
CDLL uses the cdecl calling convention. For the last two constructor
parameters, use_errno includes _FUNCFLAG_USE_ERRNO, and use_last_error
includes _FUNCFLAG_USE_LASTERROR.

The above function pointer class is used by the __getitem__ method of
a CDLL instance, which calls self._FuncPtr((name_or_ordinal, self)) to
return a function pointer instance, e.g. mydll['sum']. (Using a
subscript also allows ordinal look-up, such as mydll[42].) The
function pointer is not cached, so it has to be instantiated ever time
it's accessed this way. On the other hand, the __getattr__ method of
CDLL calls __getitem__ and caches the function pointer via
setattr(self, name, func). For example, with mydll.sum, the `sum`
function pointer is cached.

WinDLL subclasses CDLL to change the default function pointer _flags_
value to _FUNCFLAG_STDCALL because the Windows API uses the stdcall
calling convention, as do many shared libraries on Windows.

ctypes also defines a LibraryLoader class, of which ctypes.cdll and
ctypes.windll are instances, i.e. cdll = LibraryLoader(CDLL). The
constructor simply sets the shared library class as the attribute
self._dlltype. In turn, the LoadLibrary method returns
self._dlltype(name). This is silly because it's extra typing, an extra
call, and prevents passing constructor arguments (e.g.
use_last_error).

Where this goes from silly to smelly is the __getattr__ method of
LibraryLoader. This method makes the mistake of caching loaded
libraries. For example, ctypes.windll.kernel32 caches a
WinDLL('kernel32') instance on ctypes.windll. Remember that
__getattr__ is a fallback method called by __getattribute__. Thus
subsequent references to ctypes.windll.kernel32 will use the cached
library instance. This is bad because multiple packages will end up
sharing the same library instance.

Recall that the __getattr__ method of CDLL caches function pointers.
That's a good thing because function pointer look-up and instantiation
is relatively expensive. I can understand the good intentions of
carrying this over to caching entire library instances across multiple
packages. But the savings is not worth the ensuing chaos of
super-global cached state, especially for commonly used Windows
libraries such as kernel32.dll. The problem is that every function
pointer defines a prototype that consists of its errcheck, restype,
and argtypes properties, but not every package or script will define
these exactly the same, for various reasons. Thus the last one to
define the prototype wins. All other users of the library in the
process will be broken.

In short, avoid ctypes.[cdll | windll].LoadLibrary because it's silly,
and avoid ctypes.[cdll | windll].[library name] because it's smelly.


More information about the Tutor mailing list