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 choices differ, and code that relied on Boost's defaults needs attention at each: the right to emit belongs to the owner of a private emitter, a connection lives exactly as long as someone holds its handle, and an emitter takes no lock unless its owner declares it synchronized, in which case the lock is held while receivers run. JaiScript adds what a pure C++ library has no use for: script receivers, and connections that ride a save file. This page maps every Boost type and call, converts a class, and then walks through the behavior that changes. The signal framework itself is documented in Signals & Observable Properties; the last section carries the notes for Qt and sigslot code.
The concept map
Headers: <jaiscript/signals/signal_decl.hpp> where signal members are only declared, and <jaiscript/signals/signal.hpp> in a translation unit that emits (an emit instantiates the script-receiver path, which that header defines). <jaiscript/signals/receiver_owner.hpp> holds receivers of any signature.
| Boost.Signals2 | JaiScript |
|---|---|
boost::signals2::signal<void(int)> onOpen;, a public member | a 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 anywhere | onOpenSignal(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_connection | the 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_back | no equivalent: receivers run in the order they connected |
a combiner; the default returns an optional of the last slot's result | no 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 emit | the same by default; onOpen.connect_immediate(f) also joins the emits already running |
connect_extended, whose slot receives its own connection | the 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 type | the default: jai::signal_emitter<void(int)> takes no lock. The policy is a per-signal compile-time choice, as mutex_type is: 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, postconstructor | not 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:
#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);#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); }
};
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:
jai::signal<void(int)>::shared_receiver_type connection;
};
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);Both print:
log: 42
HUD: door opened to 42
log: 7The emitter moved behind the class's own wall, the scoped_connection became the plain handle, and the explicit disconnect() became dropping the handle. The emitter is declared before the facade so it is constructed first, and the facade is the only thing the class shows: 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. JaiScript turns this around: the object holds its handles, and its death ends them. A class that connects to several signals of different signatures keeps one jai::receiver_owner:
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));#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 0JaiScript keeps a receiver alive for the length of its own call, but nothing pins an object its callback captured, so a callback that can outlive what it captures captures 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, so a receiver blocked twice runs again after the second unblock(). 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, an iterator range over the slots' results that invokes each slot as it is dereferenced; the default returns an optional holding the last slot's result:
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); // 30A result every receiver contributes to travels as a reference parameter, which reaches each receiver in turn:
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 42A veto is the one built-in aggregate. emit_while, available when the signature returns bool, runs the receivers in order, stops at the first 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
A Boost signal is its whole interface: whoever can reach the object can both connect and call operator(), so a class that wants to keep emission to itself keeps the signal private and writes its own forwarding connect. JaiScript splits the two roles into two types. signal_emitter<Sig> connects and emits; signal<Sig> is a facade over a reference to an emitter with only the connecting half (connect, connect_oneshot, disconnect, connected, observer_count). The owner keeps the emitter private and builds the public facade from it in its constructor, so the compiler enforces who can fire. Script holds the same line: a facade member 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. The connection lives exactly as long as someone holds that pointer: reset it, or let the member holding it die, and the receiver stops firing; the emitter drops the dead entry after its next emit. Boost defaults the other way: a connection outlives its connection handle until someone calls disconnect or the signal is destroyed. The habit that breaks is the discarded result. Boost code often ignores what connect returns and expects the slot to stay; the unnamed connect overloads are [[nodiscard]] because a discarded handle disconnects at once, and the receiver never runs.
A connection that should belong to the signal, the way a Boost connection does, is a named connection: connect("id", f) returns the handle without requiring anyone to keep it, and the connection lasts until disconnect("id"), clear() or the emitter's death. Connecting again under an id that is 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
Here the libraries agree by default. Boost invokes the slots connected when the emit started and checks each as it reaches it; a JaiScript emit runs the receivers connected when it started, in order, and skips each one that has left by the time the emit reaches it:
- A receiver connected during an emit runs from the next emit on. An emit that a receiver starts after the connect, a re-emit of the same signal say, is a new emit, so it 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 is the one departure, and it 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 it connects 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
Here the libraries agree: an exception from a receiver propagates out of the emit and the receivers after it do not run. JaiScript holds every kind of receiver to that, script receivers included, 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 be called from 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 at compile time the way Boost's mutex_type keyword is, with the opposite default. signal_emitter<Sig> is unsynchronized: it takes no lock and one thread at a time uses it, which is 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. Emits of a synchronized signal from different threads run one after another, and 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 JaiScript 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 JaiScript host reconnects only its C++ callbacks after a load, where a Boost host reconnects everything. A saved script-text receiver is script source that runs on the next emit after the load, so an object with signal properties loads only from a trusted source, or on an engine under the safety rails.
From Qt and sigslot libraries
| JaiScript | Boost.Signals2 | Qt | |
|---|---|---|---|
| Declared as | a private signal_emitter<Sig> and a public signal<Sig> built from it | one signal<Sig> object | a signals: member function that moc implements, on a QObject |
| Who can emit | the owner of the emitter | anyone who can reach the signal | anyone who can reach the object |
connect returns | shared_receiver_type, a shared_ptr to the receiver | a connection handle | a QMetaObject::Connection handle |
| Dropping that handle | disconnects | leaves the connection in place (scoped_connection disconnects) | leaves the connection in place |
| Tied to a receiving object | the object holds the handle (a member, receiver_owner, track(...)) | the slot tracks a smart pointer (track, track_foreign) | automatic: destroying the receiver or context object disconnects |
| Script receivers | script text and script functions, connected from C++ or script | none | JavaScript handlers through the QML engine |
| Saved with the object | script-text receivers, named and unnamed (JAI_SIGNAL_PROPERTY) | no | no |
| A receiver throws | out of the emit; later receivers do not run | out of the call; later slots do not run | undefined unless the slot catches it |
| Threads | chosen per signal at compile time: no lock by default; jai::signal_synchronized holds one recursive mutex through the whole emit. Receivers run on the emitting thread | internal locking; slots run outside the signal's lock | direct connections on the emitting thread, queued ones on the receiver's thread |
| Return values | none from emit; emit_while stops at the first false | a combiner; the default returns an optional of the last slot's result | signals are documented as void |
Qt. Qt signals are member functions that moc implements, and any code holding the object can call them; keeping emission inside the class is a convention, where JaiScript's private emitter makes it a rule. Qt needs no code at all to tie a connection to a QObject receiver: destroying the sender, the receiver or the context object of a functor connection disconnects it, and a functor connected without a context object lives as long as the sender. In JaiScript the receiving object holds the handle, as in Tracking. Qt documents an exception thrown from a slot as undefined behavior unless the slot handles it. And Qt delivers across threads, which neither library does: a queued connection copies the arguments into an event for the receiver's thread and runs the slot from that thread's event loop, and the default automatic connection chooses queued delivery when the receiver lives in another thread. JaiScript has no queued delivery; a host that emits from a worker and wants receivers on its main thread hands the emit to that thread itself. Qt's QML engine connects JavaScript handlers to signals, a separate mechanism from moc's C++ slots; in JaiScript script receivers and C++ receivers sit on one emitter.
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 that disconnects on destruction), 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.
Where to next
Signals & Observable Properties covers the emitter, observable values and the three routes a signal takes to script. The binder chapter's signals section shows JAI_SIGNAL_PROPERTY on a bound class, including a script-text receiver surviving a save and load.