Calling Rust from Python

To call a Rust function from Python, we’ll use the same method as calling C from Python: ctypes. For this we need to get the Rust compiler to make a shared object file from our Rust library, which we’ll link to in Python.

Alright, enough talk, here’s the code:

// we want a .so file
#![crate_type = "dylib"]

// no_mangle lets us find the name in the symbol table
// extern makes the function externally visible
#[no_mangle]
pub extern fn square(x: i32) -> i32 {
    x * x
}

Save that as rmath.rs, and compile it:

rustc rmath.rs

The rmath.rs file will be compiled to librmath.so. This is the file we’ll open with ctypes.CDLL just as we do with regular C libraries.

Here’s some example Python code for doing this:

#!/usr/bin/env python3
import ctypes

librmath = ctypes.CDLL("librmath.so")
print("5**2 = {}".format(librmath.square(5)))

Now, if you try to run that you’ll get an OSError saying it couldn’t find librmath.so. To solve this, tell Python where to look using the LD_LIBRARY_PATH variable:

LD_LIBRARY_PATH=. ./example.py

Structures

Now that we’ve got some basics down, we can look at passing structs between Python and Rust. Since the Rust code compiles to a shared object file, this should be familiar to anyone who’s written similar code in C.

For this example, we’ll have a new library we’ll call libstex.so, so save the following to stex.rs and compile it with rustc stex.rs.

#![crate_type = "dylib"]

#[repr(C)]
pub struct Point {
    x: f64,
    y: f64,
}

#[no_mangle]
pub extern fn move_point(p: Point, x_diff: f64, y_diff: f64) -> Point {
    Point { x: p.x+x_diff, y: p.y+y_diff }
}

#[no_mangle]
pub extern fn move_point_inplace(p: &mut Point, x_diff: f64, y_diff: f64) -> () {
    p.x += x_diff;
    p.y += y_diff;
}

As you can tell, in this example we’ve got two similar functions; one returns a new copy of the Point structure and the second modifies the structure in place.

Our Python code for this will be a bit more complicated because we’ll need to define the Point structure in Python and have to deal with the return types of the functions.

#!/usr/bin/env python3

from ctypes import *

libstex = CDLL("libstex.so")

class Point(Structure):
    _fields_ = [("x", c_double), ("y", c_double)]

    def __str__(self):
        return "Point(%f,%f)" % (self.x, self.y)


# This is so ctypes can do type-checking. The argtypes isn't absolutely
# necessary, but the restype is because the default is int, and we want to get
# back a Point object instead.
libstex.move_point.argtypes = (Point, c_double, c_double)
libstex.move_point.restype = Point

libstex.move_point_inplace.argtypes = (POINTER(Point), c_double, c_double)
libstex.move_point_inplace.restype = None

p = Point(5.0, 1.0)
p2 = libstex.move_point(p, 1.0, 3.0)

print(p2)

# byref returns a pointer to the object
libstex.move_point_inplace(byref(p2), -1.0, -3.0)

print(p2)

Note: You can omit the argtypes of the functions, but you still need to inform ctypes of what types the arguments have. To do this, you’ll have to wrap the arguments in c_double(). e.g. libstex.move_point(p, c_double(1.0), c_double(3.0))

Now, you can run the code just as before:

$ LD_LIBRARY_PATH=. ./example.py
Point(6.000000,4.000000)
Point(5.000000,1.000000)

Vectors

Vectors are a bit more work to access. We’ll be writing our own Slice struct containing just a pointer and the length; we could probably access the vector struct directly, but the layout in memory is not guaranteed, so it’s safer to create our own.

As before, here’s the Rust code:

#![crate_type = "dylib"]
use std::mem;

#[repr(C)]
pub struct Slice {
    ptr: *mut i32,
    len: usize,
}

fn vec_return() -> Vec<i32> {
    vec![11, 13, 17, 19, 23, 29]
}

#[no_mangle]
pub extern fn wrapper() -> Slice {
    let mut v = vec_return();
    let p = v.as_mut_ptr();
    let len = v.len();

    unsafe {
        // so that no destructor is run on our vector
        mem::forget(v);
    }

    Slice { ptr: p, len: len }
}

Note: You should change the len to be uint64 instead of usize. I didn’t do it here because usize doesn’t implement to_u64() on my version of Rust.

In Python we’ll define the Slice just as we did in the previous section:

#!/usr/bin/env python3
from ctypes import *

libstex = CDLL("libstex.so")

class Slice(Structure):
    _fields_ = [("ptr", POINTER(c_int32)), ("len", c_uint64)]

libstex.wrapper.restype = Slice

v = libstex.wrapper()

data = [v.ptr[i] for i in range(v.len)]
print(data)

Now we can run it as before:

$ LD_LIBRARY_PATH=. ./example.py
[11, 13, 17, 19, 23, 29]

And that’s all there is to it! You can now replace C with Rust for writing your performance critical code.