diff --git a/examples/string-sum/.template/Cargo.toml b/examples/string-sum/.template/Cargo.toml new file mode 100644 index 00000000000..40afa2504c3 --- /dev/null +++ b/examples/string-sum/.template/Cargo.toml @@ -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"] } diff --git a/examples/string-sum/.template/pre-script.rhai b/examples/string-sum/.template/pre-script.rhai new file mode 100644 index 00000000000..d3341677b1f --- /dev/null +++ b/examples/string-sum/.template/pre-script.rhai @@ -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"); diff --git a/examples/string-sum/.template/pyproject.toml b/examples/string-sum/.template/pyproject.toml new file mode 100644 index 00000000000..537fdacc666 --- /dev/null +++ b/examples/string-sum/.template/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +name = "{{project-name}}" +version = "0.1.0" diff --git a/examples/string-sum/Cargo.toml b/examples/string-sum/Cargo.toml new file mode 100644 index 00000000000..4a48b221c60 --- /dev/null +++ b/examples/string-sum/Cargo.toml @@ -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] diff --git a/examples/string-sum/MANIFEST.in b/examples/string-sum/MANIFEST.in new file mode 100644 index 00000000000..becccf7bc9a --- /dev/null +++ b/examples/string-sum/MANIFEST.in @@ -0,0 +1,2 @@ +include pyproject.toml Cargo.toml +recursive-include src * diff --git a/examples/string-sum/README.md b/examples/string-sum/README.md new file mode 100644 index 00000000000..552d9020963 --- /dev/null +++ b/examples/string-sum/README.md @@ -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.) diff --git a/examples/string-sum/cargo-generate.toml b/examples/string-sum/cargo-generate.toml new file mode 100644 index 00000000000..d750c4de7a3 --- /dev/null +++ b/examples/string-sum/cargo-generate.toml @@ -0,0 +1,5 @@ +[template] +ignore = [".nox"] + +[hooks] +pre = [".template/pre-script.rhai"] diff --git a/examples/string-sum/noxfile.py b/examples/string-sum/noxfile.py new file mode 100644 index 00000000000..17a6b80f3f5 --- /dev/null +++ b/examples/string-sum/noxfile.py @@ -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") diff --git a/examples/string-sum/pyproject.toml b/examples/string-sum/pyproject.toml new file mode 100644 index 00000000000..088f2b47085 --- /dev/null +++ b/examples/string-sum/pyproject.toml @@ -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", +] diff --git a/examples/string-sum/requirements-dev.txt b/examples/string-sum/requirements-dev.txt new file mode 100644 index 00000000000..9ea7f14b6dd --- /dev/null +++ b/examples/string-sum/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest>=3.5.0 +pip>=21.3 +maturin>=0.12,<0.13 diff --git a/examples/string-sum/src/lib.rs b/examples/string-sum/src/lib.rs new file mode 100644 index 00000000000..91072418038 --- /dev/null +++ b/examples/string-sum/src/lib.rs @@ -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::(), + m_doc: "A Python module written in Rust.\0" + .as_ptr() + .cast::(), + 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::(), + 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::(), + }, + // A zeroed PyMethodDef to mark the end of the array. + PyMethodDef::zeroed(), +]; + +// The module initialization function, which must be named `PyInit_`. +#[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 { + 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::()); + 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::(), + size as usize, + )); + let msg = format!("cannot fit {} in 32 bits\0", s); + + PyErr_SetString(PyExc_OverflowError, msg.as_ptr().cast::()); + } + 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::(), + ); + 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::(), string.len() as isize) + } + None => { + PyErr_SetString( + PyExc_OverflowError, + "arguments too large to add\0".as_ptr().cast::(), + ); + std::ptr::null_mut() + } + } +} diff --git a/examples/string-sum/tests/test_.py b/examples/string-sum/tests/test_.py new file mode 100644 index 00000000000..d9b5ab5fd44 --- /dev/null +++ b/examples/string-sum/tests/test_.py @@ -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)