-
Notifications
You must be signed in to change notification settings - Fork 4
Rust in Python
RustyPy requires Python 3.5 at least and works with the latest stable version of Rust. You can check the type conversions between Python and Rust at type conversions details. Rust crate documentation.
Any function which has the python_bind_ prefix in the target Rust library will be parsed in Python and bindings will be generated for them. The communication between Rust and Python is done through C (thanks to Rust FFI), hence there is no knowledge at Python level about 'modules' ore more complex features (like traits).
As of now you can pass to Rust from Python most primitives, lists, dictionaries and tuples. The supported container types can contain inner containers.
On Python you generate the bindings through the 'bind_rs_crate_funcs' function which accepts two arguments: two valid path strings (as generated with the os.path module) which would be the entry point to the library crate (where Cargo.toml is located, which is required!), and the path the compiled library. Example:
from rustypy.rswrapper import bind_rs_crate_funcs
source_path = "/home/user/workspace/rs_test_lib"
compiled_lib = "/home/user/workspace/rs_test_lib/target/debug/libtest_lib.so"
# default prefix is "python_bind_"
optional = ["my_bind_prefix_", "other_prefix_"]
lib_binds = bind_rs_crate_funcs(source_path, compiled_lib, prefixes=optional)
This call will dynamically generate any necessary wrappings around the library extern functions.
Following the example, in Rust we could have the following functions in our library (is recommendable that the user gets familiar with Rust FFI):
#[macro_use]
extern crate rustypy;
use rustypy::{PyTuple, PyArg, PyString, PyBool};
#[no_mangle]
pub extern "C" fn python_bind_tuple(e1: i32, e2: i32) -> *mut PyTuple {
pytuple!(PyArg::I64(e1 as i64), PyArg::I64(e2 as i64))
}
#[no_mangle]
pub extern "C" fn python_bind_ref_int(num: &mut u32) {
*num += 1;
}
Then in Python we could call those functions the following way:
result = lib_binds.python_bind_tuple(10, 10)
assert result == (10, 10)
result, refs = lib_binds.python_bind_ref_int(1, return_ref=True)
assert refs[0] == 2
assert not result
All the type conversions in Python are automatically handled by rustypy.
You will notice that the function python_bind_ref_int
does not return any type, as you would expect from the declaration in Rust, but you can work with references (this is handful in some cases, like if you want more control over a complex data structure or pass it to different consumers in Rust). All functions accept an optional keyword argument (return_ref), in case is set to True the function call will dereference the data in the memory address initially passed to the function (in this case corresponding to the first argument of the function call).
Realize that this will NOT change the value of a variable as this is not the way python works (which is pass by assignment not pass by reference). For example:
x = 1
result, refs = lib_binds.python_bind_ref_int(x, return_ref=True)
assert x == 2
will fail, as the variable x still is 1 even if the value in the memory address that is passed to Rust changes. This is because x still is pointing somewhere else (and what is passed to Rust is 'assigned' to some other variable). In a nutshell, you can't get around copying in Python, or not this way.
In the case of complex return types which can contain other types inside (like PyTuple) you MUST add a return type in python so the return data can be inferred. You do this by setting the restype attribute of your function. For example, in Rust we declare:
#[no_mangle]
pub extern "C" fn python_bind_tuple_mixed(e1: i32,
e2: *mut PyBool,
e3: f32,
e4: *mut PyString)
-> *mut PyTuple {
assert_eq!(unsafe { PyBool::from_ptr(e2) }, true);
let s = PyString::from(unsafe { PyString::from_ptr_into_string(e4) });
pytuple!(PyArg::I64(e1 as i64),
PyArg::PyBool(PyBool::from(false)),
PyArg::F32(e3),
PyArg::PyString(s))
}
And then we call the function from Python:
from rustypy.rswrapper import Tuple
T = Tuple[int, bool, Float, str]
lib_binds.python_bind_tuple_mixed.restype = T
return_val = self.bindings.python_bind_tuple_mixed(1, True, 2.5, "Some from Rust")
assert return_val == (1, False, 2.5, "Some from Rust")
restype attribute must be compliant with the typing module or will fail to parse.
Because Python is not statically typed (or not by default) is ultimately the responsibility of the user to enforce type safety across the FFI boundary, just as it happens with normal Python usage. For example if you define that a function takes a string but pass an int undefined behavior or memory corruptions will happen, best case scenario is that the process will abort (depending on how and where the problem happens).
A good practice is to wrap your functions and avoid any sort of duck typing when calling those functions. While expensive in some cases checking the types of what is being passed can be appropriate sometimes.