Merge pull request #3487 from mejrs/ffi_example

refactor pyo3-ffi example to an example project
This commit is contained in:
Bruno Kolenbrander 2023-10-09 22:53:35 +00:00 committed by GitHub
commit 300f2d63ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 275 additions and 0 deletions

View file

@ -0,0 +1,12 @@
[package]
authors = ["{{authors}}"]
name = "{{project-name}}"
version = "0.1.0"
edition = "2021"
[lib]
name = "string_sum"
crate-type = ["cdylib"]
[dependencies]
pyo3-ffi = { version = "{{PYO3_VERSION}}", features = ["extension-module"] }

View file

@ -0,0 +1,4 @@
variable::set("PYO3_VERSION", "0.19.2");
file::rename(".template/Cargo.toml", "Cargo.toml");
file::rename(".template/pyproject.toml", "pyproject.toml");
file::delete(".template");

View file

@ -0,0 +1,7 @@
[build-system]
requires = ["maturin>=1,<2"]
build-backend = "maturin"
[project]
name = "{{project-name}}"
version = "0.1.0"

View file

@ -0,0 +1,13 @@
[package]
name = "string_sum"
version = "0.1.0"
edition = "2021"
[lib]
name = "string_sum"
crate-type = ["cdylib"]
[dependencies]
pyo3-ffi = { path = "../../pyo3-ffi", features = ["extension-module"] }
[workspace]

View file

@ -0,0 +1,2 @@
include pyproject.toml Cargo.toml
recursive-include src *

View file

