A thread-safe signal/slot library based on boost::signals2

Overview

About

signals2 is a thread-safe signal/slot library based on the boost::signals2 C++ library. Signals are objects that contain a list of callback functions ("slots") to be executed when the signal is "emitted". Signals and their corresponding slots can be managed through the use of connections and shared connection blocks.

signals2 contains no unsafe code and compiles on stable Rust 1.53.

signals2 is distributed under the Boost Software License, Version 1.0.

Basic usage

These basic use patterns should cover most usage scenarios.

Creating signals and connecting slots

The follwing example creates a signal with slot functions that take no arguments and do not return anything. The () argument given to the Signal template indicates the type of parameters that the slot functions will accept. In this case the slots do not accept any parameters.

use signals2::*; 

let sig: Signal<()> = Signal::new();
sig.connect(|| println!("Hello, world!"));
sig.emit(); // prints "Hello, world!"

As a more complex example we create a signal with slots that accept two arguments.

let sig: Signal<(i32, i32)> = Signal::new();
sig.connect(|x, y| println!("x + y = {}", x + y));
sig.emit(2, 3); // prints "x + y = 5"

Special care must be taken when creating signals with slots that accept only one parameter. The single parameter type must still be represented as a tuple in the type signature of the signal. The Rust compiler does not recognize (T) as a tuple-type of arity one, rather it recognizes it as simply the type T. A comma must be used to force the Rust compiler to recognize it as a tuple, e.x. (T,)

let sig: Signal<(i32,)> = Signal::new(); // Note that using Signal<(i32)> or Signal<i32> will not compile!
sig.connect(|x| println!("x = {}", x));
sig.emit(7); // prints "x = 7"

Slots can return values, and the return value of calling emit on a slot is determined by the signal's combiner. By default emit will simply return an Option<R> representing the value returned by the last slot that was executed. If no slots were executed None will be returned.

let sig: Signal<(i32, i32), i32> = Signal::new();
assert_eq!(sig.emit(2, 3), None); // no slots have been added yet
sig.connect(|x, y| x + y);
assert_eq!(sig.emit(2, 3), Some(5));

Cloning a signal will result in a new signal that "points" to the same underlying set of slots as the original signal. Any modifications made to the cloned signal will affect the original signal, and vice versa.

let sig1: Signal<()> = Signal::new();
let sig2 = sig1.clone();
sig1.connect(|| println!("Hello, world!")); // connect to the first signal
sig2.emit(); // prints "Hello, world!" because sig1 and sig2 share the same set of slots.

Using connections to manage slots

Connections are used to manage the lifetime of a slot. When a new slot is connected to a signal a corresponding connection is created. A connection manages the lifetime of exactly one slot for some particular signal. Connections can be used to disconnect slots from their signals.

let sig: Signal<()> = Signal::new();
let conn = sig.connect(|| println!("Hello, world!"));
conn.disconnect(); // disconnect the slot
sig.emit(); // prints nothing, the slot has been disconnected

A single slot may have an arbitrary number of connections associated with it. More connections for a slot may be created by calling Clone on existing connections. Note that cloning a connection creates a new connection that "points" to the same slot. Disconnecting one connection to a slot will disconnect all other connections to that slot. Once a slot has been disconnected it is permanently removed from the signal and cannot be reconnected.

let sig: Signal<()> = Signal::new();
let conn1 = sig.connect(|| println!("Hello, world!"));
let conn2 = conn1.clone();

conn2.disconnect();
assert!(conn1.connected() == false); // disconnecting conn2 has also disconnected conn1
assert!(conn2.connected() == false);

sig.emit(); // prints nothing, the slot has been disconnected.

Connections do not automatically disconnect when they are dropped. If you wish to have a connection that disconnects itself automatically when dropped, use a scoped connection. Regular connections can be converted into scoped connections by calling scoped() on them.

let sig: Signal<()> = Signal::new();
{
    let _conn = sig.connect(|| println!("Hello, world!")).scoped(); // create a scoped connection
    sig.emit(); // prints "Hello, world!"
} // _conn is dropped and disconnects

