Introduce #[pyclass(unsendable)]

This commit is contained in:
kngwyu 2020-06-29 12:05:50 +09:00
parent 6335a7f1b5
commit d76fe7835a
10 changed files with 238 additions and 55 deletions

View File

@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- `#[pyclass(unsendable)]`. [#1009](https://github.com/PyO3/pyo3/pull/1009)
## [0.11.0] - 2020-06-28 ## [0.11.0] - 2020-06-28
### Added ### Added

View File

@ -124,6 +124,8 @@ If a custom class contains references to other Python objects that can be collec
* `extends=BaseType` - Use a custom base class. The base `BaseType` must implement `PyTypeInfo`. * `extends=BaseType` - Use a custom base class. The base `BaseType` must implement `PyTypeInfo`.
* `subclass` - Allows Python classes to inherit from this class. * `subclass` - Allows Python classes to inherit from this class.
* `dict` - Adds `__dict__` support, so that the instances of this type have a dictionary containing arbitrary instance variables. * `dict` - Adds `__dict__` support, so that the instances of this type have a dictionary containing arbitrary instance variables.
* `unsendable` - Making it safe to expose `!Send` structs to Python, where all object can be accessed
by multiple threads. A class marked with `unsendable` panics when accessed by another thread.
* `module="XXX"` - Set the name of the module the class will be shown as defined in. If not given, the class * `module="XXX"` - Set the name of the module the class will be shown as defined in. If not given, the class
will be a virtual member of the `builtins` module. will be a virtual member of the `builtins` module.
@ -974,6 +976,10 @@ impl pyo3::class::proto_methods::HasProtoRegistry for MyClass {
&REGISTRY &REGISTRY
} }
} }
impl pyo3::pyclass::PyClassSend for MyClass {
type ThreadChecker = pyo3::pyclass::ThreadCheckerStub<MyClass>;
}
# let gil = Python::acquire_gil(); # let gil = Python::acquire_gil();
# let py = gil.python(); # let py = gil.python();
# let cls = py.get_type::<MyClass>(); # let cls = py.get_type::<MyClass>();

View File

