| // Written in the D programming language. |
| |
| /** |
| * Signals and Slots are an implementation of the Observer Pattern. |
| * Essentially, when a Signal is emitted, a list of connected Observers |
| * (called slots) are called. |
| * |
| * There have been several D implementations of Signals and Slots. |
| * This version makes use of several new features in D, which make |
| * using it simpler and less error prone. In particular, it is no |
| * longer necessary to instrument the slots. |
| * |
| * References: |
| * $(LUCKY A Deeper Look at Signals and Slots)$(BR) |
| * $(LINK2 http://en.wikipedia.org/wiki/Observer_pattern, Observer pattern)$(BR) |
| * $(LINK2 http://en.wikipedia.org/wiki/Signals_and_slots, Wikipedia)$(BR) |
| * $(LINK2 http://boost.org/doc/html/$(SIGNALS).html, Boost Signals)$(BR) |
| * $(LINK2 http://qt-project.org/doc/qt-5/signalsandslots.html, Qt)$(BR) |
| * |
| * There has been a great deal of discussion in the D newsgroups |
| * over this, and several implementations: |
| * |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/signal_slots_library_4825.html, signal slots library)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Signals_and_Slots_in_D_42387.html, Signals and Slots in D)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dynamic_binding_--_Qt_s_Signals_and_Slots_vs_Objective-C_42260.html, Dynamic binding -- Qt's Signals and Slots vs Objective-C)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dissecting_the_SS_42377.html, Dissecting the SS)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/dwt/about_harmonia_454.html, about harmonia)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/1502.html, Another event handling module)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/41825.html, Suggestion: signal/slot mechanism)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/13251.html, Signals and slots?)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/10714.html, Signals and slots ready for evaluation)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/1393.html, Signals & Slots for Walter)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/28456.html, Signal/Slot mechanism?)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/19470.html, Modern Features?)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/16592.html, Delegates vs interfaces)$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/16583.html, The importance of component programming (properties$(COMMA) signals and slots$(COMMA) etc))$(BR) |
| * $(LINK2 http://www.digitalmars.com/d/archives/16368.html, signals and slots)$(BR) |
| * |
| * Bugs: |
| * Slots can only be delegates formed from class objects or |
| * interfaces to class objects. If a delegate to something else |
| * is passed to connect(), such as a struct member function, |
| * a nested function or a COM interface, undefined behavior |
| * will result. |
| * |
| * Not safe for multiple threads operating on the same signals |
| * or slots. |
| * Macros: |
| * SIGNALS=signals |
| * |
| * Copyright: Copyright Digital Mars 2000 - 2009. |
| * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). |
| * Authors: $(HTTP digitalmars.com, Walter Bright) |
| * Source: $(PHOBOSSRC std/_signals.d) |
| * |
| * $(SCRIPT inhibitQuickIndex = 1;) |
| */ |
| /* Copyright Digital Mars 2000 - 2009. |
| * Distributed under the Boost Software License, Version 1.0. |
| * (See accompanying file LICENSE_1_0.txt or copy at |
| * http://www.boost.org/LICENSE_1_0.txt) |
| */ |
| module std.signals; |
| |
| import core.exception : onOutOfMemoryError; |
| import core.stdc.stdlib : calloc, realloc, free; |
| import std.stdio; |
| |
| // Special function for internal use only. |
| // Use of this is where the slot had better be a delegate |
| // to an object or an interface that is part of an object. |
| extern (C) Object _d_toObject(void* p); |
| |
| // Used in place of Object.notifyRegister and Object.notifyUnRegister. |
| alias DisposeEvt = void delegate(Object); |
| extern (C) void rt_attachDisposeEvent( Object obj, DisposeEvt evt ); |
| extern (C) void rt_detachDisposeEvent( Object obj, DisposeEvt evt ); |
| //debug=signal; |
| |
| /************************ |
| * Mixin to create a signal within a class object. |
| * |
| * Different signals can be added to a class by naming the mixins. |
| */ |
| |
| mixin template Signal(T1...) |
| { |
| static import core.exception; |
| static import core.stdc.stdlib; |
| /*** |
| * A slot is implemented as a delegate. |
| * The slot_t is the type of the delegate. |
| * The delegate must be to an instance of a class or an interface |
| * to a class instance. |
| * Delegates to struct instances or nested functions must not be |
| * used as slots. |
| */ |
| alias slot_t = void delegate(T1); |
| |
| /*** |
| * Call each of the connected slots, passing the argument(s) i to them. |
| * Nested call will be ignored. |
| */ |
| final void emit( T1 i ) |
| { |
| if (status >= ST.inemitting || !slots.length) |
| return; // should not nest |
| |
| status = ST.inemitting; |
| scope (exit) |
| status = ST.idle; |
| |
| foreach (slot; slots[0 .. slots_idx]) |
| { if (slot) |
| slot(i); |
| } |
| |
| assert(status >= ST.inemitting); |
| if (status == ST.inemitting_disconnected) |
| { |
| for (size_t j = 0; j < slots_idx;) |
| { |
| if (slots[j] is null) |
| { |
| slots_idx--; |
| slots[j] = slots[slots_idx]; |
| } |
| else |
| j++; |
| } |
| } |
| } |
| |
| /*** |
| * Add a slot to the list of slots to be called when emit() is called. |
| */ |
| final void connect(slot_t slot) |
| { |
| /* Do this: |
| * slots ~= slot; |
| * but use malloc() and friends instead |
| */ |
| auto len = slots.length; |
| if (slots_idx == len) |
| { |
| if (slots.length == 0) |
| { |
| len = 4; |
| auto p = core.stdc.stdlib.calloc(slot_t.sizeof, len); |
| if (!p) |
| core.exception.onOutOfMemoryError(); |
| slots = (cast(slot_t*) p)[0 .. len]; |
| } |
| else |
| { |
| import core.checkedint : addu, mulu; |
| bool overflow; |
| len = addu(mulu(len, 2, overflow), 4, overflow); // len = len * 2 + 4 |
| const nbytes = mulu(len, slot_t.sizeof, overflow); |
| if (overflow) assert(0); |
| |
| auto p = core.stdc.stdlib.realloc(slots.ptr, nbytes); |
| if (!p) |
| core.exception.onOutOfMemoryError(); |
| slots = (cast(slot_t*) p)[0 .. len]; |
| slots[slots_idx + 1 .. $] = null; |
| } |
| } |
| slots[slots_idx++] = slot; |
| |
| L1: |
| Object o = _d_toObject(slot.ptr); |
| rt_attachDisposeEvent(o, &unhook); |
| } |
| |
| /*** |
| * Remove a slot from the list of slots to be called when emit() is called. |
| */ |
| final void disconnect(slot_t slot) |
| { |
| debug (signal) writefln("Signal.disconnect(slot)"); |
| size_t disconnectedSlots = 0; |
| size_t instancePreviousSlots = 0; |
| if (status >= ST.inemitting) |
| { |
| foreach (i, sloti; slots[0 .. slots_idx]) |
| { |
| if (sloti.ptr == slot.ptr && |
| ++instancePreviousSlots && |
| sloti == slot) |
| { |
| disconnectedSlots++; |
| slots[i] = null; |
| status = ST.inemitting_disconnected; |
| } |
| } |
| } |
| else |
| { |
| for (size_t i = 0; i < slots_idx; ) |
| { |
| if (slots[i].ptr == slot.ptr && |
| ++instancePreviousSlots && |
| slots[i] == slot) |
| { |
| slots_idx--; |
| disconnectedSlots++; |
| slots[i] = slots[slots_idx]; |
| slots[slots_idx] = null; // not strictly necessary |
| } |
| else |
| i++; |
| } |
| } |
| |
| // detach object from dispose event if all its slots have been removed |
| if (instancePreviousSlots == disconnectedSlots) |
| { |
| Object o = _d_toObject(slot.ptr); |
| rt_detachDisposeEvent(o, &unhook); |
| } |
| } |
| |
| /* ** |
| * Special function called when o is destroyed. |
| * It causes any slots dependent on o to be removed from the list |
| * of slots to be called by emit(). |
| */ |
| final void unhook(Object o) |
| in { assert( status == ST.idle ); } |
| body { |
| debug (signal) writefln("Signal.unhook(o = %s)", cast(void*) o); |
| for (size_t i = 0; i < slots_idx; ) |
| { |
| if (_d_toObject(slots[i].ptr) is o) |
| { slots_idx--; |
| slots[i] = slots[slots_idx]; |
| slots[slots_idx] = null; // not strictly necessary |
| } |
| else |
| i++; |
| } |
| } |
| |
| /* ** |
| * There can be multiple destructors inserted by mixins. |
| */ |
| ~this() |
| { |
| /* ** |
| * When this object is destroyed, need to let every slot |
| * know that this object is destroyed so they are not left |
| * with dangling references to it. |
| */ |
| if (slots.length) |
| { |
| foreach (slot; slots[0 .. slots_idx]) |
| { |
| if (slot) |
| { Object o = _d_toObject(slot.ptr); |
| rt_detachDisposeEvent(o, &unhook); |
| } |
| } |
| core.stdc.stdlib.free(slots.ptr); |
| slots = null; |
| } |
| } |
| |
| private: |
| slot_t[] slots; // the slots to call from emit() |
| size_t slots_idx; // used length of slots[] |
| |
| enum ST { idle, inemitting, inemitting_disconnected } |
| ST status; |
| } |
| |
| /// |
| @system unittest |
| { |
| import std.signals; |
| |
| int observedMessageCounter = 0; |
| |
| class Observer |
| { // our slot |
| void watch(string msg, int value) |
| { |
| switch (observedMessageCounter++) |
| { |
| case 0: |
| assert(msg == "setting new value"); |
| assert(value == 4); |
| break; |
| case 1: |
| assert(msg == "setting new value"); |
| assert(value == 6); |
| break; |
| default: |
| assert(0, "Unknown observation"); |
| } |
| } |
| } |
| |
| class Observer2 |
| { // our slot |
| void watch(string msg, int value) |
| { |
| } |
| } |
| |
| class Foo |
| { |
| int value() { return _value; } |
| |
| int value(int v) |
| { |
| if (v != _value) |
| { _value = v; |
| // call all the connected slots with the two parameters |
| emit("setting new value", v); |
| } |
| return v; |
| } |
| |
| // Mix in all the code we need to make Foo into a signal |
| mixin Signal!(string, int); |
| |
| private : |
| int _value; |
| } |
| |
| Foo a = new Foo; |
| Observer o = new Observer; |
| auto o2 = new Observer2; |
| auto o3 = new Observer2; |
| auto o4 = new Observer2; |
| auto o5 = new Observer2; |
| |
| a.value = 3; // should not call o.watch() |
| a.connect(&o.watch); // o.watch is the slot |
| a.connect(&o2.watch); |
| a.connect(&o3.watch); |
| a.connect(&o4.watch); |
| a.connect(&o5.watch); |
| a.value = 4; // should call o.watch() |
| a.disconnect(&o.watch); // o.watch is no longer a slot |
| a.disconnect(&o3.watch); |
| a.disconnect(&o5.watch); |
| a.disconnect(&o4.watch); |
| a.disconnect(&o2.watch); |
| a.value = 5; // so should not call o.watch() |
| a.connect(&o2.watch); |
| a.connect(&o.watch); // connect again |
| a.value = 6; // should call o.watch() |
| destroy(o); // destroying o should automatically disconnect it |
| a.value = 7; // should not call o.watch() |
| |
| assert(observedMessageCounter == 2); |
| } |
| |
| // A function whose sole purpose is to get this module linked in |
| // so the unittest will run. |
| void linkin() { } |
| |
| @system unittest |
| { |
| class Observer |
| { |
| void watch(string msg, int i) |
| { |
| //writefln("Observed msg '%s' and value %s", msg, i); |
| captured_value = i; |
| captured_msg = msg; |
| } |
| |
| int captured_value; |
| string captured_msg; |
| } |
| |
| class Foo |
| { |
| @property int value() { return _value; } |
| |
| @property int value(int v) |
| { |
| if (v != _value) |
| { _value = v; |
| emit("setting new value", v); |
| } |
| return v; |
| } |
| |
| mixin Signal!(string, int); |
| |
| private: |
| int _value; |
| } |
| |
| Foo a = new Foo; |
| Observer o = new Observer; |
| |
| // check initial condition |
| assert(o.captured_value == 0); |
| assert(o.captured_msg == ""); |
| |
| // set a value while no observation is in place |
| a.value = 3; |
| assert(o.captured_value == 0); |
| assert(o.captured_msg == ""); |
| |
| // connect the watcher and trigger it |
| a.connect(&o.watch); |
| a.value = 4; |
| assert(o.captured_value == 4); |
| assert(o.captured_msg == "setting new value"); |
| |
| // disconnect the watcher and make sure it doesn't trigger |
| a.disconnect(&o.watch); |
| a.value = 5; |
| assert(o.captured_value == 4); |
| assert(o.captured_msg == "setting new value"); |
| |
| // reconnect the watcher and make sure it triggers |
| a.connect(&o.watch); |
| a.value = 6; |
| assert(o.captured_value == 6); |
| assert(o.captured_msg == "setting new value"); |
| |
| // destroy the underlying object and make sure it doesn't cause |
| // a crash or other problems |
| destroy(o); |
| a.value = 7; |
| } |
| |
| @system unittest |
| { |
| class Observer |
| { |
| int i; |
| long l; |
| string str; |
| |
| void watchInt(string str, int i) |
| { |
| this.str = str; |
| this.i = i; |
| } |
| |
| void watchLong(string str, long l) |
| { |
| this.str = str; |
| this.l = l; |
| } |
| } |
| |
| class Bar |
| { |
| @property void value1(int v) { s1.emit("str1", v); } |
| @property void value2(int v) { s2.emit("str2", v); } |
| @property void value3(long v) { s3.emit("str3", v); } |
| |
| mixin Signal!(string, int) s1; |
| mixin Signal!(string, int) s2; |
| mixin Signal!(string, long) s3; |
| } |
| |
| void test(T)(T a) { |
| auto o1 = new Observer; |
| auto o2 = new Observer; |
| auto o3 = new Observer; |
| |
| // connect the watcher and trigger it |
| a.s1.connect(&o1.watchInt); |
| a.s2.connect(&o2.watchInt); |
| a.s3.connect(&o3.watchLong); |
| |
| assert(!o1.i && !o1.l && o1.str == null); |
| assert(!o2.i && !o2.l && o2.str == null); |
| assert(!o3.i && !o3.l && o3.str == null); |
| |
| a.value1 = 11; |
| assert(o1.i == 11 && !o1.l && o1.str == "str1"); |
| assert(!o2.i && !o2.l && o2.str == null); |
| assert(!o3.i && !o3.l && o3.str == null); |
| o1.i = -11; o1.str = "x1"; |
| |
| a.value2 = 12; |
| assert(o1.i == -11 && !o1.l && o1.str == "x1"); |
| assert(o2.i == 12 && !o2.l && o2.str == "str2"); |
| assert(!o3.i && !o3.l && o3.str == null); |
| o2.i = -12; o2.str = "x2"; |
| |
| a.value3 = 13; |
| assert(o1.i == -11 && !o1.l && o1.str == "x1"); |
| assert(o2.i == -12 && !o1.l && o2.str == "x2"); |
| assert(!o3.i && o3.l == 13 && o3.str == "str3"); |
| o3.l = -13; o3.str = "x3"; |
| |
| // disconnect the watchers and make sure it doesn't trigger |
| a.s1.disconnect(&o1.watchInt); |
| a.s2.disconnect(&o2.watchInt); |
| a.s3.disconnect(&o3.watchLong); |
| |
| a.value1 = 21; |
| a.value2 = 22; |
| a.value3 = 23; |
| assert(o1.i == -11 && !o1.l && o1.str == "x1"); |
| assert(o2.i == -12 && !o1.l && o2.str == "x2"); |
| assert(!o3.i && o3.l == -13 && o3.str == "x3"); |
| |
| // reconnect the watcher and make sure it triggers |
| a.s1.connect(&o1.watchInt); |
| a.s2.connect(&o2.watchInt); |
| a.s3.connect(&o3.watchLong); |
| |
| a.value1 = 31; |
| a.value2 = 32; |
| a.value3 = 33; |
| assert(o1.i == 31 && !o1.l && o1.str == "str1"); |
| assert(o2.i == 32 && !o1.l && o2.str == "str2"); |
| assert(!o3.i && o3.l == 33 && o3.str == "str3"); |
| |
| // destroy observers |
| destroy(o1); |
| destroy(o2); |
| destroy(o3); |
| a.value1 = 41; |
| a.value2 = 42; |
| a.value3 = 43; |
| } |
| |
| test(new Bar); |
| |
| class BarDerived: Bar |
| { |
| @property void value4(int v) { s4.emit("str4", v); } |
| @property void value5(int v) { s5.emit("str5", v); } |
| @property void value6(long v) { s6.emit("str6", v); } |
| |
| mixin Signal!(string, int) s4; |
| mixin Signal!(string, int) s5; |
| mixin Signal!(string, long) s6; |
| } |
| |
| auto a = new BarDerived; |
| |
| test!Bar(a); |
| test!BarDerived(a); |
| |
| auto o4 = new Observer; |
| auto o5 = new Observer; |
| auto o6 = new Observer; |
| |
| // connect the watcher and trigger it |
| a.s4.connect(&o4.watchInt); |
| a.s5.connect(&o5.watchInt); |
| a.s6.connect(&o6.watchLong); |
| |
| assert(!o4.i && !o4.l && o4.str == null); |
| assert(!o5.i && !o5.l && o5.str == null); |
| assert(!o6.i && !o6.l && o6.str == null); |
| |
| a.value4 = 44; |
| assert(o4.i == 44 && !o4.l && o4.str == "str4"); |
| assert(!o5.i && !o5.l && o5.str == null); |
| assert(!o6.i && !o6.l && o6.str == null); |
| o4.i = -44; o4.str = "x4"; |
| |
| a.value5 = 45; |
| assert(o4.i == -44 && !o4.l && o4.str == "x4"); |
| assert(o5.i == 45 && !o5.l && o5.str == "str5"); |
| assert(!o6.i && !o6.l && o6.str == null); |
| o5.i = -45; o5.str = "x5"; |
| |
| a.value6 = 46; |
| assert(o4.i == -44 && !o4.l && o4.str == "x4"); |
| assert(o5.i == -45 && !o4.l && o5.str == "x5"); |
| assert(!o6.i && o6.l == 46 && o6.str == "str6"); |
| o6.l = -46; o6.str = "x6"; |
| |
| // disconnect the watchers and make sure it doesn't trigger |
| a.s4.disconnect(&o4.watchInt); |
| a.s5.disconnect(&o5.watchInt); |
| a.s6.disconnect(&o6.watchLong); |
| |
| a.value4 = 54; |
| a.value5 = 55; |
| a.value6 = 56; |
| assert(o4.i == -44 && !o4.l && o4.str == "x4"); |
| assert(o5.i == -45 && !o4.l && o5.str == "x5"); |
| assert(!o6.i && o6.l == -46 && o6.str == "x6"); |
| |
| // reconnect the watcher and make sure it triggers |
| a.s4.connect(&o4.watchInt); |
| a.s5.connect(&o5.watchInt); |
| a.s6.connect(&o6.watchLong); |
| |
| a.value4 = 64; |
| a.value5 = 65; |
| a.value6 = 66; |
| assert(o4.i == 64 && !o4.l && o4.str == "str4"); |
| assert(o5.i == 65 && !o4.l && o5.str == "str5"); |
| assert(!o6.i && o6.l == 66 && o6.str == "str6"); |
| |
| // destroy observers |
| destroy(o4); |
| destroy(o5); |
| destroy(o6); |
| a.value4 = 44; |
| a.value5 = 45; |
| a.value6 = 46; |
| } |
| |
| // Triggers bug from issue 15341 |
| @system unittest |
| { |
| class Observer |
| { |
| void watch() { } |
| void watch2() { } |
| } |
| |
| class Bar |
| { |
| mixin Signal!(); |
| } |
| |
| auto a = new Bar; |
| auto o = new Observer; |
| |
| //Connect both observer methods for the same instance |
| a.connect(&o.watch); |
| a.connect(&o.watch2); // not connecting watch2() or disconnecting it manually fixes the issue |
| |
| //Disconnect a single method of the two |
| a.disconnect(&o.watch); // NOT disconnecting watch() fixes the issue |
| |
| destroy(o); // destroying o should automatically call unhook and disconnect the slot for watch2 |
| a.emit(); // should not raise segfault since &o.watch2 is no longer connected |
| } |
| |
| version (none) // Disabled because of dmd @@@BUG5028@@@ |
| @system unittest |
| { |
| class A |
| { |
| mixin Signal!(string, int) s1; |
| } |
| |
| class B : A |
| { |
| mixin Signal!(string, int) s2; |
| } |
| } |
| |
| // Triggers bug from issue 16249 |
| @system unittest |
| { |
| class myLINE |
| { |
| mixin Signal!( myLINE, int ); |
| |
| void value( int v ) |
| { |
| if ( v >= 0 ) emit( this, v ); |
| else emit( new myLINE, v ); |
| } |
| } |
| |
| class Dot |
| { |
| int value; |
| |
| myLINE line_; |
| void line( myLINE line_x ) |
| { |
| if ( line_ is line_x ) return; |
| |
| if ( line_ !is null ) |
| { |
| line_.disconnect( &watch ); |
| } |
| line_ = line_x; |
| line_.connect( &watch ); |
| } |
| |
| void watch( myLINE line_x, int value_x ) |
| { |
| line = line_x; |
| value = value_x; |
| } |
| } |
| |
| auto dot1 = new Dot; |
| auto dot2 = new Dot; |
| auto line = new myLINE; |
| dot1.line = line; |
| dot2.line = line; |
| |
| line.value = 11; |
| assert( dot1.value == 11 ); |
| assert( dot2.value == 11 ); |
| |
| line.value = -22; |
| assert( dot1.value == -22 ); |
| assert( dot2.value == -22 ); |
| } |
| |