sig.emit(); // prints nothing, the slot has been disconnected.

Weak signals

A slot function may need to have access to its own signal, for example in the case where a slot wishes to recursively emit its own signal or connect a new slot to the signal. To accomplish this, an initial instinct may be to clone the signal and then move the cloned signal into a closure that is then connected to the original signal as in the following example. This will cause a memory leak.

let sig: Signal<()> = Signal::new();
let sig_clone = sig.clone();
sig.connect(move || { // memory leak!
    sig_clone.connect(|| println!("Hello, world!"));
});

sig.emit(); // prints nothing (see the "Concurrency" section for why nothing is printed here)
sig.emit(); // prints "Hello, world!" once
sig.emit(); // prints "Hello, world!" twice
// etc...

Signals maintain ownership over their slots, so therefore a slot cannot also have ownership over its own signal. A weak signal can be used to break this circular dependency.

let sig: Signal<()> = Signal::new();
let weak_sig = sig.weak();
sig.connect(move || { // no memory leak!
    weak_sig.upgrade().unwrap().connect(|| println!("Hello, world!"));
});

sig.emit(); // prints nothing
sig.emit(); // prints "Hello, world!" once
sig.emit(); // prints "Hello, world!" twice
// etc...

Advanced usage

Less common usage patterns.

Shared connection blocks

A user may wish to temporarily block a slot from executing without permanently disconnecting the slot. Shared connection blocks can be used to accomplish this. There can be an arbitrary number of shared connection blocks for any particular slot. If any of the shared connection blocks are blocking the slot, that slot will not be executed when the signal is emitted.

let sig: Signal<(), i32> = Signal::new();
let conn = sig.connect(|| 4);
assert_eq!(sig.emit(), Some(4));

let blocker1 = conn.shared_block(true); // blocking
let blocker2 = blocker1.clone(); // also blocking, since blocker1 is blocking

assert_eq!(conn.blocker_count(), 2);
assert_eq!(sig.emit(), None); // slot is blocked and will not execute 

blocker1.unblock();
assert_eq!(conn.blocker_count(), 1);
assert_eq!(sig.emit(), None); // slot is still blocked

blocker2.unblock();
assert_eq!(conn.blocker_count(), 0);
assert_eq!(sig.emit(), Some(4)); // no more active blockers

Shared connection blocks automatically unblock themselved when dropped.

let sig: Signal<(), i32> = Signal::new();
let conn = sig.connect(|| 4);
assert_eq!(sig.emit(), Some(4));
{
    let _blocker = conn.shared_block(true);
    assert_eq!(conn.blocker_count(), 1);
    assert_eq!(sig.emit(), None);
}

assert_eq!(conn.blocker_count(), 0);
assert_eq!(sig.emit(), Some(4)); // blocker was dropped

Using ConnectHandles and EmitHandles to limit access to a signal

There may be cases where it is undesireable to allow public access to a signal while still needing to connect new slots to the signal or emit the signal. For example, consider a library with a struct that has a public signal member. The desired programming pattern may be for users of the library to connect slots to the struct's signal while the struct periodically emits its own signal. But if the signal is a public member, this gives users of the library full access to the signal's API, including the ability to emit the signal. This is problematic if the struct itself should be the only one with permission to emit the signal. The same problem can happen in reverse: a struct with a public signal that library users should be able to emit, but not be able to connect slots to or disconnect slots from.

The solution to these two problems are ConnectHandles and EmmitHandles. A ConnectHandle is an object that allows new slots to be added to a signal while not allowing the signal to be emitted. Likewise, an EmitHandle is an object that allows a signal to be emitted without allowing slots to be connected or disconnected. Below is an example of using a ConnectHandle.

let sig: Signal<(), i32> = Signal::new();
let connect_handle = sig.get_connect_handle();
let conn = connect_handle.connect(|| 1);
assert!(conn.connected());
assert_eq!(sig.emit(), Some(1));