@ -8,43 +8,71 @@ For a detailed list of all changes, see [CHANGELOG.md](https://github.com/PyO3/p
### Stable Rust ### Stable Rust
PyO3 now supports the stable Rust toolchain. The minimum required version is 1.39.0. PyO3 now supports the stable Rust toolchain. The minimum required version is 1.39.0.
### `#[pyclass]` structs must now be `Send` ### `#[pyclass]` structs must now be `Send` or `unsendable`
Because `#[pyclass]` structs can be sent between threads by the Python interpreter, they must implement Because `#[pyclass]` structs can be sent between threads by the Python interpreter, they must implement
`Send` to guarantee thread safety. This bound was added in PyO3 `0.11.0`. `Send` or declared as `unsendable` (by `#[pyclass(unsendable)]`).
Note that `unsendable` is added in PyO3 `0.11.1` and `Send` is always required in PyO3 `0.11.0`.
This may "break" some code which previously was accepted, even though it was unsound. To resolve this, This may "break" some code which previously was accepted, even though it could be unsound.
consider using types like `Arc` instead of `Rc`, `Mutex` instead of `RefCell`, and add `Send` to any There can be two fixes:
boxed closures stored inside the `#[pyclass]`.
Before: 1. If you think that your `#[pyclass]` actually must be `Send`able, then let's implement `Send`.
```rust,compile_fail A common, safer way is using thread-safe types. E.g., `Arc` instead of `Rc`, `Mutex` instead of
use pyo3::prelude::*; `RefCell`, and `Box<dyn Send + T>` instead of `Box<dyn T>`.
use std::rc::Rc;
use std::cell::RefCell;
#[pyclass] Before:
struct NotThreadSafe { ```rust,compile_fail
shared_bools: Rc<RefCell<Vec<bool>>>, use pyo3::prelude::*;
closure: Box<Fn()> use std::rc::Rc;
} use std::cell::RefCell;
```
After: #[pyclass]
```rust struct NotThreadSafe {
use pyo3::prelude::*; shared_bools: Rc<RefCell<Vec<bool>>>,
use std::sync::{Arc, Mutex}; closure: Box<dyn Fn()>
}
```
#[pyclass] After:
struct ThreadSafe { ```rust
shared_bools: Arc<Mutex<Vec<bool>>>, use pyo3::prelude::*;
closure: Box<Fn() + Send> use std::sync::{Arc, Mutex};
}
```
Or in situations where you cannot change your `#[pyclass]` to automatically implement `Send` #[pyclass]
(e.g., when it contains a raw pointer), you can use `unsafe impl Send`. struct ThreadSafe {
In such cases, care should be taken to ensure the struct is actually thread safe. shared_bools: Arc<Mutex<Vec<bool>>>,
See [the Rustnomicon](ttps://doc.rust-lang.org/nomicon/send-and-sync.html) for more. closure: Box<dyn Fn() + Send>
}
```
In situations where you cannot change your `#[pyclass]` to automatically implement `Send`
(e.g., when it contains a raw pointer), you can use `unsafe impl Send`.
In such cases, care should be taken to ensure the struct is actually thread safe.
See [the Rustnomicon](https://doc.rust-lang.org/nomicon/send-and-sync.html) for more.
2. If you think that your `#[pyclass]` should not be accessed by another thread, you can use
`unsendable` flag. A class marked with `unsendable` panics when accessed by another thread,
making it thread-safe to expose an unsendable object to the Python interpreter.
Before:
```rust,compile_fail
use pyo3::prelude::*;
#[pyclass]
struct Unsendable {
pointers: Vec<*mut std::os::raw::c_char>,
}
```
After:
```rust
use pyo3::prelude::*;
#[pyclass(unsendable)]
struct Unsendable {
pointers: Vec<*mut std::os::raw::c_char>,
}
```
### All `PyObject` and `Py<T>` methods now take `Python` as an argument ### All `PyObject` and `Py<T>` methods now take `Python` as an argument
Previously, a few methods such as `Object::get_refcnt` did not take `Python` as an argument (to Previously, a few methods such as `Object::get_refcnt` did not take `Python` as an argument (to

View File

@ -19,6 +19,7 @@ pub struct PyClassArgs {
pub flags: Vec<syn::Expr>, pub flags: Vec<syn::Expr>,
pub base: syn::TypePath, pub base: syn::TypePath,
pub has_extends: bool, pub has_extends: bool,
pub has_unsendable: bool,
pub module: Option<syn::LitStr>, pub module: Option<syn::LitStr>,
} }
@ -45,6 +46,7 @@ impl Default for PyClassArgs {
flags: vec![parse_quote! { 0 }], flags: vec![parse_quote! { 0 }],
base: parse_quote! { pyo3::PyAny }, base: parse_quote! { pyo3::PyAny },
has_extends: false, has_extends: false,
has_unsendable: false,
} }
} }
} }
@ -60,7 +62,7 @@ impl PyClassArgs {
} }
} }
/// Match a single flag /// Match a key/value flag
fn add_assign(&mut self, assign: &syn::ExprAssign) -> syn::Result<()> { fn add_assign(&mut self, assign: &syn::ExprAssign) -> syn::Result<()> {
let syn::ExprAssign { left, right, .. } = assign; let syn::ExprAssign { left, right, .. } = assign;
let key = match &**left { let key = match &**left {
@ -120,31 +122,27 @@ impl PyClassArgs {
Ok(()) Ok(())
} }
/// Match a key/value flag /// Match a single flag
fn add_path(&mut self, exp: &syn::ExprPath) -> syn::Result<()> { fn add_path(&mut self, exp: &syn::ExprPath) -> syn::Result<()> {
let flag = exp.path.segments.first().unwrap().ident.to_string(); let flag = exp.path.segments.first().unwrap().ident.to_string();
let path = match flag.as_str() { let mut push_flag = |flag| {
"gc" => { self.flags.push(syn::Expr::Path(flag));
parse_quote! {pyo3::type_flags::GC} };
} match flag.as_str() {
"weakref" => { "gc" => push_flag(parse_quote! {pyo3::type_flags::GC}),
parse_quote! {pyo3::type_flags::WEAKREF} "weakref" => push_flag(parse_quote! {pyo3::type_flags::WEAKREF}),
} "subclass" => push_flag(parse_quote! {pyo3::type_flags::BASETYPE}),
"subclass" => { "dict" => push_flag(parse_quote! {pyo3::type_flags::DICT}),
parse_quote! {pyo3::type_flags::BASETYPE} "unsendable" => {
} self.has_unsendable = true;
"dict" => {
parse_quote! {pyo3::type_flags::DICT}
} }
_ => { _ => {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
&exp.path, &exp.path,
"Expected one of gc/weakref/subclass/dict", "Expected one of gc/weakref/subclass/dict/unsendable",
)) ))
} }
}; };
self.flags.push(syn::Expr::Path(path));
Ok(()) Ok(())
} }
} }
@ -386,6 +384,16 @@ fn impl_class(
quote! {} quote! {}
}; };
let thread_checker = if attr.has_unsendable {
quote! { pyo3::pyclass::ThreadCheckerImpl<#cls> }
} else if attr.has_extends {
quote! {
pyo3::pyclass::ThreadCheckerInherited<#cls, <#cls as pyo3::type_object::PyTypeInfo>::BaseType>
}
} else {
quote! { pyo3::pyclass::ThreadCheckerStub<#cls> }
};
Ok(quote! { Ok(quote! {
unsafe impl pyo3::type_object::PyTypeInfo for #cls { unsafe impl pyo3::type_object::PyTypeInfo for #cls {
type Type = #cls; type Type = #cls;
@ -424,6 +432,10 @@ fn impl_class(
type Target = pyo3::PyRefMut<'a, #cls>; type Target = pyo3::PyRefMut<'a, #cls>;
} }
impl pyo3::pyclass::PyClassSend for #cls {
type ThreadChecker = #thread_checker;
}
#into_pyobject #into_pyobject
#impl_inventory #impl_inventory
@ -433,7 +445,6 @@ fn impl_class(
#extra #extra
#gc_impl #gc_impl
}) })
} }

