Accessing Global Variables and Constants¶
Neither ctypes
nor cslug offer much help to access C globals (although
the latter may change if I feel motivated enough to do something about it).
If you're looking for a way to access constants from Python, the preferred approach is to define them in Python then pass them to C using a header file. And if you have non constant global variables then you are advised to get rid of them - they're considered bad practice and rightly so.
If I have been unsuccessful in putting you off yet then here's how to do it. Throughout this page we'll use the following C code and Python setup:
#include <stdint.h>
#include <stddef.h>
int an_int = 42;
float a_float = 1. / 3.;
double a_double = 1. / 3.;
int8_t an_8_bit_int = 43;
char a_bytes_array[] = "Hello, my name is Ned.";
wchar_t a_string[] = L"Сәлам. Минем исемем Нед.";
char contains_null[] = "This sentence has a \00 in the middle of it.";
int len_contains_null = sizeof(contains_null) / sizeof(char);
int get_an_int() {
return an_int;
}
const int a_const_int = 3;
import ctypes
from cslug import CSlug
slug = CSlug("globals.c")
Constants defined using const
are readable with the same method as global
variables but are obviously not writable. Anything defined using #define
is
preprocessor only, meaning that it gets refactored out early in the C
compilation process and doesn't exist in a shared library. To reset all globals
back to their defaults just close the library with slug.close()
.
An integer¶
We'll start with an_int
which is defined in C as:
int an_int = 42;
Variables are available as attributes of slug.dll
just
like functions are. Unfortunately, ctypes
assumes everything it finds is
a function (calling this supposed function is a seg-faultA fatal error raised when trying to read/write memory that isn't (or no longer is) owned by Python.):
>>> slug.dll.an_int
<_FuncPtr object at 0x0000003084046E10>
We need to cast this function pointer into an int pointer (because an_int
is
an int), then dereference it:
an_int = ctypes.cast(slug.dll.an_int, ctypes.POINTER(ctypes.c_int)).contents
This gives a ctypes.c_int
. Convert the to a Python int
with:
>>> an_int.value
42
And if the variable isn't declared const
then you can write to it:
an_int.value = 13
Warning
These variables are still pointers into the specific ctypes.CDLL
they originated from. Attempting to access the contents of a pointer after
it's shared library has been either closed or reloaded is an instant crash:
>>> slug.make() # or `slug.close()`
>>> print(an_int)
Process finished with exit code -1073741819 (0xC0000005)
To avoid this you may use lengthy one-liners:
>>> slug.make()
>>> ctypes.cast(slug.dll.an_int, ctypes.POINTER(ctypes.c_int)).contents.value
42
Other basic types¶
Accessing other non-pointer based types is almost identical to An integer with the only difference being the ctypes
class you provide to
ctypes.cast()
:
float a_float = 1. / 3.;
double a_double = 1. / 3.;
int8_t an_8_bit_int = 43;
See the ctypes types table to match types. There is no protection if you get it wrong.
a_float = ctypes.cast(slug.dll.a_float, ctypes.POINTER(ctypes.c_float)).contents
a_double = ctypes.cast(slug.dll.a_double, ctypes.POINTER(ctypes.c_double)).contents
an_8_bit_int = ctypes.cast(slug.dll.an_8_bit_int, ctypes.POINTER(ctypes.c_int8)).contents
>>> a_float.value, a_double.value, an_8_bit_int.value
(0.3333333432674408, 0.3333333333333333, 43)
String, bytes and arrays¶
Accessing strings and bytes is slightly different because they are already pointers.
char a_bytes_array[] = "Hello, my name is Ned.";
wchar_t a_string[] = L"Сәлам. Минем исемем Нед.";
To access them you can use:
a_bytes_array = ctypes.cast(slug.dll.a_bytes_array, ctypes.c_char_p)
a_string = ctypes.cast(slug.dll.a_string, ctypes.c_wchar_p)
But be careful with this form. It doesn't know the lengths of these strings - it
guesses, assuming they are NULL terminatedA Null terminated string has an additional zero character on the end to signify the end of the string. (which in this case they are
because, on defining them with char x[] = "literal"
, C appends a NULL
character). If a string isn't null terminated, memory trash characters will be
appended until a 0 is found and if a string contains other NULLs then it will
be terminated prematurely.
char contains_null[] = "This sentence has a \00 in the middle of it.";
int len_contains_null = sizeof(contains_null) / sizeof(char);
Because the above byte array contains a NULL character, it gets mistakenly shortened:
>>> ctypes.cast(slug.dll.contains_null, ctypes.c_char_p).value
b'This sentence has a '
A better, albeit much longer, way is to mark it as an array of a specified length. This approach will work for any array type as well as strings:
# Extract the length (just a regular int).
length = ctypes.cast(slug.dll.len_contains_null, ctypes.POINTER(ctypes.c_int)).contents.value
# Get a raw void (untyped) pointer to our string.
address = ctypes.cast(slug.dll.contains_null, ctypes.c_void_p).value
# Interpret the pointer as an array of the correct length.
contains_null = (ctypes.c_char * length).from_address(address)
This gives us a scriptable ctypes.Array
object. Not only can we see
past the NULL character in the middle but we can see that this string is also
NULL terminatedA Null terminated string has an additional zero character on the end to signify the end of the string.:
>>> contains_null
<ctypes.c_char_Array_43 object at 0x00000027896043C0>
>>> contains_null[:]
b'This sentence has a \x00 in the middle of it.\x00'
>>> contains_null.raw
b'This sentence has a \x00 in the middle of it.\x00'
Setting arrays¶
You can modify individual elements using:
contains_null[0] = b"A"
Or series of elements (note lengths must match):
contains_null[:5] = b"abcde"
Or the whole array (again the length must not change):
contains_null[:] = contains_null[:].upper()
If you intend to set array pointer to point to a different array (definitely not recommended) then read Warning for Structs Containing Pointers - the same warnings apply here.