refactor pyo3-ffi example to an example project
This commit is contained in:
parent
5b5eb1983c
commit
de27e5e41f
|
@ -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"] }
|
|
@ -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");
|
|
@ -0,0 +1,7 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["maturin>=1,<2"]
|
||||||
|
build-backend = "maturin"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "{{project-name}}"
|
||||||
|
version = "0.1.0"
|
|
@ -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]
|
|
@ -0,0 +1,2 @@
|
||||||
|
include pyproject.toml Cargo.toml
|
||||||
|
recursive-include src *
|
|
@ -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.)
|
|
@ -0,0 +1,5 @@
|
||||||
|
[template]
|
||||||
|
ignore = [".nox"]
|
||||||
|
|
||||||
|
[hooks]
|
||||||
|
pre = [".template/pre-script.rhai"]
|
|
@ -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")
|
|
@ -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",
|
||||||
|
]
|
|
@ -0,0 +1,3 @@
|
||||||
|
pytest>=3.5.0
|
||||||
|
pip>=21.3
|
||||||
|
maturin>=0.12,<0.13
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
Loading…
Reference in New Issue