View File

@ -7,7 +7,7 @@
use crate::err::{PyErr, PyResult}; use crate::err::{PyErr, PyResult};
use crate::exceptions::TypeError; use crate::exceptions::TypeError;
use crate::instance::PyNativeType; use crate::instance::PyNativeType;
use crate::pyclass::PyClass; use crate::pyclass::{PyClass, PyClassThreadChecker};
use crate::types::{PyAny, PyDict, PyModule, PyTuple}; use crate::types::{PyAny, PyDict, PyModule, PyTuple};
use crate::{ffi, GILPool, IntoPy, PyCell, Python}; use crate::{ffi, GILPool, IntoPy, PyCell, Python};
use std::cell::UnsafeCell; use std::cell::UnsafeCell;
@ -157,11 +157,12 @@ impl ModuleDef {
/// Utilities for basetype /// Utilities for basetype
#[doc(hidden)] #[doc(hidden)]
pub trait PyBaseTypeUtils { pub trait PyBaseTypeUtils: Sized {
type Dict; type Dict;
type WeakRef; type WeakRef;
type LayoutAsBase; type LayoutAsBase;
type BaseNativeType; type BaseNativeType;
type ThreadChecker: PyClassThreadChecker<Self>;
} }
impl<T: PyClass> PyBaseTypeUtils for T { impl<T: PyClass> PyBaseTypeUtils for T {
@ -169,6 +170,7 @@ impl<T: PyClass> PyBaseTypeUtils for T {
type WeakRef = T::WeakRef; type WeakRef = T::WeakRef;
type LayoutAsBase = crate::pycell::PyCellInner<T>; type LayoutAsBase = crate::pycell::PyCellInner<T>;
type BaseNativeType = T::BaseNativeType; type BaseNativeType = T::BaseNativeType;
type ThreadChecker = T::ThreadChecker;
} }
/// Utility trait to enable &PyClass as a pymethod/function argument /// Utility trait to enable &PyClass as a pymethod/function argument

View File

@ -1,10 +1,11 @@
//! Includes `PyCell` implementation. //! Includes `PyCell` implementation.
use crate::conversion::{AsPyPointer, FromPyPointer, ToPyObject}; use crate::conversion::{AsPyPointer, FromPyPointer, ToPyObject};
use crate::pyclass::{PyClass, PyClassThreadChecker};
use crate::pyclass_init::PyClassInitializer; use crate::pyclass_init::PyClassInitializer;
use crate::pyclass_slots::{PyClassDict, PyClassWeakRef}; use crate::pyclass_slots::{PyClassDict, PyClassWeakRef};
use crate::type_object::{PyBorrowFlagLayout, PyLayout, PySizedLayout, PyTypeInfo}; use crate::type_object::{PyBorrowFlagLayout, PyLayout, PySizedLayout, PyTypeInfo};
use crate::types::PyAny; use crate::types::PyAny;
use crate::{ffi, FromPy, PyClass, PyErr, PyNativeType, PyObject, PyResult, Python}; use crate::{ffi, FromPy, PyErr, PyNativeType, PyObject, PyResult, Python};
use std::cell::{Cell, UnsafeCell}; use std::cell::{Cell, UnsafeCell};
use std::fmt; use std::fmt;
use std::mem::ManuallyDrop; use std::mem::ManuallyDrop;
@ -161,6 +162,7 @@ pub struct PyCell<T: PyClass> {
inner: PyCellInner<T>, inner: PyCellInner<T>,
dict: T::Dict, dict: T::Dict,
weakref: T::WeakRef, weakref: T::WeakRef,
thread_checker: T::ThreadChecker,
} }
unsafe impl<T: PyClass> PyNativeType for PyCell<T> {} unsafe impl<T: PyClass> PyNativeType for PyCell<T> {}
@ -227,6 +229,7 @@ impl<T: PyClass> PyCell<T> {
/// } /// }
/// ``` /// ```
pub fn try_borrow(&self) -> Result<PyRef<'_, T>, PyBorrowError> { pub fn try_borrow(&self) -> Result<PyRef<'_, T>, PyBorrowError> {
self.thread_checker.ensure();
let flag = self.inner.get_borrow_flag(); let flag = self.inner.get_borrow_flag();
if flag == BorrowFlag::HAS_MUTABLE_BORROW { if flag == BorrowFlag::HAS_MUTABLE_BORROW {
Err(PyBorrowError { _private: () }) Err(PyBorrowError { _private: () })
@ -258,6 +261,7 @@ impl<T: PyClass> PyCell<T> {
/// assert!(c.try_borrow_mut().is_ok()); /// assert!(c.try_borrow_mut().is_ok());
/// ``` /// ```
pub fn try_borrow_mut(&self) -> Result<PyRefMut<'_, T>, PyBorrowMutError> { pub fn try_borrow_mut(&self) -> Result<PyRefMut<'_, T>, PyBorrowMutError> {
self.thread_checker.ensure();
if self.inner.get_borrow_flag() != BorrowFlag::UNUSED { if self.inner.get_borrow_flag() != BorrowFlag::UNUSED {
Err(PyBorrowMutError { _private: () }) Err(PyBorrowMutError { _private: () })
} else { } else {
@ -296,6 +300,7 @@ impl<T: PyClass> PyCell<T> {
/// } /// }
/// ``` /// ```
pub unsafe fn try_borrow_unguarded(&self) -> Result<&T, PyBorrowError> { pub unsafe fn try_borrow_unguarded(&self) -> Result<&T, PyBorrowError> {
self.thread_checker.ensure();
if self.inner.get_borrow_flag() == BorrowFlag::HAS_MUTABLE_BORROW { if self.inner.get_borrow_flag() == BorrowFlag::HAS_MUTABLE_BORROW {
Err(PyBorrowError { _private: () }) Err(PyBorrowError { _private: () })
} else { } else {
@ -352,6 +357,7 @@ impl<T: PyClass> PyCell<T> {
let self_ = base as *mut Self; let self_ = base as *mut Self;
(*self_).dict = T::Dict::new(); (*self_).dict = T::Dict::new();
(*self_).weakref = T::WeakRef::new(); (*self_).weakref = T::WeakRef::new();
(*self_).thread_checker = T::ThreadChecker::new();
Ok(self_) Ok(self_)
} }
} }

View File

@ -1,14 +1,16 @@
//! `PyClass` trait //! `PyClass` and related traits.
use crate::class::methods::{PyClassAttributeDef, PyMethodDefType, PyMethods}; use crate::class::methods::{PyClassAttributeDef, PyMethodDefType, PyMethods};
use crate::class::proto_methods::PyProtoMethods; use crate::class::proto_methods::PyProtoMethods;
use crate::conversion::{AsPyPointer, FromPyPointer}; use crate::conversion::{AsPyPointer, FromPyPointer};
use crate::derive_utils::PyBaseTypeUtils;
use crate::pyclass_slots::{PyClassDict, PyClassWeakRef}; use crate::pyclass_slots::{PyClassDict, PyClassWeakRef};
use crate::type_object::{type_flags, PyLayout}; use crate::type_object::{type_flags, PyLayout};
use crate::types::PyAny; use crate::types::PyAny;
use crate::{class, ffi, PyCell, PyErr, PyNativeType, PyResult, PyTypeInfo, Python}; use crate::{class, ffi, PyCell, PyErr, PyNativeType, PyResult, PyTypeInfo, Python};
use std::ffi::CString; use std::ffi::CString;
use std::marker::PhantomData;
use std::os::raw::c_void; use std::os::raw::c_void;
use std::ptr; use std::{ptr, thread};
#[inline] #[inline]
pub(crate) unsafe fn default_new<T: PyTypeInfo>( pub(crate) unsafe fn default_new<T: PyTypeInfo>(
@ -91,10 +93,10 @@ pub(crate) unsafe fn tp_free_fallback(obj: *mut ffi::PyObject) {
pub trait PyClass: pub trait PyClass:
PyTypeInfo<Layout = PyCell<Self>, AsRefTarget = PyCell<Self>> PyTypeInfo<Layout = PyCell<Self>, AsRefTarget = PyCell<Self>>
+ Sized + Sized
+ PyClassSend
+ PyClassAlloc + PyClassAlloc
+ PyMethods + PyMethods
+ PyProtoMethods + PyProtoMethods
+ Send
{ {
/// Specify this class has `#[pyclass(dict)]` or not. /// Specify this class has `#[pyclass(dict)]` or not.
type Dict: PyClassDict; type Dict: PyClassDict;
@ -308,3 +310,76 @@ fn py_class_properties<T: PyMethods>() -> Vec<ffi::PyGetSetDef> {
defs.values().cloned().collect() defs.values().cloned().collect()
} }
/// This trait is implemented for `#[pyclass]` and handles following two situations:
/// 1. In case `T` is `Send`, stub `ThreadChecker` is used and does nothing.
/// This implementation is used by default. Compile fails if `T: !Send`.
/// 2. In case `T` is `!Send`, `ThreadChecker` panics when `T` is accessed by another thread.
/// This implementation is used when `#[pyclass(unsendable)]` is given.
/// Panicking makes it safe to expose `T: !Send` to the Python interpreter, where all objects
/// can be accessed by multiple threads by `threading` module.
pub trait PyClassSend: Sized {
type ThreadChecker: PyClassThreadChecker<Self>;
}
#[doc(hidden)]
pub trait PyClassThreadChecker<T>: Sized {
fn ensure(&self);
fn new() -> Self;
private_decl! {}
}
/// Stub checker for `Send` types.
#[doc(hidden)]
pub struct ThreadCheckerStub<T: Send>(PhantomData<T>);
impl<T: Send> PyClassThreadChecker<T> for ThreadCheckerStub<T> {
fn ensure(&self) {}
fn new() -> Self {
ThreadCheckerStub(PhantomData)
}
private_impl! {}
}
impl<T: PyNativeType> PyClassThreadChecker<T> for ThreadCheckerStub<crate::PyObject> {
fn ensure(&self) {}
fn new() -> Self {
ThreadCheckerStub(PhantomData)
}
private_impl! {}
}
/// Thread checker for unsendable types.
/// Panics when the value is accessed by another thread.
#[doc(hidden)]
pub struct ThreadCheckerImpl<T>(thread::ThreadId, PhantomData<T>);
impl<T> PyClassThreadChecker<T> for ThreadCheckerImpl<T> {
fn ensure(&self) {
if thread::current().id() != self.0 {
panic!(
"{} is unsendable, but sent to another thread!",
std::any::type_name::<T>()
);
}
}
fn new() -> Self {
ThreadCheckerImpl(thread::current().id(), PhantomData)
}
private_impl! {}
}
/// Thread checker for types that have `Send` and `extends=...`.
/// Ensures that `T: Send` and the parent is not accessed by another thread.
#[doc(hidden)]
pub struct ThreadCheckerInherited<T: Send, U: PyBaseTypeUtils>(PhantomData<T>, U::ThreadChecker);
impl<T: Send, U: PyBaseTypeUtils> PyClassThreadChecker<T> for ThreadCheckerInherited<T, U> {
fn ensure(&self) {
self.1.ensure();
}
fn new() -> Self {
ThreadCheckerInherited(PhantomData, U::ThreadChecker::new())
}
private_impl! {}
}

View File

@ -75,6 +75,7 @@ macro_rules! pyobject_native_type {
type WeakRef = $crate::pyclass_slots::PyClassDummySlot; type WeakRef = $crate::pyclass_slots::PyClassDummySlot;
type LayoutAsBase = $crate::pycell::PyCellBase<$name>; type LayoutAsBase = $crate::pycell::PyCellBase<$name>;
type BaseNativeType = $name; type BaseNativeType = $name;
type ThreadChecker = $crate::pyclass::ThreadCheckerStub<$crate::PyObject>;
} }
pyobject_native_type_named!($name $(,$type_param)*); pyobject_native_type_named!($name $(,$type_param)*);
pyobject_native_type_convert!($name, $layout, $typeobject, $module, $checkfunction $(,$type_param)*); pyobject_native_type_convert!($name, $layout, $typeobject, $module, $checkfunction $(,$type_param)*);

View File

@ -163,3 +163,55 @@ fn class_with_object_field() {
py_assert!(py, ty, "ty(5).value == 5"); py_assert!(py, ty, "ty(5).value == 5");
py_assert!(py, ty, "ty(None).value == None"); py_assert!(py, ty, "ty(None).value == None");
} }
#[pyclass(unsendable)]
struct UnsendableBase {
rc: std::rc::Rc<usize>,
}
#[pymethods]
impl UnsendableBase {
fn value(&self) -> usize {
*self.rc.as_ref()
}
}
#[pyclass(extends=UnsendableBase)]
struct UnsendableChild {}
/// If a class is marked as `unsendable`, it panics when accessed by another thread.
#[test]
fn panic_unsendable() {
let gil = Python::acquire_gil();
let py = gil.python();
let base = || UnsendableBase {
rc: std::rc::Rc::new(0),
};
let unsendable_base = PyCell::new(py, base()).unwrap();
let unsendable_child = PyCell::new(py, (UnsendableChild {}, base())).unwrap();
let source = pyo3::indoc::indoc!(
r#"
def value():
return unsendable.value()
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
future = executor.submit(value)
try:
result = future.result()
assert False, 'future must panic'
except BaseException as e:
assert str(e) == 'test_class_basics::UnsendableBase is unsendable, but sent to another thread!'
"#
);
let globals = PyModule::import(py, "__main__").unwrap().dict();
let test = |unsendable| {
globals.set_item("unsendable", unsendable).unwrap();
py.run(source, Some(globals), None)
.map_err(|e| e.print(py))
.unwrap();
};
test(unsendable_base.as_ref());
test(unsendable_child.as_ref());
}

View File

@ -22,7 +22,7 @@ error: Expected string literal (e.g., "my_mod")
12 | #[pyclass(module = my_module)] 12 | #[pyclass(module = my_module)]
| ^^^^^^^^^ | ^^^^^^^^^
error: Expected one of gc/weakref/subclass/dict error: Expected one of gc/weakref/subclass/dict/unsendable
--> $DIR/invalid_pyclass_args.rs:15:11 --> $DIR/invalid_pyclass_args.rs:15:11
| |
15 | #[pyclass(weakrev)] 15 | #[pyclass(weakrev)]