How to use Rust in Python (Part 3)

You can follow the links to read the first part and the second part of this series.

In the previous part we have seen how to pass not trivial data to Rust functions such as a Python list. It is still not enough, though. In many cases we need to pass complex data structure back and forth from a Rust library. We may need to pass quaternions, 3D points, trees, a list of “books”… In short: anything.

Learning how to pass custom aggregated data types to Rust libraries (and back to Python) will be the focus of this part!

The Rust Side: Working with Points

As usual, the first thing to do is to implement the Rust library function. This time we need to design first a struct that will represent our complex object. We chose to represent 2D points.

pub struct Point {
    x: f64,
    y: f64,

This is a classic rust struct declaration, but we need to use #[repr(C)] to explain to the compiler that Point is a C struct.

Now, we can simply implement the desired function. In this example we will implement a function that, given two Point  representing a segment,returns the midpoint of the segment.

pub extern fn middle(p1: Point, p2: Point) -> Point {
    Point { x: (p1.x + p2.x)/2.0, y: (p1.y + p2.y)/2.0 }

The Python Side

The Rust side of the problem is quite easy. As you have seen, it is just Rust programming, but with a #[repr(C)] spice on top of the struct declaration.

Python side is slightly more complicated. Also in Python, we need first to declare the Point.

class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)]
    def __str__(self):
        return "Point ({},{})".format(self.x, self.y)

The class representing our C/Rust struct is different from the usual one.

  1. The class must extend the ctypes.Structure class defined in ctypes.
  2. The fields of the C struct must be defined using the builtin _fields_ attribute. This attribute must contain a **list of tuples. **Each tuple must contain the 1) name of the field and 2) the type of the field according the ctypes declaration. In our case Point has two fields, “x” and “y”, both doubles. Now it is time to setup our Rust function. We specify the type declaration for our function.
# Then we specify as usual the type declaration of the Rust function.
lib.middle.argtypes = (Point, Point)
lib.middle.restype = Point

Then we can use this function as usual!

# And then we can easily use it as a native python function!
p1 = Point(1.0, 5.0)
p2 = Point(1.0, 10.0)

res_point = lib.middle(p1, p2)

And we have done. It is not so difficult, isn’t it? Now, mixing up this lesson with the previous one you can easily setup an external Rust function to compute the centroid of a list of Point. I will leave you this as an exercise (HINT: you may need some POINTER magic).

What’s next?

For now, I think I’ve covered the important topics for this task. You should now be able to combine all this information in order to provide Python interfaces to a huge family of Rust functions (maybe every function you will ever need). If you have any particular use case I’ve not covered or you are interested in, ask me anything! I will be happy to add some other part to this series in order fill the gaps!

Finally, I remind you that all the code of this series is available on GitHub.

Have a nice day!

Related Articles