Merge pull request #3487 from mejrs/ffi_example
refactor pyo3-ffi example to an example project
This commit is contained in:
commit
300f2d63ae
12
examples/string-sum/.template/Cargo.toml
Normal file
12
examples/string-sum/.template/Cargo.toml
Normal 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"] }
|
4
examples/string-sum/.template/pre-script.rhai
Normal file
4
examples/string-sum/.template/pre-script.rhai
Normal 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");
|
7
examples/string-sum/.template/pyproject.toml
Normal file
7
examples/string-sum/.template/pyproject.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
[build-system]
|
||||
requires = ["maturin>=1,<2"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "{{project-name}}"
|
||||
version = "0.1.0"
|
13
examples/string-sum/Cargo.toml
Normal file
13
examples/string-sum/Cargo.toml
Normal 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]
|
2
examples/string-sum/MANIFEST.in
Normal file
2
examples/string-sum/MANIFEST.in
Normal file
|
@ -0,0 +1,2 @@
|
|||
include pyproject.toml Cargo.toml
|
||||
recursive-include src *
|
36
examples/string-sum/README.md
Normal file
36
examples/string-sum/README.md
Normal 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.)
|
5
examples/string-sum/cargo-generate.toml
Normal file
5
examples/string-sum/cargo-generate.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[template]
|
||||
ignore = [".nox"]
|
||||
|
||||
[hooks]
|
||||
pre = [".template/pre-script.rhai"]
|
9
examples/string-sum/noxfile.py
Normal file
9
examples/string-sum/noxfile.py
Normal 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")
|
16
examples/string-sum/pyproject.toml
Normal file
16
examples/string-sum/pyproject.toml
Normal 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",
|
||||
]
|
3
examples/string-sum/requirements-dev.txt
Normal file
3
examples/string-sum/requirements-dev.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
pytest>=3.5.0
|
||||
pip>=21.3
|
||||
maturin>=0.12,<0.13
|
127
examples/string-sum/src/lib.rs
Normal file
127
examples/string-sum/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
41
examples/string-sum/tests/test_.py
Normal file
41
examples/string-sum/tests/test_.py
Normal 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)
|
Loading…
Reference in a new issue