std::mem::drop(sig);
let conn = connect_handle.connect(|| 2);
assert!(!conn.connected());

An example of using an EmitHandle.

let sig: Signal<(), i32> = Signal::new();
let emit_handle = sig.get_emit_handle();
sig.connect(|| 1);
assert_eq!(emit_handle.emit(), Some(Some(1)));

std::mem::drop(sig);
assert_eq!(emit_handle.emit(), None);

Custom combiners

The return value of calling emit on a signal is determined by the combiner type of the signal. The default combiner simply returns an Option that represents the return value of the last slot to be executed (None in the case where no slot was executed). A custom combiner can be created by implementing the Combiner<R> trait on some type. Note that consuming the iterator passed to the combine() function of the Combiner trait is what causes slots to execute. A slot is executed each time the next function is called on the iterator.

#[derive(Default)]
struct ProductCombiner {}

impl Combiner<i32> for ProductCombiner {
    type Output = i32;

    /// ProductCombiner computes the product of all values returned by the slots
    fn combine(&self, iter: impl Iterator<Item=i32>) -> Self::Output {
        iter.product()
    }
}

let sig: Signal<(), i32, ProductCombiner> = Signal::new();

sig.connect(|| 2);
sig.connect(|| 3);
sig.connect(|| 4);

assert_eq!(sig.emit(), 24);

Controlling the order in which slots execute using groups and positions

Internally, a signal stores its slots in groups. Groups of slots are ordered, and groups with higher precedence are executed first. By default there exist two "unnamed" groups of slots. These groups are referred to as the "front" group and the "back group". The "front" group of slots will always be executed before all other groups of slots are executed. The "back" group of slots will always be executed after all other groups of slots are executed. Any number of "named" groups may be created, and they are executed according to their ordering. Named groups will always be executed after the "front" group and before the "back" group.

Additionally, when a slot is connected to a group it can be connected at either the Front or Back of the group. When a slot is connected to the front of a group it is guaranteed to execute before all other slots that were present in the group when it was connected. When a slot is connected to the back of a group it is guaranteed to execute after all other slots that were present in the group when it was connected.

The following example shows how to use the Group and Position enums to control the order in which slots are executed.

let sig: Signal<()> = Signal::new();

sig.connect_group_position(|| println!("World!"), Group::Front, Position::Front);
sig.connect_group_position(|| print!("Hello,"), Group::Front, Position::Front);

sig.emit(); // prints "Hello, world!"

A more complex example using named groups.

let sig: Signal<()> = Signal::new();

sig.connect_group_position(|| print!("lazy "), Group::Back, Position::Back);
sig.connect_group_position(|| print!("brown "), Group::Named(5), Position::Back);
sig.connect_group_position(|| print!("over "), Group::Named(7), Position::Back);
sig.connect_group_position(|| print!("the "), Group::Front, Position::Back);
sig.connect_group_position(|| print!("jumps "), Group::Named(7), Position::Front);
sig.connect_group_position(|| println!("dog"), Group::Back, Position::Back);
sig.connect_group_position(|| print!("the "), Group::Named(7), Position::Back);
sig.connect_group_position(|| print!("quick "), Group::Named(-8), Position::Back);
sig.connect_group_position(|| print!("fox "), Group::Named(5), Position::Back);

sig.emit(); // prints "the quick brown fox jumps over the lazy dog"

Named groups are ordered according to the Ord trait implemented for their underlying type. By default their underlying type is i32. This can be changed to any other type that implements the Ord trait by simply specifying the type in the signal's type. For example, named groups may be identified by Strings and sorted lexographically.

use signals2::*;
use combiner::DefaultCombiner;

let sig: Signal<(), (), DefaultCombiner, String> = Signal::new();

Examples in the basic usage section just use the connect function rather than connect_position_group. Using connect(f) is identical to using connect_position_group(f, Group::Back, Position::Back). There is also a connect_group(f, group) function that allows only a group to be specified. It is equivalent to connect_position_group(f, group, Position::Back). Likewise there is a connect_position(f, position) function that allows only a position to be specified. It is equivalent to connect_position_group(f, Group::Back, position).

Extended slots

In the basic usage section we saw how a slot can use a weak signal to maintain a persistant reference to its own signal. This is useful in the case where a slot function may need to recursivey emit its own signal or connect new slots to the signal. However, what if a slot needs to be able to disconnect/block itself? A connection is required to disconnect a signal. How can a slot access its own connection? An attempt such as the one below will fail.

let sig: Signal<(i32,)> = Signal::new();
let weak_sig = sig.weak();

let conn = sig.connect(move |x| {
    if x == 5 {
        println!("Slot recursively called itself 5 times! Disconnecting now.");
        conn.disconnect(); // compiler error
    }

    weak_sig.upgrade().unwrap().emit(x + 1);
});

sig.emit(0);

The issue is that a slot function cannot caputure its own connection because its connection is not created until it is actually connected to the signal. This problem can be solved by using "extended slots". Extended slots are similar to regular slots except that their functions take one more parameter: a connection.

let sig: Signal<(i32,)> = Signal::new();
let weak_sig = sig.weak();

sig.connect_extended(move |conn, x| { // the connection is passed in as the first parameter to the slot
    if x == 5 {
        println!("Slot recursively called itself 5 times! Disconnecting now.");
        conn.disconnect();
    }

    weak_sig.upgrade().unwrap().emit(x + 1);
});

sig.emit(0);

There are corresponding connect_position_extended, connect_group_extended, and connect_position_group_extended functions as well.

Concurrency

Signals are thread safe and may be shared between threads (provided the slot functions, combiner, and group types are threadsafe). A signal may be emitted concurrently (i.e. two or more threads may emit the signal simultaneously). A signal may have a new slot connected to it while it is currently emitting. Neither of these scenarios will result in a deadlock. The internal RWLock of a signal will never deadlock regardless of how many different threads are using the signal or how many times it is recursively emittied. There is, however, some subtley when it comes to modifying a signal while it is emitting.

It is possible (and safe) to modify a signal while it is in the process of emitting. The question is: will modifications made to a signal while it is emitting be "visible" to the currently emitting slots? The answer is: it depends. Blocking/disconnecting a slot while a signal is emitting will be visible. The newly blocked/disconnected slot will not be executed (provided that the slot has not yet started executing). However, connecting a new slot to a signal or changing its combiner while it is emitting are changes that will not be visible to currently emitting slots. Consider the following example.

let sig: Signal<()> = Signal::new();
let weak_sig = sig.weak();
sig.connect(move || {
    weak_sig.upgrade().unwrap().connect(|| println!("Hello, world!"));
});

sig.emit(); // prints nothing
sig.emit(); // prints "Hello, world!" once
sig.emit(); // prints "Hello, world!" twice
// etc...

In this example, the first call to emit will not print anything even though a new slot is connected while the signal is emitting. The newly connected slot will not be executed until the next time the signal is emitted.

You might also like...
🗄️ A simple (and safe!) to consume history of Client and Studio deployment versions.

🗄️ Roblox Version Archive A simple (and safe!) to consume history of Client and Studio deployment versions. About Parsing Roblox's DeployHistory form

A comprehensive collection of resources and learning materials for Rust programming, empowering developers to explore and master the modern, safe, and blazingly fast language.

🦀 Awesome Rust Lang ⛰️ Project Description : Welcome to the Awesome Rust Lang repository! This is a comprehensive collection of resources for Rust, a

nvim-oxi provides safe and idiomatic Rust bindings to the rich API exposed by the Neovim text editor.

đź”— nvim-oxi nvim-oxi provides safe and idiomatic Rust bindings to the rich API exposed by the Neovim text editor. The project is mostly intended for p

The safe, fast and sane package manager for Linux

moss-rs A rewrite of the Serpent OS tooling in Rust, enabling a robust implementation befitting Serpent and Solus We will initially focus on moss and

More than safe rust abstractions over rytm-sys, an unofficial SDK for writing software for Analog Rytm running on firmware 1.70.
More than safe rust abstractions over rytm-sys, an unofficial SDK for writing software for Analog Rytm running on firmware 1.70.

rytm-rs More than safe rust abstractions over rytm-sys, an unofficial SDK for writing software for Analog Rytm running on firmware 1.70. On top of CC

Simple, safe way to store and distribute tensors

Safetensors Ruby 🙂 Simple, safe way to store and distribute tensors Supports Torch.rb and Numo Installation Add this line to your application’s Gemfi

A fast, powerful, and safe interpreter written in rust!

Kraber A fast, powerful, and safe programming language written in rust! About It packs a punch like the Kraber .50-Cal. Kraber is designed to be minim

A safe and idiomatic wrapper over shared memory APIs in rust with proper cleanups.

shmem-bind A safe and idiomatic wrapper over shared memory APIs in rust with proper cleanups. Quick start: check the message-passing example for bette

Fast Symbol Ranking based compressor. Based on the idea of Matt Mahoney's SR2

Fast Symbol Ranking based compressor. Based on the idea of Matt Mahoney's SR2

Comments
  • impl Default for Signal

    impl Default for Signal

    Would adding the following be a bad idea?

    impl<Args, R, C, G> Default for Signal<Args, R, C, G>
    where 
        Args: Clone + 'static,
        R: 'static,
        C: Combiner<R> + Default + 'static,
        G: Ord + Send + Sync + 'static
    {
        /// Creates a default signal with a [Combiner] created by calling `C::default()`.
        fn default() -> Self {
            Self::new_with_combiner(C::default())
        }
    }
    
    opened by klmurphy72 1
Releases(v0.3.2)
Thread-safe cell based on atomic pointers to externally stored data

Simple thread-safe cell PtrCell is an atomic cell type that allows safe, concurrent access to shared data. No std, no data races, no nasal demons (UB)

Nikolay Levkovsky 3 Mar 23, 2024
MollySocket allows getting signal notifications via UnifiedPush.

MollySocket This software is still in alpha. MollySocket allows getting signal notifications via UnifiedPush. It works like a linked device, which doe

null 6 Dec 31, 2022
Another approach to thread stack spoofing.

Description This Twitter thread inspired the creation of this tool. Unwinder is a PoC of how to parse PE's UNWIND_INFO structs in order to achieve "pr

Kurosh Dabbagh Escalante 132 Jan 6, 2023
Wrapper over MMTk to simplify integration with runtimes: thread management system, object header, root scanning

vmkit A library which provides bunch of building blocks to make a VM in Rust. Feautures MMTK integration out of the box Thread management provided by

null 3 Aug 31, 2024
a Rust library implementing safe, lightweight context switches, without relying on kernel services

libfringe libfringe is a library implementing safe, lightweight context switches, without relying on kernel services. It can be used in hosted environ

edef 473 Dec 28, 2022
An abstract, safe, and concise color conversion library for rust nightly This requires the feature adt_const_params

colortypes A type safe color conversion library This crate provides many methods for converting between color types. Everything is implemented abstrac

Jacob 13 Dec 7, 2022
Next-generation, type-safe CLI parser for Rust

Next-generation, type-safe CLI parser for Rust

0918nobita 19 Jul 20, 2022
Safe OCaml-Rust Foreign Function Interface

ocaml-rust This repo contains code for a proof of concept for a safe OCaml-Rust interop inspired by cxx. This is mostly optimized for calling Rust cod

Laurent Mazare 23 Dec 27, 2022
Safe Unix shell-like parameter expansion/variable substitution via cross-platform CLI or Rust API

Safe Unix shell-like parameter expansion/variable substitution for those who need a more powerful alternative to envsubst but don't want to resort to

Isak Wertwein 4 Oct 4, 2022
A safe, fast, lightweight embeddable scripting language written in Rust.

Bud (budlang) A safe, fast, lightweight embeddable scripting language written in Rust. WARNING: This crate is not anywhere near being ready to publish

Khonsu Labs 13 Dec 27, 2022