@ -0,0 +1,36 @@
# string_sum
A project built using only `pyo3_ffi`, without any of PyO3's safe api.
## Building and Testing
To build this package, first install `maturin`:
```shell
pip install maturin
```
To build and test use `maturin develop`:
```shell
pip install -r requirements-dev.txt
maturin develop
pytest
```
Alternatively, install nox and run the tests inside an isolated environment:
```shell
nox
```
## Copying this example
Use [`cargo-generate`](https://crates.io/crates/cargo-generate):
```bash
$ cargo install cargo-generate
$ cargo generate --git https://github.com/PyO3/pyo3 examples/string_sum
```
(`cargo generate` will take a little while to clone the PyO3 repo first; be patient when waiting for the command to run.)

View file

@ -0,0 +1,5 @@
[template]
ignore = [".nox"]
[hooks]
pre = [".template/pre-script.rhai"]

View file

@ -0,0 +1,9 @@
import nox
@nox.session
def python(session):
session.install("-rrequirements-dev.txt")
session.install("maturin")
session.run_always("maturin", "develop")
session.run("pytest")

View file

@ -0,0 +1,16 @@
[build-system]
requires = ["maturin>=1,<2"]
build-backend = "maturin"
[project]
name = "string sum"
version = "0.1.0"
classifiers = [
"License :: OSI Approved :: MIT License",
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Rust",
"Operating System :: POSIX",
"Operating System :: MacOS :: MacOS X",
]

View file

@ -0,0 +1,3 @@
pytest>=3.5.0
pip>=21.3
maturin>=0.12,<0.13

View file

@ -0,0 +1,127 @@
use std::os::raw::{c_char, c_long};
use std::ptr;
use pyo3_ffi::*;
static mut MODULE_DEF: PyModuleDef = PyModuleDef {
m_base: PyModuleDef_HEAD_INIT,
m_name: "string_sum\0".as_ptr().cast::<c_char>(),
m_doc: "A Python module written in Rust.\0"
.as_ptr()
.cast::<c_char>(),
m_size: 0,
m_methods: unsafe { METHODS as *const [PyMethodDef] as *mut PyMethodDef },
m_slots: std::ptr::null_mut(),
m_traverse: None,
m_clear: None,
m_free: None,
};
static mut METHODS: &[PyMethodDef] = &[
PyMethodDef {
ml_name: "sum_as_string\0".as_ptr().cast::<c_char>(),
ml_meth: PyMethodDefPointer {
_PyCFunctionFast: sum_as_string,
},
ml_flags: METH_FASTCALL,
ml_doc: "returns the sum of two integers as a string\0"
.as_ptr()
.cast::<c_char>(),
},
// A zeroed PyMethodDef to mark the end of the array.
PyMethodDef::zeroed(),
];
// The module initialization function, which must be named `PyInit_<your_module>`.
#[allow(non_snake_case)]
#[no_mangle]
pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject {
PyModule_Create(ptr::addr_of_mut!(MODULE_DEF))
}
/// A helper to parse function arguments
/// If we used PyO3's proc macros they'd handle all of this boilerplate for us :)
unsafe fn parse_arg_as_i32(obj: *mut PyObject, n_arg: usize) -> Option<i32> {
if PyLong_Check(obj) == 0 {
let msg = format!(
"sum_as_string expected an int for positional argument {}\0",
n_arg
);
PyErr_SetString(PyExc_TypeError, msg.as_ptr().cast::<c_char>());
return None;
}
// Let's keep the behaviour consistent on platforms where `c_long` is bigger than 32 bits.
// In particular, it is an i32 on Windows but i64 on most Linux systems
let mut overflow = 0;
let i_long: c_long = PyLong_AsLongAndOverflow(obj, &mut overflow);
if overflow != 0 {
raise_overflowerror(obj);
None
} else if let Ok(i) = i_long.try_into() {
Some(i)
} else {
raise_overflowerror(obj);
None
}
}
unsafe fn raise_overflowerror(obj: *mut PyObject) {
let obj_repr = PyObject_Str(obj);
if !obj_repr.is_null() {
let mut size = 0;
let p = PyUnicode_AsUTF8AndSize(obj_repr, &mut size);
if !p.is_null() {
let s = std::str::from_utf8_unchecked(std::slice::from_raw_parts(
p.cast::<u8>(),
size as usize,
));
let msg = format!("cannot fit {} in 32 bits\0", s);
PyErr_SetString(PyExc_OverflowError, msg.as_ptr().cast::<c_char>());
}
Py_DECREF(obj_repr);
}
}
pub unsafe extern "C" fn sum_as_string(
_self: *mut PyObject,
args: *mut *mut PyObject,
nargs: Py_ssize_t,
) -> *mut PyObject {
if nargs != 2 {
PyErr_SetString(
PyExc_TypeError,
"sum_as_string expected 2 positional arguments\0"
.as_ptr()
.cast::<c_char>(),
);
return std::ptr::null_mut();
}
let (first, second) = (*args, *args.add(1));
let first = match parse_arg_as_i32(first, 1) {
Some(x) => x,
None => return std::ptr::null_mut(),
};
let second = match parse_arg_as_i32(second, 2) {
Some(x) => x,
None => return std::ptr::null_mut(),
};
match first.checked_add(second) {
Some(sum) => {
let string = sum.to_string();
PyUnicode_FromStringAndSize(string.as_ptr().cast::<c_char>(), string.len() as isize)
}
None => {
PyErr_SetString(
PyExc_OverflowError,
"arguments too large to add\0".as_ptr().cast::<c_char>(),
);
std::ptr::null_mut()
}
}
}

View file

@ -0,0 +1,41 @@
import pytest
from string_sum import sum_as_string
def test_sum():
a, b = 12, 42
added = sum_as_string(a, b)
assert added == "54"
def test_err1():
a, b = "abc", 42
with pytest.raises(
TypeError, match="sum_as_string expected an int for positional argument 1"
) as e:
sum_as_string(a, b)
def test_err2():
a, b = 0, {}
with pytest.raises(
TypeError, match="sum_as_string expected an int for positional argument 2"
) as e:
sum_as_string(a, b)
def test_overflow1():
a, b = 0, 1 << 43
with pytest.raises(OverflowError, match="cannot fit 8796093022208 in 32 bits") as e:
sum_as_string(a, b)
def test_overflow2():
a, b = 1 << 30, 1 << 30
with pytest.raises(OverflowError, match="arguments too large to add") as e:
sum_as_string(a, b)