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:
#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:
#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.