Let's address the first overflow, in Number's constructor:
from my_module import Number
n = Number(1 << 1337)
Traceback (most recent call last):
File "example.py", line 3, in <module>
n = Number(1 << 1337)
OverflowError: Python int too large to convert to C long
Instead of relying on the default FromPyObject extraction to parse arguments, we can specify our
own extraction function, using the #[pyo3(from_py_with = "...")] attribute. Unfortunately PyO3
doesn't provide a way to wrap Python integers out of the box, but we can do a Python call to mask it
and cast it to an i32.
#![allow(dead_code)]use pyo3::prelude::*;
fnwrap(obj: &Bound<'_, PyAny>) -> PyResult<i32> {
let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?;
let val: u32 = val.extract()?;
// 👇 This intentionally overflows!Ok(val asi32)
}
We also add documentation, via /// comments, which are visible to Python users.
#![allow(dead_code)]use pyo3::prelude::*;
fnwrap(obj: &Bound<'_, PyAny>) -> PyResult<i32> {
let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?;
let val: u32 = val.extract()?;
Ok(val asi32)
}
/// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not./// It's not a story C would tell you. It's a Rust legend.#[pyclass(module = "my_module")]structNumber(i32);
#[pymethods]impl Number {
#[new]fnnew(#[pyo3(from_py_with = "wrap")] value: i32) -> Self {
Self(value)
}
}
With that out of the way, let's implement some operators:
We do not implement the in-place operations like __iadd__ because we do not wish to mutate Number.
Similarly we're not interested in supporting operations with different types, so we do not implement
the reflected operations like __radd__ either.
Now Python can use our Number class:
from my_module import Number
defhash_djb2(s: str):'''
A version of Daniel J. Bernstein's djb2 string hashing algorithm
Like many hashing algorithms, it relies on integer wrapping.
'''
n = Number(0)
five = Number(5)
for x in s:
n = Number(ord(x)) + ((n << five) - n)
return n
assert hash_djb2('l50_50') == Number(-1152549421)
At the beginning of this chapter we said that PyO3 doesn't provide a way to wrap Python integers out
of the box but that's a half truth. There's not a PyO3 API for it, but there's a Python C API
function that does:
We can call this function from Rust by using pyo3::ffi::PyLong_AsUnsignedLongMask. This is an unsafe
function, which means we have to use an unsafe block to call it and take responsibility for upholding
the contracts of this function. Let's review those contracts:
The GIL must be held. If it's not, calling this function causes a data race.
The pointer must be valid, i.e. it must be properly aligned and point to a valid Python object.
Let's create that helper function. The signature has to be fn(&Bound<'_, PyAny>) -> PyResult<T>.
&Bound<'_, PyAny> represents a checked borrowed reference, so the pointer derived from it is valid (and not null).
Whenever we have borrowed references to Python objects in scope, it is guaranteed that the GIL is held. This reference is also where we can get a Python token to use in our call to PyErr::take.
#![allow(dead_code)]use std::os::raw::c_ulong;
use pyo3::prelude::*;
use pyo3::ffi;
fnwrap(obj: &Bound<'_, PyAny>) -> Result<i32, PyErr> {
let py: Python<'_> = obj.py();
unsafe {
let ptr = obj.as_ptr();
let ret: c_ulong = ffi::PyLong_AsUnsignedLongMask(ptr);
if ret == c_ulong::MAX {
ifletSome(err) = PyErr::take(py) {
returnErr(err);
}
}
Ok(ret asi32)
}
}