Skip to content

Commit

Permalink
Merge pull request #3487 from mejrs/ffi_example
Browse files Browse the repository at this point in the history
refactor pyo3-ffi example to an example project
  • Loading branch information
mejrs committed Oct 9, 2023
2 parents 36b4a79 + de27e5e commit 300f2d6
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 0 deletions.
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)

0 comments on commit 300f2d6

Please sign in to comment.