view libphobos/src/std/signals.d @ 158:494b0b89df80 default tip

...
author Shinji KONO <kono@ie.u-ryukyu.ac.jp>
date Mon, 25 May 2020 18:13:55 +0900
parents 1830386684a0
children
line wrap: on
line source

// 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 &amp; 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 );
}