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
- hello world example
- parameters & returns
- rust data types compared to pythons ctypes
- lists / arrays
- complex data types handling
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
from ctypes import CDLL, Structure, c_double
lib = CDLL("target/release/librust_in_python.dylib")
class Point(Structure):
_fields_ = [
('x', c_double),
('y', c_double)
]
p = Point(x=1.2, y=3.4)
lib.greet_point(p)