Writing Cython extension: how to access a C struct internal data from Python?

3k Views Asked by At

Disclaimer: I took the following example from the Python Cookbook (O'Reilly).

Let's say I have the following simple struct:

typedef struct {
  double x,y;
} Point;

with a function that calculates the Euclidean distance between two Points:

extern double distance(Point* p1, Point* p2);

All this is part of a shared library called points:

  • points.h - the header file
  • points.c - the source file
  • libpoints.so - the library file (the Cython extension links against it)

I have created my wrapping Python script (called pypoints.py):

#include "Python.h"
#include "points.h"


// Destructor for a Point instance
static void del_Point(PyObject* obj) {
  // ...
}

// Constructor for a Point instance
static void py_Point(PyObject* obj) {
  // ...
}

// Wrapper for the distance function
static PyObject* py_distance(PyObject* self, PyObject* arg) {
  // ...
}

// Method table
static PyMethodDef PointsMethods[] = {
  {"Point", py_Point, METH_VARARGS, "Constructor for a Point"},
  {"distance", py_distance, METH_VARARGS, "Calculate Euclidean distance between two Points"}
}

// Module description
static struct PyModuleDef pointsmodule = {
  PyModuleDef_HEAD_INIT,
  "points", // Name of the module; use "import points" to use
  "A module for working with points", // Doc string for the module
  -1,
  PointsMethods // Methods provided by the module
}

Note that this is just an example. For the struct and function above I can easily use ctypes or cffi but I want to learn how to write Cython extensions. The setup.py is not required here so no need to post it.

Now as you can see the constructor above allows us to do

import points

p1 = points.Point(1, 2)   # Calls py_Point(...)
p2 = points.Point(-3, 7)  # Calls py_Point(...)

dist = points.distance(p1, p2)

It works great. However what if I want to actually access the internals of the Point structure? For example how would I do

print("p1(x: " + str(p1.x) + ", y: " + str(p1.y))

As you know a struct internals can be directly accessed (if we use C++ terminology we can say that all struct members are public) so in a C code we can easily do

Point p1 = {.x = 1., .y = 2.};
printf("p1(x: %f, y: %f)", p1.x, p1.y)

In Python class members (self.x, self.y) can also be accessed without any getters and setters.

I can write functions which act as an intermediate step:

double x(Point* p);
double y(Point* p);

however I am unsure how to wrap these and how to describe their call inside the table of methods.

How can I do that? I want to have a simple p1.x for getting the x of my Point structure in Python.

1

There are 1 best solutions below

2
On BEST ANSWER

I was initially a little confused about this question since it seemed to have no Cython content (sorry the editing mess resulting from that confusion).

The Python cookbook uses Cython in a very odd way which I wouldn't recommend following. For some reason it wants to use PyCapsules which I have never seen used before in Cython.

# tell Cython about what's in "points.h"
# (this does match the cookbook version)

cdef extern from "points.h"
    ctypedef struct Point:
        double x
        double y

    double distance(Point *, Point *)

# Then we describe a class that has a Point member "pt"
cdef class Py_Point:
    cdef Point pt

    def __init__(self,x,y):
        self.pt.x = x
        self.pt.y = y

    # define properties in the normal Python way
    @property
    def x(self):
        return self.pt.x

    @x.setter
    def x(self,val):
        self.pt.x = val

    @property
    def y(self):
        return self.pt.y

    @y.setter
    def y(self,val):
        self.pt.y = val

def py_distance(Py_Point a, Py_Point b):
    return distance(&a.pt,&b.pt) # get addresses of the Point members

You can then compile it and use it from Python as

from whatever_your_module_is_called import *

# create a couple of points
pt1 = Py_Point(1.3,4.5)
pt2 = Py_Point(1.5,0)

print(pt1.x, pt1.y) # access the members

print(py_distance(pt1,pt2)) # distance between the two

In fairness to the Python Cookbook it then gives a second example that does something very similar to what I've done (but using a slightly older property syntax from when Cython didn't support the Python-like approach). So if you'd have read a bit further you wouldn't have needed this question. But avoid mixing Cython and pycapsules - it isn't a sensible solution and I don't know why they recommended it.