From e78551183b88da48b576a1db59589ad1e212092e Mon Sep 17 00:00:00 2001 From: Martin Geisler Date: Thu, 16 Nov 2023 12:00:28 +0100 Subject: Import 'fragile' crate Request Document: go/android-rust-importing-crates For CL Reviewers: go/android3p#cl-review For Build Team: go/ab-third-party-imports Bug: 310600229 Change-Id: I2de1ed95cb425cdf4c1d1c6e2d6cc555ddcaa8cc --- .cargo_vcs_info.json | 6 + .github/FUNDING.yml | 1 + .github/workflows/clippy.yml | 18 ++ .github/workflows/rustfmt.yml | 18 ++ .github/workflows/tests.yml | 32 ++++ .gitignore | 4 + .vscode/settings.json | 3 + Android.bp | 34 ++++ CHANGELOG.md | 30 +++ Cargo.toml | 32 ++++ LICENSE | 202 ++++++++++++++++++++ METADATA | 19 ++ MODULE_LICENSE_APACHE2 | 0 Makefile | 28 +++ OWNERS | 2 + README.md | 40 ++++ cargo_embargo.json | 8 + examples/basic-fragile.rs | 18 ++ examples/basic-sticky.rs | 21 +++ src/errors.rs | 14 ++ src/fragile.rs | 326 ++++++++++++++++++++++++++++++++ src/lib.rs | 157 ++++++++++++++++ src/registry.rs | 104 +++++++++++ src/semisticky.rs | 339 +++++++++++++++++++++++++++++++++ src/sticky.rs | 423 ++++++++++++++++++++++++++++++++++++++++++ src/thread_id.rs | 12 ++ 26 files changed, 1891 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/clippy.yml create mode 100644 .github/workflows/rustfmt.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Android.bp create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 METADATA create mode 100644 MODULE_LICENSE_APACHE2 create mode 100644 Makefile create mode 100644 OWNERS create mode 100644 README.md create mode 100644 cargo_embargo.json create mode 100644 examples/basic-fragile.rs create mode 100644 examples/basic-sticky.rs create mode 100644 src/errors.rs create mode 100644 src/fragile.rs create mode 100644 src/lib.rs create mode 100644 src/registry.rs create mode 100644 src/semisticky.rs create mode 100644 src/sticky.rs create mode 100644 src/thread_id.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..bb0c99d --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "77a27e9919647956516a2fca32ce52e8e1a5ca53" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7b47c03 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [mitsuhiko] diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..e812a0e --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,18 @@ +name: Clippy + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: clippy, rustfmt + override: true + - name: Run clippy + run: make lint diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml new file mode 100644 index 0000000..9002962 --- /dev/null +++ b/.github/workflows/rustfmt.yml @@ -0,0 +1,18 @@ +name: Rustfmt + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: clippy, rustfmt + override: true + - name: Run rustfmt + run: make format-check diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..68917f1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Tests + +on: [push] + +jobs: + test-latest: + name: Test on Latest + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + - name: Test + run: make test + + test-stable: + name: Test on 1.42.0 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.42.0 + profile: minimal + override: true + - name: Test + run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a821aa9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +/target +**/*.rs.bk +Cargo.lock diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..edd1e96 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.checkOnSave.command": "clippy" +} \ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..e7c3447 --- /dev/null +++ b/Android.bp @@ -0,0 +1,34 @@ +// This file is generated by cargo_embargo. +// Do not modify this file as changes will be overridden on upgrade. + +rust_test { + name: "fragile_test_src_lib", + host_supported: true, + crate_name: "fragile", + cargo_env_compat: true, + cargo_pkg_version: "2.0.0", + srcs: ["src/lib.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", +} + +rust_library { + name: "libfragile", + host_supported: true, + crate_name: "fragile", + cargo_env_compat: true, + cargo_pkg_version: "2.0.0", + srcs: ["src/lib.rs"], + edition: "2018", + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], + product_available: true, + vendor_available: true, + visibility: ["//external/rust/crates/mockall"], +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e200b76 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to similar are documented here. + +## 2.0.0 + +* `Fragile` no longer boxes internally. +* `Sticky` and `SemiSticky` now require the use of stack tokens. + For more information see [#26](https://github.com/mitsuhiko/fragile/issues/26) +* `Sticky` now tries to drop entries from the thread local registry eagerly + if it's dropped on the right thread. + +## 1.2.1 + +* Fixed non slab versions only allowing a single sticky. + +## 1.2.0 + +Note on safety: the `Sticky` and `SemiSticky` types allow data to live +longer than the wrapper type which is why they are now requiring a `'static` +bound. Previously it was possible to create a sticky containing a bare +reference which permitted unsafe access. + +* `Sticky` now requires `'static`. +* Added the `slab` feature for an internal optimization for `Sticky` to use + a slab instead of a `HashMap`. + +## Older Releases + +Older releases were yanked due to the insufficient trait bound on `Sticky`. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b33fe1c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "fragile" +version = "2.0.0" +authors = ["Armin Ronacher "] +description = "Provides wrapper types for sending non-send values to other threads." +homepage = "https://github.com/mitsuhiko/fragile" +readme = "README.md" +keywords = [ + "send", + "cell", + "non-send", + "send-wrapper", + "failure", +] +license = "Apache-2.0" +repository = "https://github.com/mitsuhiko/fragile" + +[dependencies.slab] +version = "0.4.5" +optional = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..230b483 --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "fragile" +description: "Provides wrapper types for sending non-send values to other threads." +third_party { + identifier { + type: "crates.io" + value: "https://crates.io/crates/fragile" + } + identifier { + type: "Archive" + value: "https://static.crates.io/crates/fragile/fragile-2.0.0.crate" + } + version: "2.0.0" + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 11 + day: 6 + } +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..08a16cf --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +all: build test + +build: + @cargo build + +check: + @cargo check + +doc: + @cargo doc + +test: + @cargo test + @cargo test --all-features + +format: + @rustup component add rustfmt 2> /dev/null + @cargo fmt --all + +format-check: + @rustup component add rustfmt 2> /dev/null + @cargo fmt --all -- --check + +lint: + @rustup component add clippy 2> /dev/null + @cargo clippy + +.PHONY: all check doc test format format-check lint diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..48bea6e --- /dev/null +++ b/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 688011 +include platform/prebuilts/rust:main:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e93fae --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Fragile + +[![Build Status](https://github.com/mitsuhiko/fragile/workflows/Tests/badge.svg?branch=master)](https://github.com/mitsuhiko/fragile/actions?query=workflow%3ATests) +[![Crates.io](https://img.shields.io/crates/d/fragile.svg)](https://crates.io/crates/fragile) +[![License](https://img.shields.io/github/license/mitsuhiko/fragile)](https://github.com/mitsuhiko/fragile/blob/master/LICENSE) +[![rustc 1.42.0](https://img.shields.io/badge/rust-1.42%2B-orange.svg)](https://img.shields.io/badge/rust-1.42%2B-orange.svg) +[![Documentation](https://docs.rs/fragile/badge.svg)](https://docs.rs/fragile) + +This library provides wrapper types that permit sending non Send types to other +threads and use runtime checks to ensure safety. + +It provides the `Fragile`, `Sticky` and `SemiSticky` types which are +similar in nature but have different behaviors with regards to how destructors +are executed. The `Fragile` will panic if the destructor is called in another +thread, `Sticky` will temporarily leak the object until the thread shuts down. +`SemiSticky` is a compromise of the two. It behaves like `Sticky` but it +avoids the use of thread local storage if the type does not need `Drop`. + +## Example + +```rust +use std::thread; + +// creating and using a fragile object in the same thread works +let val = Fragile::new(true); +assert_eq!(*val.get(), true); +assert!(val.try_get().is_ok()); + +// once send to another thread it stops working +thread::spawn(move || { + assert!(val.try_get().is_err()); +}).join() + .unwrap(); +``` + +## License and Links + +- [Documentation](https://docs.rs/fragile/) +- [Issue Tracker](https://github.com/mitsuhiko/fragile/issues) +- License: [Apache 2.0](https://github.com/mitsuhiko/fragile/blob/master/LICENSE) diff --git a/cargo_embargo.json b/cargo_embargo.json new file mode 100644 index 0000000..0d0e572 --- /dev/null +++ b/cargo_embargo.json @@ -0,0 +1,8 @@ +{ + "tests": true, + "module_visibility": { + "libfragile": [ + "//external/rust/crates/mockall" + ] + } +} diff --git a/examples/basic-fragile.rs b/examples/basic-fragile.rs new file mode 100644 index 0000000..54e33fd --- /dev/null +++ b/examples/basic-fragile.rs @@ -0,0 +1,18 @@ +use std::thread; + +use fragile::Fragile; + +fn main() { + // creating and using a fragile object in the same thread works + let val = Fragile::new(true); + println!("debug print in same thread: {:?}", &val); + println!("try_get in same thread: {:?}", val.try_get()); + + // once send to another thread it stops working + thread::spawn(move || { + println!("debug print in other thread: {:?}", &val); + println!("try_get in other thread: {:?}", val.try_get()); + }) + .join() + .unwrap(); +} diff --git a/examples/basic-sticky.rs b/examples/basic-sticky.rs new file mode 100644 index 0000000..e982b1d --- /dev/null +++ b/examples/basic-sticky.rs @@ -0,0 +1,21 @@ +use std::thread; + +use fragile::Sticky; + +fn main() { + fragile::stack_token!(tok); + + // creating and using a fragile object in the same thread works + let val = Sticky::new(true); + println!("debug print in same thread: {:?}", &val); + println!("try_get in same thread: {:?}", val.try_get(tok)); + + // once send to another thread it stops working + thread::spawn(move || { + fragile::stack_token!(tok); + println!("debug print in other thread: {:?}", &val); + println!("try_get in other thread: {:?}", val.try_get(tok)); + }) + .join() + .unwrap(); +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..bf3fb4c --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,14 @@ +use std::error; +use std::fmt; + +/// Returned when borrowing fails. +#[derive(Debug)] +pub struct InvalidThreadAccess; + +impl fmt::Display for InvalidThreadAccess { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "fragile value accessed from foreign thread") + } +} + +impl error::Error for InvalidThreadAccess {} diff --git a/src/fragile.rs b/src/fragile.rs new file mode 100644 index 0000000..92eb3d3 --- /dev/null +++ b/src/fragile.rs @@ -0,0 +1,326 @@ +use std::cmp; +use std::fmt; +use std::mem; +use std::num::NonZeroUsize; + +use crate::errors::InvalidThreadAccess; +use crate::thread_id; +use std::mem::ManuallyDrop; + +/// A [`Fragile`] wraps a non sendable `T` to be safely send to other threads. +/// +/// Once the value has been wrapped it can be sent to other threads but access +/// to the value on those threads will fail. +/// +/// If the value needs destruction and the fragile wrapper is on another thread +/// the destructor will panic. Alternatively you can use +/// [`Sticky`](crate::Sticky) which is not going to panic but might temporarily +/// leak the value. +pub struct Fragile { + // ManuallyDrop is necessary because we need to move out of here without running the + // Drop code in functions like `into_inner`. + value: ManuallyDrop, + thread_id: NonZeroUsize, +} + +impl Fragile { + /// Creates a new [`Fragile`] wrapping a `value`. + /// + /// The value that is moved into the [`Fragile`] can be non `Send` and + /// will be anchored to the thread that created the object. If the + /// fragile wrapper type ends up being send from thread to thread + /// only the original thread can interact with the value. + pub fn new(value: T) -> Self { + Fragile { + value: ManuallyDrop::new(value), + thread_id: thread_id::get(), + } + } + + /// Returns `true` if the access is valid. + /// + /// This will be `false` if the value was sent to another thread. + pub fn is_valid(&self) -> bool { + thread_id::get() == self.thread_id + } + + #[inline(always)] + fn assert_thread(&self) { + if !self.is_valid() { + panic!("trying to access wrapped value in fragile container from incorrect thread."); + } + } + + /// Consumes the `Fragile`, returning the wrapped value. + /// + /// # Panics + /// + /// Panics if called from a different thread than the one where the + /// original value was created. + pub fn into_inner(self) -> T { + self.assert_thread(); + + let mut this = ManuallyDrop::new(self); + + // SAFETY: `this` is not accessed beyond this point, and because it's in a ManuallyDrop its + // destructor is not run. + unsafe { ManuallyDrop::take(&mut this.value) } + } + + /// Consumes the `Fragile`, returning the wrapped value if successful. + /// + /// The wrapped value is returned if this is called from the same thread + /// as the one where the original value was created, otherwise the + /// [`Fragile`] is returned as `Err(self)`. + pub fn try_into_inner(self) -> Result { + if thread_id::get() == self.thread_id { + Ok(self.into_inner()) + } else { + Err(self) + } + } + + /// Immutably borrows the wrapped value. + /// + /// # Panics + /// + /// Panics if the calling thread is not the one that wrapped the value. + /// For a non-panicking variant, use [`try_get`](Self::try_get). + pub fn get(&self) -> &T { + self.assert_thread(); + &*self.value + } + + /// Mutably borrows the wrapped value. + /// + /// # Panics + /// + /// Panics if the calling thread is not the one that wrapped the value. + /// For a non-panicking variant, use [`try_get_mut`](Self::try_get_mut). + pub fn get_mut(&mut self) -> &mut T { + self.assert_thread(); + &mut *self.value + } + + /// Tries to immutably borrow the wrapped value. + /// + /// Returns `None` if the calling thread is not the one that wrapped the value. + pub fn try_get(&self) -> Result<&T, InvalidThreadAccess> { + if thread_id::get() == self.thread_id { + Ok(&*self.value) + } else { + Err(InvalidThreadAccess) + } + } + + /// Tries to mutably borrow the wrapped value. + /// + /// Returns `None` if the calling thread is not the one that wrapped the value. + pub fn try_get_mut(&mut self) -> Result<&mut T, InvalidThreadAccess> { + if thread_id::get() == self.thread_id { + Ok(&mut *self.value) + } else { + Err(InvalidThreadAccess) + } + } +} + +impl Drop for Fragile { + fn drop(&mut self) { + if mem::needs_drop::() { + if thread_id::get() == self.thread_id { + // SAFETY: `ManuallyDrop::drop` cannot be called after this point. + unsafe { ManuallyDrop::drop(&mut self.value) }; + } else { + panic!("destructor of fragile object ran on wrong thread"); + } + } + } +} + +impl From for Fragile { + #[inline] + fn from(t: T) -> Fragile { + Fragile::new(t) + } +} + +impl Clone for Fragile { + #[inline] + fn clone(&self) -> Fragile { + Fragile::new(self.get().clone()) + } +} + +impl Default for Fragile { + #[inline] + fn default() -> Fragile { + Fragile::new(T::default()) + } +} + +impl PartialEq for Fragile { + #[inline] + fn eq(&self, other: &Fragile) -> bool { + *self.get() == *other.get() + } +} + +impl Eq for Fragile {} + +impl PartialOrd for Fragile { + #[inline] + fn partial_cmp(&self, other: &Fragile) -> Option { + self.get().partial_cmp(other.get()) + } + + #[inline] + fn lt(&self, other: &Fragile) -> bool { + *self.get() < *other.get() + } + + #[inline] + fn le(&self, other: &Fragile) -> bool { + *self.get() <= *other.get() + } + + #[inline] + fn gt(&self, other: &Fragile) -> bool { + *self.get() > *other.get() + } + + #[inline] + fn ge(&self, other: &Fragile) -> bool { + *self.get() >= *other.get() + } +} + +impl Ord for Fragile { + #[inline] + fn cmp(&self, other: &Fragile) -> cmp::Ordering { + self.get().cmp(other.get()) + } +} + +impl fmt::Display for Fragile { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt::Display::fmt(self.get(), f) + } +} + +impl fmt::Debug for Fragile { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match self.try_get() { + Ok(value) => f.debug_struct("Fragile").field("value", value).finish(), + Err(..) => { + struct InvalidPlaceholder; + impl fmt::Debug for InvalidPlaceholder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("") + } + } + + f.debug_struct("Fragile") + .field("value", &InvalidPlaceholder) + .finish() + } + } + } +} + +// this type is sync because access can only ever happy from the same thread +// that created it originally. All other threads will be able to safely +// call some basic operations on the reference and they will fail. +unsafe impl Sync for Fragile {} + +// The entire point of this type is to be Send +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for Fragile {} + +#[test] +fn test_basic() { + use std::thread; + let val = Fragile::new(true); + assert_eq!(val.to_string(), "true"); + assert_eq!(val.get(), &true); + assert!(val.try_get().is_ok()); + thread::spawn(move || { + assert!(val.try_get().is_err()); + }) + .join() + .unwrap(); +} + +#[test] +fn test_mut() { + let mut val = Fragile::new(true); + *val.get_mut() = false; + assert_eq!(val.to_string(), "false"); + assert_eq!(val.get(), &false); +} + +#[test] +#[should_panic] +fn test_access_other_thread() { + use std::thread; + let val = Fragile::new(true); + thread::spawn(move || { + val.get(); + }) + .join() + .unwrap(); +} + +#[test] +fn test_noop_drop_elsewhere() { + use std::thread; + let val = Fragile::new(true); + thread::spawn(move || { + // force the move + val.try_get().ok(); + }) + .join() + .unwrap(); +} + +#[test] +fn test_panic_on_drop_elsewhere() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + use std::thread; + let was_called = Arc::new(AtomicBool::new(false)); + struct X(Arc); + impl Drop for X { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + let val = Fragile::new(X(was_called.clone())); + assert!(thread::spawn(move || { + val.try_get().ok(); + }) + .join() + .is_err()); + assert!(!was_called.load(Ordering::SeqCst)); +} + +#[test] +fn test_rc_sending() { + use std::rc::Rc; + use std::sync::mpsc::channel; + use std::thread; + + let val = Fragile::new(Rc::new(true)); + let (tx, rx) = channel(); + + let thread = thread::spawn(move || { + assert!(val.try_get().is_err()); + let here = val; + tx.send(here).unwrap(); + }); + + let rv = rx.recv().unwrap(); + assert!(**rv.get()); + + thread.join().unwrap(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..16edc9d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,157 @@ +//! This library provides wrapper types that permit sending non `Send` types to +//! other threads and use runtime checks to ensure safety. +//! +//! It provides three types: [`Fragile`] and [`Sticky`] which are similar in nature +//! but have different behaviors with regards to how destructors are executed and +//! the extra [`SemiSticky`] type which uses [`Sticky`] if the value has a +//! destructor and [`Fragile`] if it does not. +//! +//! All three types wrap a value and provide a `Send` bound. Neither of the types permit +//! access to the enclosed value unless the thread that wrapped the value is attempting +//! to access it. The difference between the types starts playing a role once +//! destructors are involved. +//! +//! A [`Fragile`] will actually send the `T` from thread to thread but will only +//! permit the original thread to invoke the destructor. If the value gets dropped +//! in a different thread, the destructor will panic. +//! +//! A [`Sticky`] on the other hand does not actually send the `T` around but keeps +//! it stored in the original thread's thread local storage. If it gets dropped +//! in the originating thread it gets cleaned up immediately, otherwise it leaks +//! until the thread shuts down naturally. [`Sticky`] because it borrows into the +//! TLS also requires you to "prove" that you are not doing any funny business with +//! the borrowed value that lives for longer than the current stack frame which +//! results in a slightly more complex API. +//! +//! There is a third typed called [`SemiSticky`] which shares the API with [`Sticky`] +//! but internally uses a boxed [`Fragile`] if the type does not actually need a dtor +//! in which case [`Fragile`] is preferred. +//! +//! # Fragile Usage +//! +//! [`Fragile`] is the easiest type to use. It works almost like a cell. +//! +//! ``` +//! use std::thread; +//! use fragile::Fragile; +//! +//! // creating and using a fragile object in the same thread works +//! let val = Fragile::new(true); +//! assert_eq!(*val.get(), true); +//! assert!(val.try_get().is_ok()); +//! +//! // once send to another thread it stops working +//! thread::spawn(move || { +//! assert!(val.try_get().is_err()); +//! }).join() +//! .unwrap(); +//! ``` +//! +//! # Sticky Usage +//! +//! [`Sticky`] is similar to [`Fragile`] but because it places the value in the +//! thread local storage it comes with some extra restrictions to make it sound. +//! The advantage is it can be dropped from any thread but it comes with extra +//! restrictions. In particular it requires that values placed in it are `'static` +//! and that [`StackToken`]s are used to restrict lifetimes. +//! +//! ``` +//! use std::thread; +//! use fragile::Sticky; +//! +//! // creating and using a fragile object in the same thread works +//! fragile::stack_token!(tok); +//! let val = Sticky::new(true); +//! assert_eq!(*val.get(tok), true); +//! assert!(val.try_get(tok).is_ok()); +//! +//! // once send to another thread it stops working +//! thread::spawn(move || { +//! fragile::stack_token!(tok); +//! assert!(val.try_get(tok).is_err()); +//! }).join() +//! .unwrap(); +//! ``` +//! +//! # Why? +//! +//! Most of the time trying to use this crate is going to indicate some code smell. But +//! there are situations where this is useful. For instance you might have a bunch of +//! non `Send` types but want to work with a `Send` error type. In that case the non +//! sendable extra information can be contained within the error and in cases where the +//! error did not cross a thread boundary yet extra information can be obtained. +//! +//! # Drop / Cleanup Behavior +//! +//! All types will try to eagerly drop a value if they are dropped on the right thread. +//! [`Sticky`] and [`SemiSticky`] will however temporarily leak memory until a thread +//! shuts down if the value is dropped on the wrong thread. The benefit however is that +//! if you have that type of situation, and you can live with the consequences, the +//! type is not panicking. A [`Fragile`] dropped in the wrong thread will not just panic, +//! it will effectively also tear down the process because panicking in destructors is +//! non recoverable. +//! +//! # Features +//! +//! By default the crate has no dependencies. Optionally the `slab` feature can +//! be enabled which optimizes the internal storage of the [`Sticky`] type to +//! make it use a [`slab`](https://docs.rs/slab/latest/slab/) instead. +mod errors; +mod fragile; +mod registry; +mod semisticky; +mod sticky; +mod thread_id; + +use std::marker::PhantomData; + +pub use crate::errors::InvalidThreadAccess; +pub use crate::fragile::Fragile; +pub use crate::semisticky::SemiSticky; +pub use crate::sticky::Sticky; + +/// A token that is placed to the stack to constrain lifetimes. +/// +/// For more information about how these work see the documentation of +/// [`stack_token!`] which is the only way to create this token. +pub struct StackToken(PhantomData<*const ()>); + +impl StackToken { + /// Stack tokens must only be created on the stack. + #[doc(hidden)] + pub unsafe fn __private_new() -> StackToken { + // we place a const pointer in there to get a type + // that is neither Send nor Sync. + StackToken(PhantomData) + } +} + +/// Crates a token on the stack with a certain name for semi-sticky. +/// +/// The argument to the macro is the target name of a local variable +/// which holds a reference to a stack token. Because this is the +/// only way to create such a token, it acts as a proof to [`Sticky`] +/// or [`SemiSticky`] that can be used to constrain the lifetime of the +/// return values to the stack frame. +/// +/// This is necessary as otherwise a [`Sticky`] placed in a [`Box`] and +/// leaked with [`Box::leak`] (which creates a static lifetime) would +/// otherwise create a reference with `'static` lifetime. This is incorrect +/// as the actual lifetime is constrained to the lifetime of the thread. +/// For more information see [`issue 26`](https://github.com/mitsuhiko/fragile/issues/26). +/// +/// ```rust +/// let sticky = fragile::Sticky::new(true); +/// +/// // this places a token on the stack. +/// fragile::stack_token!(my_token); +/// +/// // the token needs to be passed to `get` and others. +/// let _ = sticky.get(my_token); +/// ``` +#[macro_export] +macro_rules! stack_token { + ($name:ident) => { + let $name = &unsafe { $crate::StackToken::__private_new() }; + }; +} diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..1ee070d --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,104 @@ +pub struct Entry { + /// The pointer to the object stored in the registry. This is a type-erased + /// `Box`. + pub ptr: *mut (), + /// The function that can be called on the above pointer to drop the object + /// and free its allocation. + pub drop: unsafe fn(*mut ()), +} + +#[cfg(feature = "slab")] +mod slab_impl { + use std::cell::UnsafeCell; + use std::num::NonZeroUsize; + + use super::Entry; + + pub struct Registry(pub slab::Slab); + + thread_local!(static REGISTRY: UnsafeCell = UnsafeCell::new(Registry(slab::Slab::new()))); + + pub use usize as ItemId; + + pub fn insert(thread_id: NonZeroUsize, entry: Entry) -> ItemId { + let _ = thread_id; + REGISTRY.with(|registry| unsafe { (*registry.get()).0.insert(entry) }) + } + + pub fn with R>(item_id: ItemId, thread_id: NonZeroUsize, f: F) -> R { + let _ = thread_id; + REGISTRY.with(|registry| f(unsafe { &*registry.get() }.0.get(item_id).unwrap())) + } + + pub fn remove(item_id: ItemId, thread_id: NonZeroUsize) -> Entry { + let _ = thread_id; + REGISTRY.with(|registry| unsafe { (*registry.get()).0.remove(item_id) }) + } + + pub fn try_remove(item_id: ItemId, thread_id: NonZeroUsize) -> Option { + let _ = thread_id; + REGISTRY.with(|registry| unsafe { (*registry.get()).0.try_remove(item_id) }) + } +} + +#[cfg(not(feature = "slab"))] +mod map_impl { + use std::cell::UnsafeCell; + use std::num::NonZeroUsize; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use super::Entry; + + pub struct Registry(pub std::collections::HashMap<(NonZeroUsize, NonZeroUsize), Entry>); + + thread_local!(static REGISTRY: UnsafeCell = UnsafeCell::new(Registry(Default::default()))); + + pub type ItemId = NonZeroUsize; + + fn next_item_id() -> NonZeroUsize { + static COUNTER: AtomicUsize = AtomicUsize::new(1); + NonZeroUsize::new(COUNTER.fetch_add(1, Ordering::SeqCst)) + .expect("more than usize::MAX items") + } + + pub fn insert(thread_id: NonZeroUsize, entry: Entry) -> ItemId { + let item_id = next_item_id(); + REGISTRY + .with(|registry| unsafe { (*registry.get()).0.insert((thread_id, item_id), entry) }); + item_id + } + + pub fn with R>(item_id: ItemId, thread_id: NonZeroUsize, f: F) -> R { + REGISTRY.with(|registry| { + f(unsafe { &*registry.get() } + .0 + .get(&(thread_id, item_id)) + .unwrap()) + }) + } + + pub fn remove(item_id: ItemId, thread_id: NonZeroUsize) -> Entry { + REGISTRY + .with(|registry| unsafe { (*registry.get()).0.remove(&(thread_id, item_id)).unwrap() }) + } + + pub fn try_remove(item_id: ItemId, thread_id: NonZeroUsize) -> Option { + REGISTRY.with(|registry| unsafe { (*registry.get()).0.remove(&(thread_id, item_id)) }) + } +} + +#[cfg(feature = "slab")] +pub use self::slab_impl::*; + +#[cfg(not(feature = "slab"))] +pub use self::map_impl::*; + +impl Drop for Registry { + fn drop(&mut self) { + for (_, value) in self.0.iter() { + // SAFETY: This function is only called once, and is called with the + // pointer it was created with. + unsafe { (value.drop)(value.ptr) }; + } + } +} diff --git a/src/semisticky.rs b/src/semisticky.rs new file mode 100644 index 0000000..2b6c0f4 --- /dev/null +++ b/src/semisticky.rs @@ -0,0 +1,339 @@ +use std::cmp; +use std::fmt; +use std::mem; + +use crate::errors::InvalidThreadAccess; +use crate::fragile::Fragile; +use crate::sticky::Sticky; +use crate::StackToken; + +enum SemiStickyImpl { + Fragile(Box>), + Sticky(Sticky), +} + +/// A [`SemiSticky`] keeps a value T stored in a thread if it has a drop. +/// +/// This is a combined version of [`Fragile`] and [`Sticky`]. If the type +/// does not have a drop it will effectively be a [`Fragile`], otherwise it +/// will be internally behave like a [`Sticky`]. +/// +/// This type requires `T: 'static` for the same reasons as [`Sticky`] and +/// also uses [`StackToken`]s. +pub struct SemiSticky { + inner: SemiStickyImpl, +} + +impl SemiSticky { + /// Creates a new [`SemiSticky`] wrapping a `value`. + /// + /// The value that is moved into the `SemiSticky` can be non `Send` and + /// will be anchored to the thread that created the object. If the + /// sticky wrapper type ends up being send from thread to thread + /// only the original thread can interact with the value. In case the + /// value does not have `Drop` it will be stored in the [`SemiSticky`] + /// instead. + pub fn new(value: T) -> Self { + SemiSticky { + inner: if mem::needs_drop::() { + SemiStickyImpl::Sticky(Sticky::new(value)) + } else { + SemiStickyImpl::Fragile(Box::new(Fragile::new(value))) + }, + } + } + + /// Returns `true` if the access is valid. + /// + /// This will be `false` if the value was sent to another thread. + pub fn is_valid(&self) -> bool { + match self.inner { + SemiStickyImpl::Fragile(ref inner) => inner.is_valid(), + SemiStickyImpl::Sticky(ref inner) => inner.is_valid(), + } + } + + /// Consumes the [`SemiSticky`], returning the wrapped value. + /// + /// # Panics + /// + /// Panics if called from a different thread than the one where the + /// original value was created. + pub fn into_inner(self) -> T { + match self.inner { + SemiStickyImpl::Fragile(inner) => inner.into_inner(), + SemiStickyImpl::Sticky(inner) => inner.into_inner(), + } + } + + /// Consumes the [`SemiSticky`], returning the wrapped value if successful. + /// + /// The wrapped value is returned if this is called from the same thread + /// as the one where the original value was created, otherwise the + /// [`SemiSticky`] is returned as `Err(self)`. + pub fn try_into_inner(self) -> Result { + match self.inner { + SemiStickyImpl::Fragile(inner) => inner.try_into_inner().map_err(|inner| SemiSticky { + inner: SemiStickyImpl::Fragile(Box::new(inner)), + }), + SemiStickyImpl::Sticky(inner) => inner.try_into_inner().map_err(|inner| SemiSticky { + inner: SemiStickyImpl::Sticky(inner), + }), + } + } + + /// Immutably borrows the wrapped value. + /// + /// # Panics + /// + /// Panics if the calling thread is not the one that wrapped the value. + /// For a non-panicking variant, use [`try_get`](Self::try_get). + pub fn get<'stack>(&'stack self, _proof: &'stack StackToken) -> &'stack T { + match self.inner { + SemiStickyImpl::Fragile(ref inner) => inner.get(), + SemiStickyImpl::Sticky(ref inner) => inner.get(_proof), + } + } + + /// Mutably borrows the wrapped value. + /// + /// # Panics + /// + /// Panics if the calling thread is not the one that wrapped the value. + /// For a non-panicking variant, use [`try_get_mut`](Self::try_get_mut). + pub fn get_mut<'stack>(&'stack mut self, _proof: &'stack StackToken) -> &'stack mut T { + match self.inner { + SemiStickyImpl::Fragile(ref mut inner) => inner.get_mut(), + SemiStickyImpl::Sticky(ref mut inner) => inner.get_mut(_proof), + } + } + + /// Tries to immutably borrow the wrapped value. + /// + /// Returns `None` if the calling thread is not the one that wrapped the value. + pub fn try_get<'stack>( + &'stack self, + _proof: &'stack StackToken, + ) -> Result<&'stack T, InvalidThreadAccess> { + match self.inner { + SemiStickyImpl::Fragile(ref inner) => inner.try_get(), + SemiStickyImpl::Sticky(ref inner) => inner.try_get(_proof), + } + } + + /// Tries to mutably borrow the wrapped value. + /// + /// Returns `None` if the calling thread is not the one that wrapped the value. + pub fn try_get_mut<'stack>( + &'stack mut self, + _proof: &'stack StackToken, + ) -> Result<&'stack mut T, InvalidThreadAccess> { + match self.inner { + SemiStickyImpl::Fragile(ref mut inner) => inner.try_get_mut(), + SemiStickyImpl::Sticky(ref mut inner) => inner.try_get_mut(_proof), + } + } +} + +impl From for SemiSticky { + #[inline] + fn from(t: T) -> SemiSticky { + SemiSticky::new(t) + } +} + +impl Clone for SemiSticky { + #[inline] + fn clone(&self) -> SemiSticky { + crate::stack_token!(tok); + SemiSticky::new(self.get(tok).clone()) + } +} + +impl Default for SemiSticky { + #[inline] + fn default() -> SemiSticky { + SemiSticky::new(T::default()) + } +} + +impl PartialEq for SemiSticky { + #[inline] + fn eq(&self, other: &SemiSticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) == *other.get(tok) + } +} + +impl Eq for SemiSticky {} + +impl PartialOrd for SemiSticky { + #[inline] + fn partial_cmp(&self, other: &SemiSticky) -> Option { + crate::stack_token!(tok); + self.get(tok).partial_cmp(other.get(tok)) + } + + #[inline] + fn lt(&self, other: &SemiSticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) < *other.get(tok) + } + + #[inline] + fn le(&self, other: &SemiSticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) <= *other.get(tok) + } + + #[inline] + fn gt(&self, other: &SemiSticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) > *other.get(tok) + } + + #[inline] + fn ge(&self, other: &SemiSticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) >= *other.get(tok) + } +} + +impl Ord for SemiSticky { + #[inline] + fn cmp(&self, other: &SemiSticky) -> cmp::Ordering { + crate::stack_token!(tok); + self.get(tok).cmp(other.get(tok)) + } +} + +impl fmt::Display for SemiSticky { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + crate::stack_token!(tok); + fmt::Display::fmt(self.get(tok), f) + } +} + +impl fmt::Debug for SemiSticky { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + crate::stack_token!(tok); + match self.try_get(tok) { + Ok(value) => f.debug_struct("SemiSticky").field("value", value).finish(), + Err(..) => { + struct InvalidPlaceholder; + impl fmt::Debug for InvalidPlaceholder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("") + } + } + + f.debug_struct("SemiSticky") + .field("value", &InvalidPlaceholder) + .finish() + } + } + } +} + +#[test] +fn test_basic() { + use std::thread; + let val = SemiSticky::new(true); + crate::stack_token!(tok); + assert_eq!(val.to_string(), "true"); + assert_eq!(val.get(tok), &true); + assert!(val.try_get(tok).is_ok()); + thread::spawn(move || { + crate::stack_token!(tok); + assert!(val.try_get(tok).is_err()); + }) + .join() + .unwrap(); +} + +#[test] +fn test_mut() { + let mut val = SemiSticky::new(true); + crate::stack_token!(tok); + *val.get_mut(tok) = false; + assert_eq!(val.to_string(), "false"); + assert_eq!(val.get(tok), &false); +} + +#[test] +#[should_panic] +fn test_access_other_thread() { + use std::thread; + let val = SemiSticky::new(true); + thread::spawn(move || { + crate::stack_token!(tok); + val.get(tok); + }) + .join() + .unwrap(); +} + +#[test] +fn test_drop_same_thread() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + let was_called = Arc::new(AtomicBool::new(false)); + struct X(Arc); + impl Drop for X { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + let val = SemiSticky::new(X(was_called.clone())); + mem::drop(val); + assert!(was_called.load(Ordering::SeqCst)); +} + +#[test] +fn test_noop_drop_elsewhere() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + use std::thread; + + let was_called = Arc::new(AtomicBool::new(false)); + + { + let was_called = was_called.clone(); + thread::spawn(move || { + struct X(Arc); + impl Drop for X { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + + let val = SemiSticky::new(X(was_called.clone())); + assert!(thread::spawn(move || { + // moves it here but do not deallocate + crate::stack_token!(tok); + val.try_get(tok).ok(); + }) + .join() + .is_ok()); + + assert!(!was_called.load(Ordering::SeqCst)); + }) + .join() + .unwrap(); + } + + assert!(was_called.load(Ordering::SeqCst)); +} + +#[test] +fn test_rc_sending() { + use std::rc::Rc; + use std::thread; + let val = SemiSticky::new(Rc::new(true)); + thread::spawn(move || { + crate::stack_token!(tok); + assert!(val.try_get(tok).is_err()); + }) + .join() + .unwrap(); +} diff --git a/src/sticky.rs b/src/sticky.rs new file mode 100644 index 0000000..bc15c40 --- /dev/null +++ b/src/sticky.rs @@ -0,0 +1,423 @@ +#![allow(clippy::unit_arg)] + +use std::cmp; +use std::fmt; +use std::marker::PhantomData; +use std::mem; +use std::num::NonZeroUsize; + +use crate::errors::InvalidThreadAccess; +use crate::registry; +use crate::thread_id; +use crate::StackToken; + +/// A [`Sticky`] keeps a value T stored in a thread. +/// +/// This type works similar in nature to [`Fragile`](crate::Fragile) and exposes a +/// similar interface. The difference is that whereas [`Fragile`](crate::Fragile) has +/// its destructor called in the thread where the value was sent, a +/// [`Sticky`] that is moved to another thread will have the internal +/// destructor called when the originating thread tears down. +/// +/// Because [`Sticky`] allows values to be kept alive for longer than the +/// [`Sticky`] itself, it requires all its contents to be `'static` for +/// soundness. More importantly it also requires the use of [`StackToken`]s. +/// For information about how to use stack tokens and why they are neded, +/// refer to [`stack_token!`](crate::stack_token). +/// +/// As this uses TLS internally the general rules about the platform limitations +/// of destructors for TLS apply. +pub struct Sticky { + item_id: registry::ItemId, + thread_id: NonZeroUsize, + _marker: PhantomData<*mut T>, +} + +impl Drop for Sticky { + fn drop(&mut self) { + // if the type needs dropping we can only do so on the + // right thread. worst case we leak the value until the + // thread dies. + if mem::needs_drop::() { + unsafe { + if self.is_valid() { + self.unsafe_take_value(); + } + } + + // otherwise we take the liberty to drop the value + // right here and now. We can however only do that if + // we are on the right thread. If we are not, we again + // need to wait for the thread to shut down. + } else if let Some(entry) = registry::try_remove(self.item_id, self.thread_id) { + unsafe { + (entry.drop)(entry.ptr); + } + } + } +} + +impl Sticky { + /// Creates a new [`Sticky`] wrapping a `value`. + /// + /// The value that is moved into the [`Sticky`] can be non `Send` and + /// will be anchored to the thread that created the object. If the + /// sticky wrapper type ends up being send from thread to thread + /// only the original thread can interact with the value. + pub fn new(value: T) -> Self { + let entry = registry::Entry { + ptr: Box::into_raw(Box::new(value)).cast(), + drop: |ptr| { + let ptr = ptr.cast::(); + // SAFETY: This callback will only be called once, with the + // above pointer. + drop(unsafe { Box::from_raw(ptr) }); + }, + }; + + let thread_id = thread_id::get(); + let item_id = registry::insert(thread_id, entry); + + Sticky { + item_id, + thread_id, + _marker: PhantomData, + } + } + + #[inline(always)] + fn with_value R, R>(&self, f: F) -> R { + self.assert_thread(); + + registry::with(self.item_id, self.thread_id, |entry| { + f(entry.ptr.cast::()) + }) + } + + /// Returns `true` if the access is valid. + /// + /// This will be `false` if the value was sent to another thread. + #[inline(always)] + pub fn is_valid(&self) -> bool { + thread_id::get() == self.thread_id + } + + #[inline(always)] + fn assert_thread(&self) { + if !self.is_valid() { + panic!("trying to access wrapped value in sticky container from incorrect thread."); + } + } + + /// Consumes the `Sticky`, returning the wrapped value. + /// + /// # Panics + /// + /// Panics if called from a different thread than the one where the + /// original value was created. + pub fn into_inner(mut self) -> T { + self.assert_thread(); + unsafe { + let rv = self.unsafe_take_value(); + mem::forget(self); + rv + } + } + + unsafe fn unsafe_take_value(&mut self) -> T { + let ptr = registry::remove(self.item_id, self.thread_id) + .ptr + .cast::(); + *Box::from_raw(ptr) + } + + /// Consumes the `Sticky`, returning the wrapped value if successful. + /// + /// The wrapped value is returned if this is called from the same thread + /// as the one where the original value was created, otherwise the + /// `Sticky` is returned as `Err(self)`. + pub fn try_into_inner(self) -> Result { + if self.is_valid() { + Ok(self.into_inner()) + } else { + Err(self) + } + } + + /// Immutably borrows the wrapped value. + /// + /// # Panics + /// + /// Panics if the calling thread is not the one that wrapped the value. + /// For a non-panicking variant, use [`try_get`](#method.try_get`). + pub fn get<'stack>(&'stack self, _proof: &'stack StackToken) -> &'stack T { + self.with_value(|value| unsafe { &*value }) + } + + /// Mutably borrows the wrapped value. + /// + /// # Panics + /// + /// Panics if the calling thread is not the one that wrapped the value. + /// For a non-panicking variant, use [`try_get_mut`](#method.try_get_mut`). + pub fn get_mut<'stack>(&'stack mut self, _proof: &'stack StackToken) -> &'stack mut T { + self.with_value(|value| unsafe { &mut *value }) + } + + /// Tries to immutably borrow the wrapped value. + /// + /// Returns `None` if the calling thread is not the one that wrapped the value. + pub fn try_get<'stack>( + &'stack self, + _proof: &'stack StackToken, + ) -> Result<&'stack T, InvalidThreadAccess> { + if self.is_valid() { + Ok(self.with_value(|value| unsafe { &*value })) + } else { + Err(InvalidThreadAccess) + } + } + + /// Tries to mutably borrow the wrapped value. + /// + /// Returns `None` if the calling thread is not the one that wrapped the value. + pub fn try_get_mut<'stack>( + &'stack mut self, + _proof: &'stack StackToken, + ) -> Result<&'stack mut T, InvalidThreadAccess> { + if self.is_valid() { + Ok(self.with_value(|value| unsafe { &mut *value })) + } else { + Err(InvalidThreadAccess) + } + } +} + +impl From for Sticky { + #[inline] + fn from(t: T) -> Sticky { + Sticky::new(t) + } +} + +impl Clone for Sticky { + #[inline] + fn clone(&self) -> Sticky { + crate::stack_token!(tok); + Sticky::new(self.get(tok).clone()) + } +} + +impl Default for Sticky { + #[inline] + fn default() -> Sticky { + Sticky::new(T::default()) + } +} + +impl PartialEq for Sticky { + #[inline] + fn eq(&self, other: &Sticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) == *other.get(tok) + } +} + +impl Eq for Sticky {} + +impl PartialOrd for Sticky { + #[inline] + fn partial_cmp(&self, other: &Sticky) -> Option { + crate::stack_token!(tok); + self.get(tok).partial_cmp(other.get(tok)) + } + + #[inline] + fn lt(&self, other: &Sticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) < *other.get(tok) + } + + #[inline] + fn le(&self, other: &Sticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) <= *other.get(tok) + } + + #[inline] + fn gt(&self, other: &Sticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) > *other.get(tok) + } + + #[inline] + fn ge(&self, other: &Sticky) -> bool { + crate::stack_token!(tok); + *self.get(tok) >= *other.get(tok) + } +} + +impl Ord for Sticky { + #[inline] + fn cmp(&self, other: &Sticky) -> cmp::Ordering { + crate::stack_token!(tok); + self.get(tok).cmp(other.get(tok)) + } +} + +impl fmt::Display for Sticky { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + crate::stack_token!(tok); + fmt::Display::fmt(self.get(tok), f) + } +} + +impl fmt::Debug for Sticky { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + crate::stack_token!(tok); + match self.try_get(tok) { + Ok(value) => f.debug_struct("Sticky").field("value", value).finish(), + Err(..) => { + struct InvalidPlaceholder; + impl fmt::Debug for InvalidPlaceholder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("") + } + } + + f.debug_struct("Sticky") + .field("value", &InvalidPlaceholder) + .finish() + } + } + } +} + +// similar as for fragile ths type is sync because it only accesses TLS data +// which is thread local. There is nothing that needs to be synchronized. +unsafe impl Sync for Sticky {} + +// The entire point of this type is to be Send +unsafe impl Send for Sticky {} + +#[test] +fn test_basic() { + use std::thread; + let val = Sticky::new(true); + crate::stack_token!(tok); + assert_eq!(val.to_string(), "true"); + assert_eq!(val.get(tok), &true); + assert!(val.try_get(tok).is_ok()); + thread::spawn(move || { + crate::stack_token!(tok); + assert!(val.try_get(tok).is_err()); + }) + .join() + .unwrap(); +} + +#[test] +fn test_mut() { + let mut val = Sticky::new(true); + crate::stack_token!(tok); + *val.get_mut(tok) = false; + assert_eq!(val.to_string(), "false"); + assert_eq!(val.get(tok), &false); +} + +#[test] +#[should_panic] +fn test_access_other_thread() { + use std::thread; + let val = Sticky::new(true); + thread::spawn(move || { + crate::stack_token!(tok); + val.get(tok); + }) + .join() + .unwrap(); +} + +#[test] +fn test_drop_same_thread() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + let was_called = Arc::new(AtomicBool::new(false)); + struct X(Arc); + impl Drop for X { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + let val = Sticky::new(X(was_called.clone())); + mem::drop(val); + assert!(was_called.load(Ordering::SeqCst)); +} + +#[test] +fn test_noop_drop_elsewhere() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + use std::thread; + + let was_called = Arc::new(AtomicBool::new(false)); + + { + let was_called = was_called.clone(); + thread::spawn(move || { + struct X(Arc); + impl Drop for X { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + + let val = Sticky::new(X(was_called.clone())); + assert!(thread::spawn(move || { + // moves it here but do not deallocate + crate::stack_token!(tok); + val.try_get(tok).ok(); + }) + .join() + .is_ok()); + + assert!(!was_called.load(Ordering::SeqCst)); + }) + .join() + .unwrap(); + } + + assert!(was_called.load(Ordering::SeqCst)); +} + +#[test] +fn test_rc_sending() { + use std::rc::Rc; + use std::thread; + let val = Sticky::new(Rc::new(true)); + thread::spawn(move || { + crate::stack_token!(tok); + assert!(val.try_get(tok).is_err()); + }) + .join() + .unwrap(); +} + +#[test] +fn test_two_stickies() { + struct Wat; + + impl Drop for Wat { + fn drop(&mut self) { + // do nothing + } + } + + let s1 = Sticky::new(Wat); + let s2 = Sticky::new(Wat); + + // make sure all is well + + drop(s1); + drop(s2); +} diff --git a/src/thread_id.rs b/src/thread_id.rs new file mode 100644 index 0000000..00468b2 --- /dev/null +++ b/src/thread_id.rs @@ -0,0 +1,12 @@ +use std::num::NonZeroUsize; +use std::sync::atomic::{AtomicUsize, Ordering}; + +fn next() -> NonZeroUsize { + static COUNTER: AtomicUsize = AtomicUsize::new(1); + NonZeroUsize::new(COUNTER.fetch_add(1, Ordering::SeqCst)).expect("more than usize::MAX threads") +} + +pub(crate) fn get() -> NonZeroUsize { + thread_local!(static THREAD_ID: NonZeroUsize = next()); + THREAD_ID.with(|&x| x) +} -- cgit v1.2.3