diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml new file mode 100644 index 0000000..5491638 --- /dev/null +++ b/.github/workflows/rustfmt.yml @@ -0,0 +1,24 @@ +# The file is the workflow for rustfmt +# +# It runs `cargo fmt --check` +# +# It will fail if there are formatting problems. +on: [push, pull_request] +name: rustfmt + +env: + CARGO_TERM_COLOR: always + +jobs: + rustfmt: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - shell: bash + run: | + cargo fmt --all -- --check diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml new file mode 100644 index 0000000..d76b0f6 --- /dev/null +++ b/.github/workflows/spellcheck.yml @@ -0,0 +1,23 @@ +# Checks spelling with typos +# This has very few false positives, only checking for known misspellings (similar to codespell). +# This action is based on https://github.com/crate-ci/typos/blob/master/docs/github-action.md +name: Check Spelling +on: [push, pull_request] + +env: + CLICOLOR: 1 + +permissions: + contents: read + +jobs: + typos: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + + name: Check spelling with typos + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run typos (pinned version) + uses: crate-ci/typos@v1.35.6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a84ef57 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,174 @@ +# We use `actions-rs` for most of our actions +# +# This file is for the main tests. clippy & rustfmt are separate workflows +on: [push, pull_request] +name: Cargo Test + +env: + CARGO_TERM_COLOR: always + # has a history of occasional bugs (especially on old versions) + # + # the ci is free so we might as well use it ;) + CARGO_INCREMENTAL: 0 + + + +jobs: + test: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + + runs-on: ubuntu-latest + strategy: + fail-fast: false # Even if one job fails we still want to see the other ones + matrix: + rust: + # Minimum Supported Rust Version + # + # This is hardcoded and needs to be in sync with Cargo.toml and the README + # + # If one of the features does not support this MSRV, + # you need to remove this from the main list and manually add the desired + # feature/version combinations to 'include' + # This hack is not currently needed because serde-erased v0.3 supports our MSRV. + - 1.63 + + # Intermediate Releases (between MSRV and latest stable) + # Be careful not to add these needlessly; they hold up CI + + # The most recent version of stable rust (automatically updated) + - stable + - nightly + # NOTE: Features to test must be specified manually. They are applied to all versions separately. + features: + - "std" + - "std bytemuck slog serde" + include: + - rust: stable + features: "std parking_lot" + - rust: nightly + features: "nightly" # no features except nightly + - rust: nightly + features: "nightly alloc" # no features except nightly + alloc + - rust: nightly + features: "std nightly" + - rust: nightly + features: "std unique-wrap-std nightly" + - rust: nightly + features: "std nightly parking_lot" + - rust: nightly + features: "std nightly parking_lot bytemuck slog serde" + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Cache Cargo Registry + id: cache-index + uses: actions/cache@v4 + with: + path: + # Before the sparse index, updating the registry took forever + ~/.cargo/registry/index/ + key: ${{ runner.os }}-cargo-${{ matrix.rust }} + restore-keys: | + ${{ runner.os }}-cargo- + continue-on-error: false + - name: Test + # NOTE: Running --all-targets does not include doc tests + # Does not compile benchmarks because they break on MSRV. Still checked by clippy + run: | + cargo test --all --verbose --no-default-features --features "${{ matrix.features }}" --exclude "benchmarks" + + clippy: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: + # in hardcoded versions, warnings will fail the build + - 1.89 + # in auto-updated versions, warnings will not fail the build + - stable + - nightly + features: + # NOTE: Unfortunately, the benchmarks crate implicitly requires 'std' + - "std parking_lot bytemuck slog serde" + include: + - rust: nightly + features: "std slog bytemuck parking_lot serde nightly" + - rust: nightly + features: "std nightly unique-wrap-std" + + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: clippy + - name: Clippy + run: | + cargo clippy --all --all-targets --verbose --no-default-features --features "${{ matrix.features }}" -- -D warnings + # When using hardcoded/pinned versions, warnings are forbidden. + # + # On automatically updated versions of rust (both stable & nightly) we allow clippy to fail. + # This is because automatic updates can introduce new lints or change existing lints. + continue-on-error: ${{ !contains(matrix.rust, '1.') }} + + docs: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + + runs-on: ubuntu-latest + env: + RUSTDOCFLAGS: "-D warnings" + strategy: + fail-fast: false + matrix: + rust: + - nightly + - stable + features: + - "std parking_lot bytemuck slog serde" + include: + - rust: nightly + features: "std parking_lot bytemuck slog serde nightly nightly-docs" + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Docs + run: | + cargo doc --verbose --no-default-features --features "${{ matrix.features }}" + cargo-rdme: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + components: rust-src + # need to cache cargo-rdme to avoid redundant install + - name: Cache Binaries + id: cache-binaries + uses: actions/cache@v4 + with: + path: + ~/.cargo/bin/cargo-rdme + key: ${{ runner.os }}-binary-cargo-rdme + - name: Install cargo-rdme + shell: bash + # NOTE: This doesn't worry about installing updates + run: | + if not test -f "~/.cargo/bin/cargo-rdme"; then + cargo install cargo-rdme + fi + - name: Run cargo-rdme + run: | + cargo rdme --check diff --git a/Cargo.toml b/Cargo.toml index 58f13ff..98afe0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "threadid" description = "Fast and flexible thread identifiers" -version = "0.1.0-alpha.1" +version = "0.1.0" license = "Apache-2.0 OR MIT" repository = "https://github.com/Techcable/threadid.rs" edition = "2021" @@ -10,6 +10,8 @@ edition = "2021" # 1.61 - dep:foo and foo?/std syntax # 1.63 - const Mutex::new rust-version = "1.63" +categories = ["concurrency", "development-tools::debugging", "no-std", "no-std::no-alloc"] +keywords = ["threadid", "tid", "name"] [dependencies] cfg-if = "1" @@ -18,19 +20,16 @@ portable-atomic = { version = "1", features = ["require-cas"] } nonmax = { version = "0.5", default-features = false } # optional parking_lot = { version = "0.12", optional = true } -serde = { version = "1", optional = true, features = ["derive"] } +serde = { version = "1", optional = true } slog = { version = "2.6", optional = true, default-features = false } -bytemuck = { version = "1.23", optional = true, features = ["derive"] } +bytemuck = { version = "1.23", optional = true } [dev-dependencies] -# criterion = "0.7" crossbeam-utils = "0.8" -[[bench]] -name = "access" -required-features = [] -harness = false -test = false +[[example]] +name = "thread_name" +required-features = ["std"] [features] default = ["std"] @@ -42,7 +41,7 @@ alloc = [] nightly = ["parking_lot?/nightly"] # Enables serde serialization for most types # -# Deserialization can not be reasonbly implemented +# Deserialization can not be reasonably implemented serde = ["dep:serde", "nonmax/serde"] # Implements slog::Value for most types slog = ["dep:slog"] @@ -64,3 +63,10 @@ parking_lot = ["dep:parking_lot"] # By default, this is implicitly enabled on nightly + std, # this feature only makes the requirement explicit. unique-wrap-std = ["std"] + +[package.metadata.docs.rs] +all-features = true + +[workspace] +members = [".", "benchmarks"] +resolver = "2" diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml new file mode 100644 index 0000000..fd3b918 --- /dev/null +++ b/benchmarks/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "threadid-benchmarks" +description = "Benchmarks for the threadid crate" +version = "0.0.0" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/Techcable/threadid.rs" +publish = false # internal use only +edition = "2021" + +[dev-dependencies] +threadid.path = ".." +criterion = "0.7" +crossbeam-utils = "0.8" + +[[bench]] +name = "access" +required-features = [] +harness = false +test = false diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..dda445f --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,2 @@ +# Benchmarks for thraedid.rs +Separate from the main crate to avoid compiling criterion and because the MSRV is different. diff --git a/benches/access.rs b/benchmarks/benches/access.rs similarity index 93% rename from benches/access.rs rename to benchmarks/benches/access.rs index 1d91f06..7a919a7 100644 --- a/benches/access.rs +++ b/benchmarks/benches/access.rs @@ -1,3 +1,5 @@ +#![allow(clippy::redundant_closure)] // slightly cleaner + use criterion::{Criterion, criterion_group, criterion_main}; use threadid::{LiveThreadId, StdThreadId, UniqueThreadId}; diff --git a/src/lib.rs b/src/lib.rs index 911f3e8..d725cc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,10 +50,7 @@ )] #[cfg(not(any(feature = "nightly", feature = "std")))] -compile_error!("Either the `nightly` or `std` feature must be enabled for this crate to work"); - -#[cfg(all(feature = "unique-wrap-std", not(feature = "nightly")))] -compile_error!("The `unique-wrap-std` feature currently requires the `nightly` feature to be enabled"); +compile_error!("The `threadid` crate requires at least one of the `nightly` or `std` features"); #[cfg(feature = "alloc")] extern crate alloc; @@ -70,9 +67,12 @@ pub use unique::UniqueThreadId; #[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "std")))] pub use self::std::StdThreadId; +#[macro_use] +mod utils; #[macro_use] mod locals; #[cfg(feature = "std")] +#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "std")))] pub mod debug; #[cfg(feature = "std")] #[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "std")))] @@ -81,7 +81,6 @@ pub mod live; #[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "std")))] pub mod std; pub mod unique; -mod utils; /// Defines methods common to all thread ids. /// diff --git a/src/live.rs b/src/live.rs index bc7eb30..073623b 100644 --- a/src/live.rs +++ b/src/live.rs @@ -24,7 +24,6 @@ use crate::utils::sync::{Mutex, MutexGuard}; /// It is guaranteed that `Option` has the same representation as `LiveThreadId`. /// Currently [`LiveThreadId::to_int`] can be zero, reducing wasted indexes. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[must_use] #[repr(transparent)] pub struct LiveThreadId { @@ -112,13 +111,17 @@ impl LiveThreadId { self.index.get() } } +simple_serde_serialize!(LiveThreadId, |this| this.to_int()); #[cfg(feature = "bytemuck")] +#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "bytemuck")))] // SAFETY: We wrap a NonMax, which has the same niche as NonZero unsafe impl bytemuck::ZeroableInOption for LiveThreadId {} #[cfg(feature = "bytemuck")] +#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "bytemuck")))] // SAFETY: A NonMax is equivalent to a NonZero unsafe impl bytemuck::NoUninit for LiveThreadId {} #[cfg(feature = "slog")] +#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "slog")))] impl slog::Value for LiveThreadId { fn serialize(&self, _record: &slog::Record, key: slog::Key, serializer: &mut dyn slog::Serializer) -> slog::Result { serializer.emit_arguments(key, &format_args!("{self:?}")) diff --git a/src/locals.rs b/src/locals.rs index 978e77b..f6693ec 100644 --- a/src/locals.rs +++ b/src/locals.rs @@ -13,12 +13,15 @@ macro_rules! fast_thread_local { static $var: $tp = const { $init };)* } } else { - compile_error!("Either the `std` or `nightly` feature must be enabled"); + $( + static $var: $crate::locals::dummy::DummyLocalKey<$tp> = $crate::locals::dummy::DummyLocalKey::new($init); + )* } } }; } +/// Version of [`std::thread::LocalKey`] using the nightly `#[thread_local]` attribute. #[cfg(feature = "nightly")] #[cfg_attr(not(feature = "std"), allow(dead_code))] pub mod nightly { @@ -41,3 +44,34 @@ pub mod nightly { } type AccessError = core::convert::Infallible; } + +/// Dummy version of [`std::thread::LocalKey`] to avoid duplicate compilation errors. +#[cfg(not(any(feature = "nightly", feature = "std")))] +pub mod dummy { + use core::mem::ManuallyDrop; + + pub struct DummyLocalKey { + _value: ManuallyDrop, + } + // always Sync because we don't give any access + unsafe impl Sync for DummyLocalKey {} + impl DummyLocalKey { + pub const fn new(value: T) -> Self { + DummyLocalKey { + _value: ManuallyDrop::new(value), + } + } + #[inline] + pub fn with R, R>(&self, func: F) -> R { + let _ = func; + unimplemented!("thread local unsupported") + } + #[inline] + #[allow(clippy::unnecessary_wraps)] + pub fn try_with R, R>(&self, func: F) -> Result { + let _ = func; + Err("thread local unsupported") + } + } + type AccessError = &'static str; +} diff --git a/src/std.rs b/src/std.rs index f898a58..fd0726a 100644 --- a/src/std.rs +++ b/src/std.rs @@ -43,6 +43,7 @@ unsafe impl crate::IThreadId for StdThreadId { } } #[cfg(feature = "bytemuck")] +#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "bytemuck")))] // SAFETY: We are #[repr(transparent)] unsafe impl bytemuck::TransparentWrapper for StdThreadId {} // SAFETY: stdlib guarantees that threadid is unique diff --git a/src/unique.rs b/src/unique.rs index 94155d9..8b073ff 100644 --- a/src/unique.rs +++ b/src/unique.rs @@ -18,7 +18,6 @@ fast_thread_local! { /// While the current value is a [`core::num::NonZero`], /// this may change in the future if other niche types like `NonMax` become stabilized. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[must_use] #[repr(transparent)] pub struct UniqueThreadId(NonZeroU64); @@ -43,8 +42,15 @@ impl UniqueThreadId { #[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "unique-wrap-std")))] #[inline] pub fn from_std(id: impl Into) -> Self { - // SAFETY: Enabling the feature guarantees ids are equivalent - unsafe { Self::from_int(id.into().as_u64().get()) } + cfg_if::cfg_if! { + if #[cfg(feature="nightly")] { + // SAFETY: Enabling the feature guarantees ids are equivalent + unsafe { Self::from_int(id.into().as_u64().get()) } + } else { + let _ = id; + unreachable!("unique-wrap-std not possible without nightly features") + } + } } /// Convert a [`UniqueThreadId`] into an integer value. @@ -82,7 +88,7 @@ impl UniqueThreadId { if #[cfg(all(feature = "std", feature = "nightly"))] { UniqueThreadId(crate::StdThreadId::current().0.as_u64()) } else if #[cfg(feature = "unique-wrap-std")] { - compile_error!("requires nightly + std") + compile_error!("The `unique-wrap-std` feature requires the `nightly` feature to be enabled") } else { THREAD_ID.with(|cell| { match cell.get() { @@ -98,6 +104,7 @@ impl UniqueThreadId { } } } +simple_serde_serialize!(UniqueThreadId, |this| this.to_int()); // SAFETY: Unique across all threads that have ever existed unsafe impl crate::IThreadId for UniqueThreadId { #[inline] @@ -106,9 +113,11 @@ unsafe impl crate::IThreadId for UniqueThreadId { } } #[cfg(feature = "bytemuck")] +#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "bytemuck")))] // SAFETY: Wraps a NonZero unsafe impl bytemuck::ZeroableInOption for UniqueThreadId {} #[cfg(feature = "bytemuck")] +#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "bytemuck")))] // SAFETY: Wraps a NonZero unsafe impl bytemuck::NoUninit for UniqueThreadId {} impl From for u64 { diff --git a/src/utils.rs b/src/utils.rs index 4b9e3fa..ef16383 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -72,3 +72,16 @@ mod cell { // SAFETY: Fine to send because old thread loses access unsafe impl Send for OnceCell {} } +macro_rules! simple_serde_serialize { + ($target:ident, |$this:ident| $to_inner:expr) => { + #[cfg(feature = "serde")] + #[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "serde")))] + impl serde::Serialize for $target { + fn serialize(&self, serializer: S) -> Result { + let $this = self; + let value = $to_inner; + serde::Serialize::serialize(&value, serializer) + } + } + }; +} diff --git a/tests/basic.rs b/tests/basic.rs index 91bbfa6..cec5537 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -5,12 +5,16 @@ use std::collections::HashSet; use std::sync::{Barrier, Mutex}; use crossbeam_utils::thread; -use threadid::{IThreadId, LiveThreadId, StdThreadId, UniqueThreadId}; +use threadid::{IThreadId, UniqueThreadId}; +#[cfg(feature = "std")] +use threadid::{LiveThreadId, StdThreadId}; #[test] fn death_reuse() { let seen_unique_ids = Mutex::new(HashSet::::new()); + #[cfg(feature = "std")] let seen_std_ids = Mutex::new(HashSet::::new()); + #[cfg(feature = "std")] let seen_live_ids = Mutex::new(HashSet::::new()); fn add_new(lock: &Mutex>) { let id = threadid::current(); @@ -26,8 +30,11 @@ fn death_reuse() { } } add_new(&seen_unique_ids); - add_new(&seen_std_ids); - add_new(&seen_live_ids); + #[cfg(feature = "std")] + { + add_new(&seen_std_ids); + add_new(&seen_live_ids); + } { let start = Barrier::new(2); let end = Barrier::new(2); @@ -35,15 +42,21 @@ fn death_reuse() { let t1 = scope.spawn(|_scope| { start.wait(); add_new(&seen_unique_ids); - add_new(&seen_std_ids); - add_new(&seen_live_ids); + #[cfg(feature = "std")] + { + add_new(&seen_std_ids); + add_new(&seen_live_ids); + } end.wait(); }); let t2 = scope.spawn(|_scope| { start.wait(); add_new(&seen_unique_ids); - add_new(&seen_std_ids); - add_new(&seen_live_ids); + #[cfg(feature = "std")] + { + add_new(&seen_std_ids); + add_new(&seen_live_ids); + } end.wait(); }); t1.join().unwrap_or_else(|payload| propagate_panic(payload)); @@ -56,9 +69,12 @@ fn death_reuse() { scope .spawn(|_scope| { add_new(&seen_unique_ids); - add_new(&seen_std_ids); - let live_id: LiveThreadId = threadid::current(); - assert!(seen_live_ids.lock().unwrap().contains(&live_id)); + #[cfg(feature = "std")] + { + add_new(&seen_std_ids); + let live_id: LiveThreadId = threadid::current(); + assert!(seen_live_ids.lock().unwrap().contains(&live_id)); + } }) .join() .unwrap_or_else(|payload| propagate_panic(payload));