Skip to content

Commit

Permalink
Create subinterpreter example
Browse files Browse the repository at this point in the history
  • Loading branch information
mejrs committed Oct 12, 2023
1 parent 642b335 commit 4892633
Show file tree
Hide file tree
Showing 16 changed files with 514 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Below is a brief description of each of these:
| `setuptools-rust-starter` | A template project which is configured to use [`setuptools_rust`](https://github.com/PyO3/setuptools-rust/) for development. |
| `word-count` | A quick performance comparison between word counter implementations written in each of Rust and Python. |
| `plugin` | Illustrates how to use Python as a scripting language within a Rust application |
| `sequential` | Illustrates how to use pyo3-ffi to write subinterpreter-safe modules |

## Creating new projects from these examples

Expand Down
12 changes: 12 additions & 0 deletions examples/sequential/.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 = "sequential"
crate-type = ["cdylib", "lib"]

[dependencies]
pyo3-ffi = { version = "{{PYO3_VERSION}}", features = ["extension-module"] }
4 changes: 4 additions & 0 deletions examples/sequential/.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/sequential/.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/sequential/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "sequential"
version = "0.1.0"
edition = "2021"

[lib]
name = "sequential"
crate-type = ["cdylib", "lib"]

[dependencies]
pyo3-ffi = { path = "../../pyo3-ffi", features = ["extension-module"] }

[workspace]
2 changes: 2 additions & 0 deletions examples/sequential/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/sequential/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# sequential

A project built using only `pyo3_ffi`, without any of PyO3's safe api. It can be executed by subinterpreters that have their own GIL.

## 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/sequential
```

(`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/sequential/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/sequential/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/sequential/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/sequential/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
133 changes: 133 additions & 0 deletions examples/sequential/src/id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use core::ffi::{c_char, c_int, c_ulonglong, c_void};
use core::sync::atomic::{AtomicU64, Ordering};
use core::{mem, ptr};

use pyo3_ffi::*;

#[repr(C)]
pub struct PyId {
_ob_base: PyObject,
id: Id,
}

static COUNT: AtomicU64 = AtomicU64::new(0);

#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub struct Id(u64);

impl Id {
fn new() -> Self {
Id(COUNT.fetch_add(1, Ordering::Relaxed))
}
}

unsafe extern "C" fn id_new(
subtype: *mut PyTypeObject,
args: *mut PyObject,
kwds: *mut PyObject,
) -> *mut PyObject {
if PyTuple_Size(args) != 0 || !kwds.is_null() {
PyErr_SetString(
PyExc_TypeError,
"Id() takes no arguments\0".as_ptr().cast::<c_char>(),
);
return ptr::null_mut();
}

let f: allocfunc = (*subtype).tp_alloc.unwrap_or(PyType_GenericAlloc);
let slf = f(subtype, 0);

if slf.is_null() {
return ptr::null_mut();
} else {
let id = Id::new();
let slf = slf.cast::<PyId>();
ptr::addr_of_mut!((*slf).id).write(id);
}

slf
}

unsafe extern "C" fn id_repr(slf: *mut PyObject) -> *mut PyObject {
let slf = slf.cast::<PyId>();
let id = (*slf).id.0;
let string = format!("Id({id})");
PyUnicode_FromStringAndSize(string.as_ptr().cast::<c_char>(), string.len() as Py_ssize_t)
}

unsafe extern "C" fn id_int(slf: *mut PyObject) -> *mut PyObject {
let slf = slf.cast::<PyId>();
let id = (*slf).id.0;
PyLong_FromUnsignedLongLong(id as c_ulonglong)
}

unsafe extern "C" fn id_richcompare(
slf: *mut PyObject,
other: *mut PyObject,
op: c_int,
) -> *mut PyObject {
let pytype = Py_TYPE(slf); // guaranteed to be `sequential.Id`
if Py_TYPE(other) != pytype {
return Py_NewRef(Py_NotImplemented());
}
let slf = (*slf.cast::<PyId>()).id;
let other = (*other.cast::<PyId>()).id;

let cmp = match op {
pyo3_ffi::Py_LT => slf < other,
pyo3_ffi::Py_LE => slf <= other,
pyo3_ffi::Py_EQ => slf == other,
pyo3_ffi::Py_NE => slf != other,
pyo3_ffi::Py_GT => slf > other,
pyo3_ffi::Py_GE => slf >= other,
unrecognized => {
let msg = format!("unrecognized richcompare opcode {unrecognized}\0");
PyErr_SetString(PyExc_SystemError, msg.as_ptr().cast::<c_char>());
return ptr::null_mut();
}
};

if cmp {
Py_NewRef(Py_True())
} else {
Py_NewRef(Py_False())
}
}

static mut SLOTS: &[PyType_Slot] = &[
PyType_Slot {
slot: Py_tp_new,
pfunc: id_new as *mut c_void,
},
PyType_Slot {
slot: Py_tp_doc,
pfunc: "An id that is increased every time an instance is created\0"
.as_ptr()
.cast::<c_void>()
.cast_mut(),
},
PyType_Slot {
slot: Py_tp_repr,
pfunc: id_repr as *mut c_void,
},
PyType_Slot {
slot: Py_nb_int,
pfunc: id_int as *mut c_void,
},
PyType_Slot {
slot: Py_tp_richcompare,
pfunc: id_richcompare as *mut c_void,
},
PyType_Slot {
slot: 0,
pfunc: ptr::null_mut(),
},
];

pub static mut ID_SPEC: PyType_Spec = PyType_Spec {
name: "sequential.Id\0".as_ptr().cast::<c_char>(),
basicsize: mem::size_of::<PyId>() as c_int,
itemsize: 0,
flags: Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE,
slots: unsafe { SLOTS as *const [PyType_Slot] as *mut PyType_Slot },
};
14 changes: 14 additions & 0 deletions examples/sequential/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::ptr;

use pyo3_ffi::*;

mod id;
mod module;
use crate::module::MODULE_DEF;

// The module initialization function, which must be named `PyInit_<your_module>`.
#[allow(non_snake_case)]
#[no_mangle]
pub unsafe extern "C" fn PyInit_sequential() -> *mut PyObject {
PyModuleDef_Init(ptr::addr_of_mut!(MODULE_DEF))
}
82 changes: 82 additions & 0 deletions examples/sequential/src/module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use core::ffi::{c_char, c_int, c_void};
use core::{mem, ptr};
use pyo3_ffi::*;

pub static mut MODULE_DEF: PyModuleDef = PyModuleDef {
m_base: PyModuleDef_HEAD_INIT,
m_name: "sequential\0".as_ptr().cast::<c_char>(),
m_doc: "A library for generating sequential ids, written in Rust.\0"
.as_ptr()
.cast::<c_char>(),
m_size: mem::size_of::<sequential_state>() as Py_ssize_t,
m_methods: std::ptr::null_mut(),
m_slots: unsafe { SEQUENTIAL_SLOTS as *const [PyModuleDef_Slot] as *mut PyModuleDef_Slot },
m_traverse: Some(sequential_traverse),
m_clear: Some(sequential_clear),
m_free: Some(sequential_free),
};

static mut SEQUENTIAL_SLOTS: &[PyModuleDef_Slot] = &[
PyModuleDef_Slot {
slot: Py_mod_exec,
value: sequential_exec as *mut c_void,
},
PyModuleDef_Slot {
slot: Py_mod_multiple_interpreters,
value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED,
},
PyModuleDef_Slot {
slot: 0,
value: ptr::null_mut(),
},
];

unsafe extern "C" fn sequential_exec(module: *mut PyObject) -> c_int {
let state: *mut sequential_state = PyModule_GetState(module).cast();

let id_type = PyType_FromModuleAndSpec(
module,
ptr::addr_of_mut!(crate::id::ID_SPEC),
ptr::null_mut(),
);
if id_type.is_null() {
PyErr_SetString(
PyExc_SystemError,
"cannot locate type object\0".as_ptr().cast::<c_char>(),
);
return -1;
}
(*state).id_type = id_type.cast::<PyTypeObject>();

PyModule_AddObjectRef(module, "Id\0".as_ptr().cast::<c_char>(), id_type)
}

unsafe extern "C" fn sequential_traverse(
module: *mut PyObject,
visit: visitproc,
arg: *mut c_void,
) -> c_int {
let state: *mut sequential_state = PyModule_GetState(module.cast()).cast();
let id_type: *mut PyObject = (*state).id_type.cast();

if id_type.is_null() {
0
} else {
(visit)(id_type, arg)
}
}

unsafe extern "C" fn sequential_clear(module: *mut PyObject) -> c_int {
let state: *mut sequential_state = PyModule_GetState(module.cast()).cast();
Py_CLEAR(ptr::addr_of_mut!((*state).id_type).cast());
0
}

unsafe extern "C" fn sequential_free(module: *mut c_void) {
sequential_clear(module.cast());
}

#[repr(C)]
struct sequential_state {
id_type: *mut PyTypeObject,
}
Loading

0 comments on commit 4892633

Please sign in to comment.