Getting Started
Warning
Read the ctypes documentation first if you have not done so already. zugbruecke is a drop-in replacement for ctypes. zugbruecke’s documentation does NOT cover how to use ctypes.
Minimal Example
A lot of code, which was written with ctypes
’ cdll
, windll
or oledll
in mind and which runs under Windows, should run just fine with zugbruecke on Unix. Let’s assume you are dealing with a DLL named demo.dll
containing a compiled version of the following bit of C code:
float
__stdcall __declspec(dllimport)
simplefunc(
float a,
float b
)
{
return a - (a / b);
}
On Windows, you could call it via ctypes
by importing it first:
import ctypes
On Unix-like systems such as Linux, Mac OS or BSD, you simply import zugbruecke.ctypes
instead:
import zugbruecke.ctypes as ctypes
From now on, the code is - in most but not all cases - the same. You can call the function as follows:
simplefunc = ctypes.windll.LoadLibrary('demo.dll').simplefunc
simplefunc.argtypes = (ctypes.c_float, ctypes.c_float)
simplefunc.restype = ctypes.c_float
result = simplefunc(20.0, 1.07)
print(f'Got "{result:f}".')
It will happily print Got "1.308412".
Optional argtypes
and restype
annotations
The original ctypes allows to omit specifying argtypes
and restype
annotations in some cases. It tends to default to integers, ctypes.c_int
, in most cases, while also accepting pointers. zugbruecke is more strict in those cases and basically only allows passing integers without type annotations.
Note
Differing from original ctypes, it is recommended to always specify argtypes
and restype
when using zugbruecke. Pointer types must be explicitly indicated. ctypes may accept data and even implicitly pass it as pointers in one way or another which is a mechanism not properly supported by zugbruecke.
Platform-Independent Code
Because of the drop-in replacement design of zugbruecke, it is therefore possible to write platform-independent Python code which works under both Unices and Windows.
from sys import platform
if any(platform.startswith(name) for name in ('linux', 'darwin', 'freebsd')):
import zugbruecke.ctypes as ctypes
elif platform.startswith('win'):
import ctypes
else:
raise SystemError('unsupported platform')
32 bit and 64 bit DLLs
Thanks to Wine, which can run both in 32 bit and in 64 bit mode, it is perfectly possible to call into both 32 bit and 64 bit DLLs - even side by side. For this and similar use cases, zugbruecke allows to start multiple sessions simultaneously, each with its own separate configuration. Instead of importing zugbruecke.ctypes
, you must create instances from the zugbruecke.CtypesSession
class. Consider the following example:
from zugbruecke import CtypesSession
ctypes_windows32 = CtypesSession(arch = 'win32')
ctypes_windows64 = CtypesSession(arch = 'win64')
Platform-Dependent Integer Widths
The width, i.e. size in bytes, of long integer types differs between platforms and therefore also between Unix-like systems and Windows systems. zugbruecke is no exception here, where long integer types have different widths between the Unix-side from where you want to call a DLL function and the Wine-side where the DLL function is being executed:
(env) user@comp:~> python -m platform
Linux
(env) user@comp:~> python -c "from ctypes import c_long, c_ulong, sizeof as s; \
> print(s(c_long), s(c_ulong))"
8 8
(env) user@comp:~> wenv python -m platform
Windows
(env) user@comp:~> wenv python -c "from ctypes import c_long, c_ulong, sizeof as s; \
> print(s(c_long), s(c_ulong))"
4 4
For consistency, zugbruecke deals with this by sticking to the width of long integers on Unix-like systems, i.e. 8 bytes.
Warning
c_long on the Unix side is being translated to c_int64 on the Wine side. c_ulong on the Unix side is being translated to c_uint64 on the Wine side. If you require c_long or c_ulong following Windows’ definitions, you will have to use c_int32 or c_uint32 on the Unix side.
Note
Integers with specified length, e.g. c_int32, always have the same length on both, the Unix and the Wine side.
Platform-Dependent Floating-Point Behaviour
Single-precision floats, c_float, 32 bits or 4 bytes long as per IEEE 754, as well as double-precision floats, c_double, 64 bits or 8 bytes long as per IEEE 754, behave identical both on Unix-like systems and Windows. They can be passed to and retrieved from DLL functions without issues.
Warning
Long double types or quadruple-precision floating-point types pose a special kind of problem. Long story short: Support and implementation between platforms differ significantly. Windows, a bit simplified, has virtually no support for those types although e.g. certain C compilers offer some support on Windows on their own. Since zugbruecke is built on top of CPython and ctypes, it is following its capabilities. While c_longdouble is in fact 128 bits or 16 bytes long on Linux for instance, it maps to c_double on Windows at 64 bits or 8 bytes. Passing floating point numbers longer than 64 bits is therefore basically not supported by zugbruecke. This may be partially worked around by passing buffers of 16 bytes per number or using numpy’s support of long double.
(env) user@comp:~> python -m platform
Linux
(env) user@comp:~> python -c "from ctypes import c_longdouble, sizeof as s; \
print(s(c_longdouble))"
16
(env) user@comp:~> wenv python -m platform
Windows
(env) user@comp:~> wenv python -c "from ctypes import c_longdouble, sizeof as s; \
print(s(c_longdouble))"
8
Warning
Half-precision floating-point types have recently seen a boost in popularity thanks to machine learning applications. There are completely different variations/implementations of the idea going by different names such as bfloat16. Since zugbruecke is built on top of CPython and ctypes, which does not offer any form of support for half-precision floating-point types, it is following its capabilities i.e. there is no direct support for them in zugbruecke. This may be partially worked around by passing buffers of 2 bytes per number or using numpy’s support of half-precision floating-point. numpy’s support of bfloat16 is still, as of late 2022, under development.
Calling Conventions
While the handling of different calling conventions is absolutely identical between ctypes and zugbruecke, it is actually a common trap for beginners when using zugbruecke (or ctypes) for the first time. The confusion arises around ctypes.CDLL
/ ctypes.cdll
and ctypes.WinDLL
/ ctypes.windll
, which refer to two different calling conventions. In fact, both types of calling conventions can be found on Windows. Any given DLL might use either one. While both calling conventions are ironically identical for 64-bit DLLs and can be used interchangeably in those cases, calling conventions actually differ for 32-bit DLLs. The main difference or symptom is that function arguments are passed to the function in a location differing by an offset of 8 bytes. You might see exceptions similar to “arguments 8 bytes too short” or “arguments 8 bytes too long” if using the wrong calling convention. In rare cases, you might not even get an exception but receive plain wrong results from the called function instead.
Note
If the DLL’s C-code is available, the calling convention can be deduced from “annotations” in front of functions. __stdcall
points to WINDLL
, while __cdecl
points to CDLL
.
Memory Synchronization
Because zugbruecke executes DLL routines in a separate Windows Python process on top of Wine, it must translate pointers and keep memory between the Unix Python and the Windows Python processes in sync. zugbruecke can handle this task partially automatically but does require special memsync
directives in certain cases. A good set of introductory examples is provided in the chapter covering pointers and memory synchronization.
Further Examples
For an overview over its entire range of capabilities have a look at zugbruecke’s test suite.