Initial commit
Some checks failed
Cargo test & clippy / task (push) Has been cancelled

This commit is contained in:
2025-02-03 23:33:53 +01:00
commit d3b3fdf026
9 changed files with 711 additions and 0 deletions

30
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Cargo test & clippy
permissions:
contents: read
pull-requests: write
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
task:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose --all-features
- name: Run clippy
run: cargo clippy --all-features

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

72
Cargo.lock generated Normal file
View File

@@ -0,0 +1,72 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "laika"
version = "0.1.4"
dependencies = [
"tokio",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"pin-project-lite",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"

21
Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "laika"
version = "0.1.4"
edition = "2024"
authors = ["Laika Schmidt <laika.schmidt@magenta.de>"]
description = "A namespace crate containing miscellaneous submodules (like an SPMC channel) of owner its-laika"
repository = "https://github.com/its-laika/crates"
readme = "README.md"
license = "MIT"
keywords = ["channel", "spmc", "oneshot"]
categories = ["concurrency"]
include = ["**/*.rs", "Cargo.toml"]
[lib]
source = "src/lib.rs"
[features]
shotgun = []
[dev-dependencies]
tokio = { version = "1.48", features = ["rt", "macros"] }

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Laika Schmidt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

102
README.md Normal file
View File

@@ -0,0 +1,102 @@
# Laika's namespace crate
**This module contains multiple submodules (included via feature flags) with
different functionalities. They're all grouped under the `laika` namespace,
providing some kind of scoped crates (avoiding naming conflicts).**
## Submodules / Features
## shotgun
_A dead simple one-shot single producer, multiple consumer (SPMC) channel_
### About
Shotgun is a simple one-shot single producer, multiple consumer (SPMC) channel.
It internally uses `std::sync::Mutex` and `std::sync::Arc` and does not contain
any unsafe code.
### When to use
Likely when you need to pass a signal to multiple threads or functions to stop
in order to shut down the application.
### How to use
#### Synchronous
```rust
fn main() {
use laika::shotgun::channel;
use std::thread;
use std::time;
let (tx, rx) = channel();
let rx1 = rx.clone();
let thread1 = thread::spawn(move || loop {
if rx1.try_recv().is_some() {
return 1;
}
thread::sleep(time::Duration::from_secs(1));
});
let rx2 = rx.clone();
let thread2 = thread::spawn(move || loop {
if rx2.try_recv().is_some() {
return 2;
}
thread::sleep(time::Duration::from_secs(1));
});
thread::sleep(time::Duration::from_secs(2));
tx.send(()); // `tx` is dropped here.
assert!(thread1.join().is_ok_and(|v| v == 1));
assert!(thread2.join().is_ok_and(|v| v == 2));
}
```
#### Asynchronous
```rust
#[tokio]
async fn main() {
use laika::shotgun::channel;
use std::thread;
use std::time;
let (tx, rx) = channel();
let rx1 = rx.clone();
let fun1 = async move {
rx1.await;
1
};
let rx2 = rx.clone();
let fun2 = async move {
// Explicit call to recv(), does the same as calling`.await` directly.
rx2.recv().await;
2
};
thread::sleep(time::Duration::from_secs(2));
tx.send(());
let rx3 = rx.clone();
let fun3 = async move {
rx3.await;
3
};
let result = join!(fun1, fun2);
assert_eq!(result.0, 1);
assert_eq!(result.1, 2);
assert_eq!(fun3.await, 3);
}
```
# License
[MIT](LICENSE)

22
SECURITY.md Normal file
View File

@@ -0,0 +1,22 @@
# Security Policy
## Supported Versions
Support is limited to the latest version and also the current state in the main
branch.
| Version | Supported |
| ------------- | ------------------ |
| _main_ | :white_check_mark: |
| 0.1.4 | :white_check_mark: |
| <= 0.1.3 | :x: |
## Reporting a Vulnerability
If you want to report a vulnerability, check if it's possible to create an issue
without endangering other users. If that's not the case, send a mail to the
address on my ([its-laika](https://github.com/its-laika)) profile. I check my
mails pretty frequently so expect a reply within a few days. From there on, I'll
do my best to fix the vulnerability asap. If you want to, you can also provide
patches directly or via pull requests. A new version is pushed as soon as the
vulnerability is fixed.

16
src/lib.rs Normal file
View File

@@ -0,0 +1,16 @@
//! # Laika's namespace crate
//!
//! This module contains multiple submodules (included via feature flags) with
//! different functionalities. They're all grouped under the `laika` namespace,
//! providing some kind of scoped crates (avoiding naming conflicts).
//!
//! ## Submodules / Features
//!
//! ### [`shotgun`]
//!
//! Shotgun is a simple one-shot single producer, multiple consumer (SPMC)
//! channel. It internally uses `std::sync::Mutex` and `std::sync::Arc` and does
//! not contain any unsafe code.
//! See module documentation for more information.
#[cfg(feature = "shotgun")]
pub mod shotgun;

426
src/shotgun.rs Normal file
View File

@@ -0,0 +1,426 @@
#![forbid(unsafe_code)]
//! # A dead simple one-shot single producer, multiple consumer (SPMC) channel
//!
//! Shotgun is a simple oneshot single producer, multiple consumer (SPMC)
//! channel. Internally using [`std::sync::Mutex`] and [`std::sync::Arc`], not
//! containing any unsafe code.
use std::{
clone::Clone,
future::Future,
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll, Waker},
};
/// Oneshot receiver of a [`channel`]
///
/// Use [`Receiver::try_recv`] or [`Receiver::recv`] to (try to) receive a value
/// from the channel, if it has been sent. As this is a oneshot receiver, only
/// one value can be received.
///
/// # Examples
///
/// ## Synchronous
///
/// ```rust
/// let (mut tx, rx) = laika::shotgun::channel();
///
/// // Initialy, oneshot receiver has no value
/// assert_eq!(rx.try_recv(), None);
///
/// // Send a value
/// tx.send(12);
///
/// // Now, oneshot receiver has the value
/// assert_eq!(rx.try_recv(), Some(12));
/// ```
///
/// ## Asynchronous
///
/// ```no_run
/// let (mut tx, rx) = laika::shotgun::channel();
///
/// // ... in any async runtime
///
/// let fun1 = async move {
/// rx.recv().await;
/// return 1;
/// };
///
/// // Send a value
/// tx.send(12);
/// ```
#[derive(Clone, Debug)]
pub struct Receiver<T>
where
T: Clone,
{
/// Inner receiver that holds the sent value and possible wakers
inner: Arc<Mutex<_Receiver<T>>>,
}
/// Oneshot sender of a [`channel`]
///
/// Use [`Sender::send`] to send a value to all receivers of the channel.
/// As this is a oneshot sender, only one value can be sent.
///
/// # Examples
/// ## Send a value
///
/// ```rust
/// let (mut tx, rx) = laika::shotgun::channel();
///
/// // Send a value
/// tx.send(12);
/// ```
///
/// ## Sender is dropped after sending
///
/// ```compile_fail
/// let (mut tx, rx) = laika::shotgun::channel();
///
/// // Send a value
/// tx.send(12);
/// tx.send(13); // This won't compile
/// ```
#[derive(Debug)]
pub struct Sender<T>
where
T: Clone,
{
inner: _Sender<T>,
}
impl<T> Receiver<T>
where
T: Clone,
{
/// Try to receive a value from the channel, if it has been sent.
/// As this is a oneshot receiver, only one value can be received.
/// This function is **non-blocking** and just returns [`None`] if no value
/// has been sent.
///
/// # Panics
///
/// Panics if mutex is poisened due to another thread panicking while using
/// inner receiver too.
///
/// # Examples
/// ```rust
/// let (mut tx, rx) = laika::shotgun::channel();
///
/// // Initialy, oneshot receiver has no value
/// assert_eq!(rx.try_recv(), None);
///
/// // Send a value
/// tx.send(12);
///
/// // Now, oneshot receiver has the value
/// assert_eq!(rx.try_recv(), Some(12));
/// // Value is kept after being received
/// assert_eq!(rx.try_recv(), Some(12));
/// ```
pub fn try_recv(&self) -> Option<T>
where
T: Clone,
{
self.inner
.as_ref()
.lock()
.expect("Mutex is poisoned")
.try_recv()
}
/// Receive a value from the channel.
/// Waits until value has been sent and then returns it.
/// This function is blocking asynchronously.
///
/// # Note
/// You can directly [`Future`]'s `.await` on the receiver too.
///
/// # Examples
/// (*Note that this won't compile because no async runtime exists here.*)
/// ```compile_fail
/// let (mut tx, rx) = laika::shotgun::channel();
///
/// let fun1 = async move {
/// rx.recv().await;
/// return 1;
/// };
///
/// std::thread::sleep(std::time::Duration::from_secs(1));
///
/// // Send a value
/// tx.send(());
///
/// // Now, oneshot receiver has the value
/// assert_eq!(fun1.await, 1);
/// ```
pub async fn recv(self) -> T {
self.await
}
}
impl<T> Sender<T>
where
T: Clone,
{
/// Send a value to all receivers of the channel.
/// As this is a oneshot sender, only one value can be sent.
///
/// # Examples
/// ## Send a value
///
/// ```rust
/// let (mut tx, rx) = laika::shotgun::channel();
///
/// // Send a value
/// tx.send(12);
/// ```
pub fn send(self, value: T) {
self.inner.send(value);
}
}
/// Inner receiver of a [`channel`]
#[derive(Clone, Debug)]
struct _Receiver<T>
where
T: Clone,
{
/// Value that was sent by [`_Sender`]
value: Option<T>,
/// Wakers that will be woken up when value is sent by [`_Sender`]
wakers: Vec<Waker>,
}
/// Inner sender of a [`channel`]
#[derive(Clone, Debug)]
struct _Sender<T>
where
T: Clone,
{
/// [`_Receiver`] instance that will receive the value and is referecend by
/// all [`Receiver`]s.
receiver: Option<Arc<Mutex<_Receiver<T>>>>,
}
impl<T> _Receiver<T>
where
T: Clone,
{
/// Clones the value (if it has been given by [`_Sender`]) and returns clone
/// of it.
fn try_recv(&self) -> Option<T> {
self.value.clone()
}
/// Sets the value to be received by all [`Receiver`]s from [`_Sender`].
fn set(&mut self, value: T) {
self.value = Some(value);
for waker in self.wakers.clone() {
waker.wake();
}
}
}
/// Implement [`Future`] for [`Receiver`] to be able to use it in async
/// functions.
impl<T> Future for Receiver<T>
where
T: Clone,
{
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut inner = self.inner.lock().expect("Mutex is poisoned");
if let Some(value) = &inner.value {
Poll::Ready(value.clone())
} else {
if inner.wakers.iter().all(|w| !w.will_wake(cx.waker())) {
inner.wakers.push(cx.waker().clone());
}
Poll::Pending
}
}
}
impl<T> _Sender<T>
where
T: Clone,
{
/// Send a value to all [`Receiver`]s.
///
/// # Panics
///
/// Panics if mutex is poisened due to another thread panicking while using
/// referenced receiver too.
fn send(self, value: T) {
if let Some(recv) = self.receiver.as_ref() {
recv.lock().expect("Mutex is poisoned").set(value);
}
}
}
/// Creates a one-shot, single producer multiple consumer channel that can be
/// used to send one value to multiple receivers.
///
/// # Examples
///
/// ```rust
/// let (mut tx, rx) = laika::shotgun::channel::<u8>();
/// // do something with tx and rx
/// ```
pub fn channel<T>() -> (Sender<T>, Receiver<T>)
where
T: Clone,
{
let mut sender = Sender {
inner: _Sender { receiver: None },
};
let receiver_ref = Arc::new(Mutex::new(_Receiver {
value: None,
wakers: Vec::new(),
}));
let receiver = Receiver {
inner: receiver_ref.clone(),
};
sender.inner.receiver = Some(receiver_ref);
(sender, receiver)
}
#[cfg(test)]
mod test {
use super::*;
use tokio::task::JoinSet;
#[test]
fn test_basic() {
let (tx, rx) = channel();
assert_eq!(rx.try_recv(), None);
assert_eq!(rx.try_recv(), None);
tx.send(());
assert_eq!(rx.try_recv(), Some(()));
assert_eq!(rx.try_recv(), Some(()));
}
#[test]
fn test_work_without_receiver() {
let (tx, rx) = channel();
assert_eq!(rx.try_recv(), None);
drop(rx);
tx.send(());
}
#[test]
fn test_work_without_sender() {
let (tx, rx) = channel::<()>();
assert_eq!(rx.try_recv(), None);
drop(tx);
assert_eq!(rx.try_recv(), None);
}
#[test]
fn test_work_with_multiple_receivers() {
let (tx, rx) = channel();
let rx1 = rx.clone();
let rx2 = rx.clone();
assert_eq!(rx.try_recv(), None);
assert_eq!(rx1.try_recv(), None);
assert_eq!(rx2.try_recv(), None);
tx.send(1337);
assert_eq!(rx.try_recv(), Some(1337));
assert_eq!(rx1.try_recv(), Some(1337));
assert_eq!(rx2.try_recv(), Some(1337));
}
#[test]
fn test_works_in_threads() {
use std::thread;
use std::time;
let (tx, rx) = channel();
let rx1 = rx.clone();
let thread1 = thread::spawn(move || loop {
if rx1.try_recv().is_some() {
return 1;
}
thread::sleep(time::Duration::from_secs(1));
});
let rx2 = rx.clone();
let thread2 = thread::spawn(move || loop {
if rx2.try_recv().is_some() {
return 2;
}
thread::sleep(time::Duration::from_secs(1));
});
thread::sleep(time::Duration::from_secs(2));
tx.send(());
assert!(thread1.join().is_ok_and(|v| v == 1));
assert!(thread2.join().is_ok_and(|v| v == 2));
}
#[tokio::test]
async fn test_recv() {
use std::thread;
use std::time;
let (tx, rx) = channel();
let mut join_set = JoinSet::new();
let rx1 = rx.clone();
join_set.spawn(async move {
rx1.await;
1
});
let rx2 = rx.clone();
join_set.spawn(async move {
rx2.recv().await; // Explicit call to recv
2
});
thread::sleep(time::Duration::from_secs(2));
tx.send(());
let rx3 = rx.clone();
let fun3 = async move {
rx3.await;
3
};
let result = join_set.join_all().await;
assert_eq!(result[0], 1);
assert_eq!(result[1], 2);
assert_eq!(fun3.await, 3);
}
}