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:

globals.c
#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.