Safety of passing integers between Python and Rust

119 Views Asked by At

Is This Safe?

Suppose I wanted to call some Rust code from within Python. Suppose my lib.rs looks something like this:

#[no_mangle]
pub extern fn add(left: i32, right: i32) -> i32 {
    return left+right;
}

And supposed I called this code from Python using ctypes like this:

import ctypes

def rust_add(left, right):
    rust_lib = ctypes.CDLL("path/to/the/so/file")
    return rust_lib.add(left, right)

Then is the above safe?

An Anticipated Snag: Integer Overflow

Overflow in the Inputs

One issue which even I - with my extremely tenuous grasp of Rust - can foresee is integer overflow. I imagine that, if either left or right was greater than 2^32, that would cause bad things to happen: either Rust would hit a run-time error (most likely), or it would just return silly answers (worst case). Would some type-checking in the Python, perhaps using NumPy's uint32, be sufficient to prevent this issue?

Overflow in the Workings

This one is more insidious: suppose I ask, from Python, my Rust function to add (2^32)-1 and (2^32)-2. There shouldn't be any immediate overflow, but there will be once we add the two numbers together. As I understand it, on hitting such an overflow, Rust will panic in debug mode but try its best to soldier on in release mode.

Summary

  • Is there any well-known practice for smoothing out the difficulties that arise when passing integers between a dynamic-width language, such as Python, and a fixed-width one, such as Rust, other than thorough testing?
  • Are there any issues with passing integers between Rust and Python apart from integer overflow?
1

There are 1 best solutions below

5
ChrisB On BEST ANSWER

By default, ctypes will convert integer parameters and function return values to it's c_int type, whose size is dependant on the platform. Values exceeding this size will be truncated. If c_int happens to not be 32 bits, that would be lead to undefined behavior in the Rust sense (you end up calling the rust function with an incorrect calling convention). That is probably not what you would want in this case. (Admittedly, c_int is 32 bits on most common desktop plattforms).

I still recommend the following python code:

_rust_lib = ctypes.CDLL("path/to/the/so/file")
_rust_add = _rust_lib.add
_rust_add.argtypes = [ctypes.c_int32, ctypes.c_int32]
_rust_add.restype = ctypes.c_int32

def rust_add(left, right):
    return _rust_add(left, right)

This way, ctypes will convert arguments and return types to the correct integer type regardless of the plattform.

It will still truncate larger integers and throw an exception for other python types though. If you want to have a different way of handling those cases you would have to do that yourself. For more details on how type conversions are handled by ctypes I recommend their excellent documentation.


If you just want to make sure that the passed in values are sane, and get some kind of exception in all other cases, you could write a simple function like this:

def sanitize_i32(v):
    # alternatively throw some exception
    assert isinstance(v, int)
    assert v >= -2**31 and v < 2**31
    return v

def rust_add(left, right):
    return _rust_add(sanitize_i32(left), sanitize_i32(right))

Regarding overflows on the Rust side: those will follow Rust's usual integer overflow rules (panic in debug, two's complement wrap in release). If that's not what you want, code some other logic.