I don’t claim to be a particularly good Python programmer at all. I’m probably one of the… less useful people to program Python - I much prefer Go (and to be rather honest, C).

But I think I’ve encountered the nastiest piece of library I’ve used with Python - ctypes.

What is ctypes?

It’s kind of like the psuedo C package for Go - it allows direct access to C functions and structures and all those nice things, and lets you use C libraries in your Python program.

I thought, “Well, this sounds nice! Let’s write Python bindings for libdpx! It can’t be too hard!” I was wrong.

Struggling along

I’ve learned two major annoyances:

Calling a function is hard.

The documentation claims that that this is possible:

from ctypes import *
import atexit

dpx = CDLL('libdpx.so')

def cleanup():
    dpx.dpx_cleanup()

dpx.dpx_init()
atexit.Register(cleanup)

peer = dpx.dpx_peer_new()

# do stuff with peer here

And it’d be all nice and dandy, right? NOPE. Nowhere in the documentation does ctypes seem to know what to do when you get a pointer back unless you explicitly say it. It does say that return values would “assume to be int”. So technically doing the following:

peer = c_void_p(dpx.dpx_peer_new())

should work, right? Because c_void_p takes an integer argument, which comes out of the function thanks to ctypes. And that integer argument is probably a pointer, given the C return value.

But nooo. segfaults everywhere. I had to do this:

dpn = dpx.dpx_peer_new
dpn.argtypes = []
dpn.restype = c_void_p

# now call it
peer = dpn()

For every single function.

I apparently cannot work with pointers nicely with ctypes.

Because, well.. Python seems to suck at pointer support. It’s understandable, so I’m not sure what I was expecting, but it’s still annoying when I do the following:

dfn = dpx.dpx_frame_new
dfn.argtypes = [c_void_p]
dfn.restype = POINTER(CFRAME)
# POINTER(CFRAME) means the type is a pointer to a CFRAME
# CFRAME was a class derived from "Structure" that mimics dpx_frame in dpx.h

new_frame = dfn(None)

new_frame.payloadSize = 10
new_frame.payload = payload

channel.send(new_frame)
# Sending frame: 0 bytes...

Only to realise that payloadSize that channel gets is size 0. Why?

It’s because the object new_frame is a pointer object. It’s like any other Python object, so I was actually setting the attributes ‘payloadSize’ and ‘payload’ on the object itself.

So then, how to get to the actual object?

You can’t do new_frame.contents like the docs say, because the docs also say that Python will make a copy of the object. So that’s useless.

Insted, I have to treat it like an array:

actual_new_frame = new_frame[0]
actual_new_frame.payloadSize = 10
actual_new_frame.payload = payload

channel.send(new_frame)
# Sending frame: 10 bytes...

So those problems are solved, at least.

So you’re happy now?

Nope. See that cover image? Crashes everywhere.

The same code that worked for the most core test case is now failing for the next because ctypes is passing invalid pointers (or I am).

I think I’m going to try my hand at making a Python extension instead. At least I can code that in C…

EDIT: I’ve legitimately failed. It’s segfaulting because I’m cleaning up structures prematurely. So therefore pointers that used to point to valid stuff no longer do.

I think I need to take shots.

Where is your code so I can laugh at it

By all means, please do! #ctypestruggles

It’s living in a branch of duplex right now. Feel free to check it out.