Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor pyo3-ffi example to an example project #3487

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/string-sum/.template/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions examples/string-sum/.template/pre-script.rhai
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions examples/string-sum/.template/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[build-system]
requires = ["maturin>=1,<2"]
build-backend = "maturin"

[project]
name = "{{project-name}}"
version = "0.1.0"
13 changes: 13 additions & 0 deletions examples/string-sum/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions examples/string-sum/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include pyproject.toml Cargo.toml
recursive-include src *
36 changes: 36 additions & 0 deletions examples/string-sum/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions examples/string-sum/cargo-generate.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[template]
ignore = [".nox"]

[hooks]
pre = [".template/pre-script.rhai"]
9 changes: 9 additions & 0 deletions examples/string-sum/noxfile.py
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions examples/string-sum/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions examples/string-sum/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pytest>=3.5.0
pip>=21.3
maturin>=0.12,<0.13
127 changes: 127 additions & 0 deletions examples/string-sum/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 41 additions & 0 deletions examples/string-sum/tests/test_.py
Original file line number Diff line number Diff line change
@@ -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)