Calling into Rust from python

Here is how I've approached a similar situation: calling into Rust from Python for faster array-based computation. (Disclaimer -- there definitely may be some issues with this approach! But it's worked for me so far. I put this together from a number of helpful posts online. Any corrections or suggestions are welcome).

For my use case, I do a lot of I/O and pre-processing in Python and using NumPy. I then drop down into Rust for heavy lifting. I create the arrays in Python as NumPy arrays then pass these into Rust through pointers. Rust then performs in-place mutation.

In lib.rs:

#![crate_type = "dylib"]

use std::mem;

...

#[derive(Debug, Clone, Copy)]
pub struct Args {
    arg1: usize,
    arg2: f64
}

pub unsafe fn construct_array_1d<T>(input: *mut T, num_elems: usize) -> Vec<T> {
    // Create a Rust `Vec` associated with a `numpy` array via the raw pointer. This is unsafe in Rust parlance
    // because Rust can't verify that the pointer actually points to legitimate memory, so to speak.
    // At least check that the pointer is not null.
    assert!(!input.is_null());
    Vec::from_raw_parts(input, num_elems, num_elems)
}

pub fn do_computation(args: Args, v1: &Vec<f64>, v2: &Vec<f64>, mut v3: &mut Vec<f64>) {
    // Do in-place heavy lifting here.
    ...
}

#[no_mangle]
pub extern fn rust_entry_fn(arg1: usize, arg2: f64, v1_ptr: *mut f64,
    v1_num_elems: usize, v2_ptr: *mut f64, v2_num_elems: usize,
    v3_ptr: *mut f64, v3_num_elems: usize) {
    
    ...
    
    // Build ergonomic struct from input parameters.
    let args = Args { arg1: arg1, arg2 : arg2 };
    
    // Build immutable input vectors from raw parts.
    let v1 = unsafe { construct_array_1d(v1_ptr, v1_num_elems) };
    let v2 = unsafe { construct_array_1d(v2_ptr, v2_num_elems) };
    
    // Build mutable output vector from raw parts -- will mutate inplace in Rust.
    let mut v3 = unsafe { construct_array_1d(v3_ptr, v3_num_elems) };
    
    // Call into Rust function that performs computations and performs in-state
    // mutation of the output array `v3`.
    do_computation(args, &v1, &v2, &mut v3);
    
    // Don't let Rust free these arrays after leaving this function... This is
    // an FFI call from Python and the arrays are actually "owned" and
    // garbage collected by NumPy/Python.
    mem::forget(v1);
    mem::forget(v2);
    mem::forget(v3);
}

Then on the Python side, in main.py:

import numpy as np
from cffi import FFI

## Helper functions for Python-Rust FFI through `cffi`.
def _as_f64(num):
    """ Cast np.float64 for Rust."""
    return ffi.cast("double", num)

def _as_f64_array(array):
    """ Cast np.float64 array to a pointer to float 64s."""
    return ffi.cast("double*", num)

def _as_usize(num):
    """ Cast `num` to Rust `usize`."""
    return ffi.cast("unsigned long", num)

## Instantiate `cffi` object.
ffi = FFI()
ffi.cdef(
    """
    void* rust_entry_fn(unsigned long arg1, double arg2, double* v1_ptr, unsigned long v1_num_elems,
        double* v2_ptr, unsigned long v2_num_elems, double* v3_ptr, unsigned long v3_num_elems);
    """
)

## Go get the Rust library.
lib = ffi.dlopen("../target/release/mylib.dylib")

arg1 = _as_usize(calc_arg1(...))
arg2 = _as_f64(calc_arg2(...))
v1 = calc_input_array_v1(...)
v1_ptr = _as_f64_array(v1)
v1_num_elems = _as_usize(v1.size)
v2 = calc_input_array_v2(...)
v2_ptr = _as_f64_array(v2)
v2_num_elems = _as_usize(v2.size)
v3 = np.zeros_like(v1)
v3_ptr = _as_f64_array(v3)
v3_num_elems = _as_usize(v3.size)

lib.rust_entry_fn(arg1, arg2, v1_ptr, v1_num_elems, v2_ptr, v2_num_elems, v3_ptr, v3_num_elems)

... ## Do stuff with `v3`.
5 Likes