-
Notifications
You must be signed in to change notification settings - Fork 783
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3487 from mejrs/ffi_example
refactor pyo3-ffi example to an example project
- Loading branch information
Showing
12 changed files
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
include pyproject.toml Cargo.toml | ||
recursive-include src * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[template] | ||
ignore = [".nox"] | ||
|
||
[hooks] | ||
pre = [".template/pre-script.rhai"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |