From Boost.Signals2​

Boost.Signals2 and jai::signal share a model: a typed multicast event, callables connected to it, every receiver run in connection order when it fires. Most of a migration is spelling.

Three defaults differ:

  • only the owner of a private emitter can emit;
  • a connection lives exactly as long as someone holds its handle;
  • an emitter takes no lock unless its type says so, and then the lock is held while receivers run.

The framework is documented in Signals & Observable Properties. The last section has the notes for Qt and sigslot code.

The concept map​

Headers: <jaiscript/signals/signal_decl.hpp> where signal members are only declared, <jaiscript/signals/signal.hpp> in a translation unit that emits (an emit instantiates the script-receiver path, which that header defines), and <jaiscript/signals/receiver_owner.hpp> for receiver_owner.

Boost.Signals2JaiScript
boost::signals2::signal<void(int)> onOpen;, a public membera private jai::signal_emitter<void(int)> onOpenSignal; and a public jai::signal<void(int)> onOpen; built from it in the constructor (onOpen(onOpenSignal)); in a property_owner, JAI_SIGNAL_PROPERTY(void(int), onOpen) declares the pair
onOpen(42);, from anywhereonOpenSignal(42); or onOpenSignal.emit(42);, from the owner
boost::signals2::connection c = onOpen.connect(f);auto c = onOpen.connect(f);, a jai::signal<void(int)>::shared_receiver_type (a std::shared_ptr to the receiver), [[nodiscard]]
c.disconnect();c = nullptr; or onOpen.disconnect(c);
c.connected()c->connected()
boost::signals2::scoped_connectionthe default: the handle disconnects when it is destroyed
slot_type(f).track(p), .track_foreign(p)the tracked object holds the handle: a member, or a jai::receiver_owner (built into every property_owner as track(...))
boost::signals2::shared_connection_block block(c);c->block(); ... c->unblock(); (counted, so blocks nest); block() / unblock() on the emitter blocks every receiver
onOpen.disconnect_all_slots()onOpenSignal.clear()
onOpen.num_slots(), onOpen.empty()onOpen.observer_count(), onOpen.observer_count() == 0
onOpen.disconnect(f) by slot, onOpen.disconnect(group)by handle, onOpen.disconnect(c), or by name: onOpen.connect("hud", f) and onOpen.disconnect("hud")
groups, at_front / at_backno equivalent: receivers run in the order they connected
a combiner; the default returns an optional of the last slot's resultno equivalent: emit returns nothing. Receivers write into an accumulator passed by reference, and a bool signature has emit_while (Combiners)
a slot connected during an emit, first called by the next emitthe same by default; onOpen.connect_immediate(f) also joins the emits already running
connect_extended, whose slot receives its own connectionthe callback captures a variable that holds its handle; connect_oneshot(f) for a receiver that fires once
keywords::mutex_type<dummy_mutex> for one signal typethe default: jai::signal_emitter<void(int)> takes no lock. jai::signal_emitter<void(int), jai::signal_synchronized> with a jai::signal<void(int), jai::signal_synchronized> facade (or JAI_SYNCHRONIZED_SIGNAL_PROPERTY) locks (Threads)
boost::signals2::deconstruct, postconstructornot needed: the object that connects holds the handle, so nothing has to run after construction

A class converted​

A door that announces how far it opened, a HUD that listens while it exists, and a log that listens until it is told to stop:

C++ (Boost.Signals2)
#include <boost/signals2.hpp>

class Door {
public:
    boost::signals2::signal<void(int)> onOpen;
    void open(int width) { onOpen(width); }
};

class Hud {
public:
    explicit Hud(Door& door) {
        connection = door.onOpen.connect([this](int width) { show(width); });
    }
    void show(int width) { std::cout << "HUD: door opened to " << width << "\n"; }
private:
    boost::signals2::scoped_connection connection;
};

Door door;
boost::signals2::connection logged = door.onOpen.connect([](int width) { std::cout << "log: " << width << "\n"; });
{
    Hud hud(door);
    door.open(42);
}
door.open(7);
logged.disconnect();
door.open(1);

In JaiScript, Hud changes only the type of its member, to jai::signal<void(int)>::shared_receiver_type:

C++ (JaiScript)
#include <jaiscript/signals/signal.hpp>

class Door {
private:
    jai::signal_emitter<void(int)> onOpenSignal;
public:
    jai::signal<void(int)> onOpen;
    Door() : onOpen(onOpenSignal) {}
    void open(int width) { onOpenSignal(width); }
};

Door door;
auto logged = door.onOpen.connect([](int width) { std::cout << "log: " << width << "\n"; });
{
    Hud hud(door);
    door.open(42);
}
door.open(7);
logged = nullptr;
door.open(1);
output (both)
log: 42
HUD: door opened to 42
log: 7

The emitter is declared before the facade so it is constructed first. door.onOpen(5) outside Door does not compile.

Tracking and blocking​

Boost ties a connection to an object by having the slot watch it: track (or track_foreign for a std::shared_ptr) ends the connection when the object expires and keeps the object alive while its slot runs. In JaiScript the object holds its handles, and its death ends them. One jai::receiver_owner holds receivers of any signature:

C++ (Boost.Signals2)
auto panel = std::make_shared<Panel>();
using slot_type = boost::signals2::signal<void(int)>::slot_type;
door.onOpen.connect(slot_type([p = panel.get()](int width) { p->show(width); }).track_foreign(panel));
C++ (JaiScript)
#include <jaiscript/signals/receiver_owner.hpp>

class Panel {
public:
    void watch(Door& door) { connections.track(door.onOpen.connect([this](int width) { show(width); })); }
    void show(int width) { std::cout << "panel: " << width << "\n"; }
private:
    jai::receiver_owner connections;
};

auto panel = std::make_shared<Panel>();
panel->watch(door);
door.open(3);       // panel: 3
panel.reset();      // the receivers die with the panel
door.open(4);       // nothing; observer_count() is 0

A receiver stays alive for the length of its own call, but nothing pins an object its callback captured: a callback that can outlive what it captures holds a std::weak_ptr and locks it.

A shared_connection_block is a scope; a receiver's block() and unblock() are calls, counted the way Boost counts blocks. The emitter has its own pair: unblock() returns true when the signal fired while it was blocked, and set_blocked_callback(f) runs f in place of the receivers during a block.

Combiners and return values​

A JaiScript signature may return a value, but emit and operator() return nothing: each receiver's result is discarded. Boost hands the results to a combiner, and the default returns an optional holding the last slot's result:

C++ (Boost.Signals2)
boost::signals2::signal<int(int)> modifiers;
modifiers.connect([](int base) { return base + 2; });
modifiers.connect([](int base) { return base * 3; });
boost::optional<int> last = modifiers(10);    // 30

A result every receiver contributes to travels as a reference parameter:

C++ (JaiScript)
jai::signal_emitter<void(int, int&)> modifiers;
auto add = modifiers.connect([](int base, int& total) { total += base + 2; });
auto triple = modifiers.connect([](int base, int& total) { total += base * 3; });
int total = 0;
modifiers(10, total);                          // total is 42

A veto is the one built-in aggregate. emit_while, available when the signature returns bool, stops at the first receiver that returns false and returns false, otherwise true (a blocked signal returns true without running any). In Boost that is a combiner that stops iterating at the first false.

Behavior that changes​

Only the owner fires​

Whoever can reach a Boost signal can both connect and call it, so a class that keeps emission to itself keeps the signal private and writes a forwarding connect. JaiScript splits the roles into two types: signal_emitter<Sig> connects and emits, and signal<Sig> is a facade over a reference to an emitter with only the connecting half (connect, connect_oneshot, disconnect, connected, observer_count). Script holds the same line: a facade read from script has no emit, and script fires a signal only when the host binds the emitter itself.

The facade holds a reference, so a copy of it is valid only while the owning object lives.

The handle is the connection​

connect returns a shared_ptr to the receiver, and the emitter keeps only a weak reference. Reset the pointer, or let the member holding it die, and the receiver stops firing; the emitter drops the dead entry after its next emit.

A Boost connection outlives its connection handle until someone calls disconnect or the signal is destroyed, so Boost code often ignores what connect returns. Here a discarded handle disconnects at once and the receiver never runs, which is why the unnamed connect overloads are [[nodiscard]].

A connection that should belong to the signal, as a Boost connection does, is a named connection:

  • connect("id", f) returns a handle nobody has to keep; the connection lasts until disconnect("id"), clear() or the emitter's death.
  • Connecting again under an id already connected takes the name over: the earlier receiver stops firing unless C++ still holds its handle.
  • Script connects only this way.
  • If the emitter dies first, a handle stays safe to hold and its connected() reports false, as a Boost connection does.

Connecting and disconnecting during an emit​

The libraries agree by default. An emit runs the receivers connected when it started, in order, and skips each one that has left by the time it reaches it:

  • A receiver connected during an emit runs from the next emit on. An emit a receiver starts after the connect (a re-emit of the same signal) is a new emit and runs the new receiver; the emit that was running does not. A receiver that connects another every time it fires therefore cannot keep an emit going.
  • A receiver disconnected during an emit does not run later in it, however it left: disconnect(c), disconnect("id"), clear(), its last handle dropped (a connect that takes a name over drops the emitter's handle to the receiver it replaces), or a connect of the same receiver again, which moves it to the end of the list as a new connection.

connect_immediate has no Boost counterpart. It takes the same arguments as connect (a callable, script text, a receiver handle, or a name with any of those), and the receiver also joins every emit already running, the one in progress and each emit it is nested in, after the receivers those emits started with. Script connects with plain connect. A receiver may connect, disconnect or re-emit the same signal while it fires.

Receivers that throw​

The libraries agree: an exception from a receiver propagates out of the emit and the receivers after it do not run. That holds for script receivers too, and the emitter stays consistent as the exception unwinds, so the emit site can catch it and fire again (When a receiver fails).

Threads​

Boost.Signals2 locks internally, so connect, disconnect and invocation can run on different threads at once, and it runs slots without holding the signal's own lock. A JaiScript emitter's threading is part of its type, chosen per signal as Boost's mutex_type is, with the opposite default:

  • signal_emitter<Sig> takes no lock and one thread at a time uses it: Boost's dummy_mutex.
  • signal_emitter<Sig, jai::signal_synchronized> holds one recursive mutex through every call and the whole of an emit, receivers included (Threads has the full guarantee).

A Boost signal that several threads use becomes a synchronized emitter, whose emits from different threads run one after another. A receiver must not wait on another thread that is connecting to or emitting the same signal: Boost code that does this without deadlocking deadlocks here.

What Boost has no counterpart for​

A receiver can be script: source text or a script function, connected from C++ or from script, and it keeps working across hot reload (Script receivers and hot reload).

A signal declared with JAI_SIGNAL_PROPERTY saves its script-text receivers with the object that owns it and reconnects them on load (Connections that save), so a host reconnects only its C++ callbacks after a load. A saved receiver is script source that runs on the next emit: load an object with signal properties only from a trusted source, or on an engine under the safety rails.

From Qt and sigslot libraries​

QtJaiScript
Declared asa signals: member function that moc implements, on a QObjecta private signal_emitter<Sig> and a public signal<Sig> built from it
Who can emitanyone who can reach the object; keeping emission inside the class is a conventionthe owner of the emitter
connect returnsa QMetaObject::Connection handle; dropping it leaves the connection in placeshared_receiver_type; dropping it disconnects
Tied to a receiving objectautomatic: destroying the sender, the receiver or a functor's context object disconnects; a functor with no context object lives as long as the senderthe object holds the handle (a member, receiver_owner, track(...))
Script receiversJavaScript handlers through the QML engine, a separate mechanism from C++ slotsscript text and script functions on the same emitter as C++ receivers
Saved with the objectnoscript-text receivers, named and unnamed (JAI_SIGNAL_PROPERTY)
A receiver throwsundefined behavior unless the slot catches itout of the emit; later receivers do not run
Threadsdirect connections run on the emitting thread; a queued connection copies the arguments into an event and runs the slot from the receiver thread's event loop, and the default automatic connection queues when the receiver lives in another threadreceivers run on the emitting thread, with no queued delivery: a host that emits from a worker and wants receivers on its main thread hands the emit over itself. Locking is chosen per signal (Threads)
Return valuessignals are documented as voidnone from emit; emit_while stops at the first false

Sigslot libraries. Lightweight header-only sigslot libraries usually take Boost's shape with fewer features: a signal object anyone can emit, connect returning a connection handle (often with a scoped variant), lifetime tracking through an observer base class or a weak pointer, and thread safety chosen by a template or a compile-time switch. Where this page says Boost, read that family too: an observer base class becomes a receiver_owner member, and a scoped connection becomes the plain handle.