Structs – Almost Object-Oriented Programming

Nothing makes you miss object-oriented programming more than being stuck without it. Classes are C++ only (which cslug doesn't support) but you can get pretty close using Structures.

cslug does relatively little to help here. All it does is parse struct definitions from your source code and turn them into classes based on the ctypes.Structure template class.

Throughout this page snippets of an example C file will be included. You may access the whole file with the link below:

Defining a Struct

We'll start by writing a simple C file containing a struct definition:

cards.c
#include <stdint.h> // Needed for unit8_t.

typedef struct Card {
  uint8_t face;
  uint8_t suit;
} Card;

Note that the simpler struct Person {…}; form of a struct definition is intentionally not supported. You must use the above typedef form.

Note

If you define a struct in a header file, you should add that file to your cslug.CSlug sources.

Using a Struct in Python

Let's compile the above code and play with it.

from cslug import CSlug
slug = CSlug("cards.c")

Structs are available as attributes in exactly the same way as functions. The structure class forms a neat bucket class in Python:

>>> slug.dll.Card(6, 2)
Card(face=6, suit=2)

With sensible (albeit non-configurable) defaults:

>>> slug.dll.Card(suit=1)
Card(face=0, suit=1)

And writeable, type-checked attributes corresponding to struct fields:

>>> slug.dll.Card(4).face
4

Passing a Struct from Python to C

First let's extend cards.c to define some uninspiring functions which take our struct as an argument. (Remember to call slug.make() after modifying the C code.) We can either pass a structure by value or with a pointer. The following adds a function for each case.

uint8_t get_card_face(Card card) {
  return card.face;
}

uint8_t get_card_ptr_face(Card * card) {
  return card -> face;
}

To pass by value you can just pass the struct as-is to a C function:

>>> slug.dll.get_card_face(slug.dll.Card(face=6))
6

To pass by pointer you need its memory address which is kept for convenience in a _ptr attribute. Its value is just the output of ctypes.addressof(card).

>>> card = slug.dll.Card(face=7)
>>> slug.dll.get_card_ptr_face(card._ptr)
7

Warning

Using slug.dll.Card(face=6)._ptr causes the card itself to be immediately deleted, leaving a dangling pointerA pointer whose target object has been deleted leaving it pointing to trash memory.. You must retain a reference to the original object until after you no longer need the pointer.

Passing a Struct from C to Python

By Value

Returning a struct by value from a C function is straight forward:

Card make_card(uint8_t face, uint8_t suit) {
  Card card;
  card.face = face;
  card.suit = suit;
  return card;
}

By Pointer

Returning a struct by pointer is not straight forward. In fact it's highly discouraged and considered bad C. If you're feeling brave, curious or foolhardy enough to dismiss this then read on:

The following naive approach creates a Card and returns its address. But the card is deleted at the end of the function leaving another dangling pointerA pointer whose target object has been deleted leaving it pointing to trash memory. (your compiler should detect and warn you about this):

Card * make_card_ptr(uint8_t face, uint8_t suit) {
  Card card;
  card.face = face;
  card.suit = suit;
  return &card;
}

We can improve the situation by using malloc(), to reserve memory beyond the scope of this function call:

Card * make_card_ptr_safer(uint8_t face, uint8_t suit) {
  Card * card =  malloc(sizeof(Card));
  if (!card) return NULL; // Return None if no memory available.
  card -> face = face;
  card -> suit = suit;
  return card;
}

This has two problems. First cslug doesn't implicitly dereference pointers:

>>> slug.dll.make_card_ptr_safer(1, 2)
90846685936

But this is easily solved:

>>> slug.dll.Card.from_address(slug.dll.make_card_ptr_safer(1, 2))
Card(face=1, suit=2)

Secondly, this is a memory leakMemory allocated but never freed. because we allocate structs but never free them again:

# Watch Python's memory usage go up and up...
while True:
    slug.dll.make_card_ptr_safer(1, 2)

To deallocate, use free() to get the memory back once we no longer need the struct:

void delete_card(Card * card) {
  free(card);
}
# This doesn't leak memory.
while True:
    card = slug.dll.Card.from_address(slug.dll.make_card_ptr_safer(1, 2))
    # Do something with the card.
    # Then delete it safely:
    slug.dll.delete_card(card._ptr)

As delete_card() is just an alias for free() there's no need to create a function for it. Instead use the free function directly:

from cslug.stdlib import free

while True:
    card = slug.dll.Card.from_address(slug.dll.make_card_ptr_safer(1, 2))
    # Do something with the card.
    # Then delete it safely:
    free(card._ptr)

Warning for Structs Containing Pointers

Be vary careful for dangling pointersA pointer whose target object has been deleted leaving it pointing to trash memory. if you're struct contains pointers. This harmless looking structure, containing a string pointer, is deceptively dangerous:

person.c
#include <stddef.h>

typedef struct Person {
    wchar_t * name;
} Person;

Person make_person(wchar_t * name) {
    Person person;
    person.name = name;
    return person;
}
from cslug import CSlug, anchor

slug = CSlug(anchor("person.c"))

slug.make()

Creating a Person in Python is Ok:

>>> slug.dll.Person("Bill")
Person(name='Bill')

But creating a Person in a C function isn't.

>>> slug.dll.make_person("Bill")
Person(name='璀L$')

What's happened here is that the string buffer we passed a pointer to is deleted as soon as make_person() exits, leaving the name attribute a dangling pointer. If you're calling make_person() from Python like above you can get around this by maintaining a reference to the buffer in Python:

>>> import ctypes
>>> name = ctypes.create_unicode_buffer("Bill")
>>> slug.dll.make_person(name)
Person(name='Bill')

But bear in mind that this reference is mutable:

>>> person = slug.dll.make_person(name)
>>> person
Person(name='Bill')
>>> name.value = "Bob"  # Must be no longer than 'Bill' (<= 4 characters)
>>> person
Person(name='Bob')

And easily deletable:

>>> del name
>>> person
Person(name='㟐]$')

This is more subtle when you remember that local variables are deleted at the end of functions in Python:

def make_person(name):
    name = ctypes.create_unicode_buffer(name)
    return slug.dll.make_person(name)  # `name` is automatically deleted here.