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.