Rust in Python made easy
Tags: Ffi, Python, Rust

Python is truly amazing. With all that greatness generally there has to be a tradeoff and in the case of Python it’s performance.

Luckily there is an easy way to run computation intensive work in Rust, which is of course orders of magnitude faster. Let’s see how!

Overview

Photo by Jonathan Chng on Unsplash

Lets assume we want to run the following python code in rust.

def add(a, b):

return a + b



add(1, 2) # 3

Lets see the steps we need to take to achieve this:

write some rust code compile rust import rust in python run the rust code inside of python

Hello world example

First lets create a new rust project by running:

cargo new rust_in_python

Then lets rename src/main.rs to src/lib.rs as we want a library and not standalone program.

mv src/main.rs src/lib.rs

Now we simply write a hello world function in rust

#[no_mangle]

fn hello() {

println!("Hello from rust 👋");

}

For every function that need to be available to other languages (in our case Python) through foreign function call (ffi) we will need to add the #[no_mangle] flag to it.

The last step is to tell rust to compile to a dynamic library. To do so simply add the following to your Cargo.toml config file.

[lib]

crate-type = ["dylib"]

Now we are ready to build 🚀

cargo build --release

Now just create a main.py file and we can import and run our function.

from ctypes import CDLL



lib = CDLL("target/release/librust_in_python.dylib")

lib.hello()

And if you run it you will be greeted from rust. No need to install, the ctypes package is included the standard python library.

python main.py

With return & parameters

Of course without giving parameters to the function and receiving its output this whole endeavour would be useless.

Before we start I would like to remind you that python is untyped whereas rust of course is strongly typed. This means that we will need to tell python what types the parameters and the return of our rust function we have.

First lets write the simple add function in rust

#[no_mangle]

fn add(a: f64, b: f64) -> f64 {

return a + b;

}

Don’t forget to build again 😉

cargo build --release

Now to the python part

from ctypes import CDLL, c_double



lib = CDLL("target/release/librust_in_python.dylib")



lib.add.argtypes = (c_double, c_double)

lib.add.restype = c_double



result = lib.add(1.5, 2.5)

print(result) # 4.0

Lets see what is happening here:

With lib.add.argtypes we must pass a tuple specifying how python should parse the data we pass to the add function. The ctypes package includes a list of different types we can use. See the full list here.

The same happens with lib.add.restype . As you might have guessed this tells python what type is returned from the rust function.

In our case it’s all c_double as we are using f64 in rust.

Data types in rust

Lets see some other data types and their equivalent in rust.

Python C Rust c_bool — c_byte char i8 c_ubyte unsigned char u8 c_short short i16 c_ushort unsigned short u16 c_int int i32 c_uint unsigned int u32 c_long long i64 c_ulong unsigned long u64 c_float float f32 c_double double f64

Arrays & List

So what about lists? Unfortunately I have not found a way to use Vectors for dynamic size arrays. So for now it’s just fixed size arrays.

Rust

#[no_mangle]

fn sum(arr: [i32; 5]) -> i32 {

let mut total: i32 = 0;

for number in arr.iter() {

total += number;

}

return total;

}

Python

from ctypes import CDLL, c_int



lib = CDLL("target/release/librust_in_python.dylib")



lst = [1, 2, 3, 4, 5]

# Create the memory of the list size

seq = c_int * len(lst)

arr = seq(*lst)



result = lib.sum(arr)

print(result)

Classes and complex data types

Often it can be very useful to send and/or receive data in a structured, compact way. We can do this using structs.

Rust

#[repr(C)]

pub struct Point {

pub x: f64,

pub y: f64,

}



#[no_mangle]

fn greet_point(p: Point) {

println!("x: {}, y: {}", p.x, p.y);

}

Python