This Week in D September 18, 2016

Welcome to This Week in D! Each week, we'll summarize what's been going on in the D community and write brief advice columns to help you get the most out of the D Programming Language.

The D Programming Language is a general purpose programming language that offers modern convenience, modeling power, and native efficiency with a familiar C-style syntax.

This Week in D has an RSS feed.

This Week in D is edited by Adam D. Ruppe. Contact me with any questions, comments, or contributions.

Statistics

In the community

Community announcements

See more at the announce forum.

Tip of the Week

This week's tip is courtesy of ketmar, writing about an event system. The following his his writing:

This is simple "event bus" system, which works somewhat similar to "signals" concept, but is slightly easier to use.

Programmes has to subclass Event object and define the necessary properties in it, like this:

    class EventChat : Event {
      string msg;
      this (ChatServer asrv, string amsg) { source = asrv; msg = amsg; }
    }

Then one must post it with .post() method. This can be combined into one-liner:

    (new EventChat(srv0, "message #0")).post;

To process all posted (queued) events, one has to call processEvents() function.

Now the most interesting part: receiving and processing events. To receive event, programmer has to register event listener for it:

    addEventListener(null, (EventChat evt) {
      import std.stdio;
      writeln("server #", (cast(ChatServer)evt.source).id, ": ", evt.msg);
      log ~= evt.msg;
    });

Note the smart trick there: we are specifying event type to catch right in the delegate, and event system will route only EventChat (and it's subclasses) to this listener.

The implementation of the above scheme require some trickery, though. In runtime, we can check object type via cast: cast(MyObj)obj, which can return null if obj cannot be casted to the given type. Our event system stores all handlers in common structure:

    struct EventListenerInfo {
      TypeInfo_Class ti;
      void delegate (/*Event*/void* e) dg; // actually, `e` is any `Event` subclass; cheater!
      uint id;
    }

Here, we have class type, but not class itself. If we'll try to do cast(ti)obj, compiler will complain. So how can we emulate dynamic casting in this case? Well, the compiler is emiting a call to druntime function for such casts, and that function accepts TypeInfo_Class! So we will just emulate what the compiler does:

    // import druntime casting function
    private extern(C) void* _d_dynamic_cast (Object o, ClassInfo c);
    // and call it!
    auto cobj = _d_dynamic_cast(obj, ti);
    // now, cobj is `null` if `obj` has inappropriate type

Of course, we can create internal delegate in addEventListener() and do the casting in it, as we have exact type there, but then I won't have a chance to show you this cool casting trick! ;-)

Also the source code includes "weak reference" implementation, so adding event listener for some object won't make it "always alive".

Now, the full source for "event bus", with example:

// ////////////////////////////////////////////////////////////////////////// //
module eventbus;

// ////////////////////////////////////////////////////////////////////////// //
// sample usage
class ChatServer {
  uint id;
  this (uint aid) { id = aid; }
}

class EventChat : Event {
  string msg;
  this (ChatServer asrv, string amsg) { source = asrv; msg = amsg; }
}

class ChatTextPane {
  string[] log;
  uint lid;

  this () {
    lid = addEventListener(null, (EventChat evt) {
      import std.stdio;
      writeln("server #", (cast(ChatServer)evt.source).id, ": ", evt.msg);
      log ~= evt.msg;
    });
  }

  ~this () { removeEventListener(lid); }
}


void main () {
  auto srv0 = new ChatServer(0);
  auto srv1 = new ChatServer(1);
  // text pane will receive all chat events
  auto textlog = new ChatTextPane();
  // this will receive only server1 chat events
  addEventListener(srv1, (EventChat evt) {
    assert(evt.source is srv1);
    import std.stdio;
    writeln("LOG for server #1: ", evt.msg);
  });

  // now send some events
  (new EventChat(srv0, "message #0")).post;
  (new EventChat(srv1, "message #1")).post;

  // process queued events
  processEvents();
}


// ////////////////////////////////////////////////////////////////////////// //
public class Event {
  Object source; // can be null

  // propagation flags
  enum PFlags : ubyte {
    Eaten     = 1U<<0, // event is processed, but not cancelled
    Cancelled = 1U<<1, // event is cancelled (it may be *both* processed and cancelled!)
    Posted    = 1U<<7, // event is posted
  }
  private ubyte flags;

  final void post () {
    if (posted) throw new Exception("can't post already posted event");
    flags |= PFlags.Posted;
    events ~= this;
  }

final pure nothrow @safe @nogc:
  void eat () { flags |= PFlags.Eaten; }
  void cancel () { flags |= PFlags.Cancelled; }

const @property:
  bool eaten () { return ((flags&(PFlags.Eaten|PFlags.Cancelled)) == PFlags.Eaten); }
  bool cancelled () { return ((flags&PFlags.Cancelled) != 0); }
  bool processed () { return (eaten || cancelled); }
  bool posted () { return ((flags&PFlags.Posted) != 0); }
}


// ////////////////////////////////////////////////////////////////////////// //
// this returns event listener id which can be used in `removeEventListener()` or 0
public uint addEventListener(E:Event) (Object srcobj, void delegate (E evt) dg) {
  if (dg is null) return 0;
  foreach (ref EventListenerInfo eli; llist) {
    if (typeid(E) == eli.ti && eli.dg is cast(EventListenerInfo.DgType)dg) return eli.id;
  }
  if (lastid == lastid.max) lastid = 1; // wrapping
  llist ~= EventListenerInfo(typeid(E), cast(EventListenerInfo.DgType)dg, srcobj);
  return llist[$-1].id;
}


// returns `true` if a listener was succesfully removed
// this is @nogc, so it can be called in dtors
public bool removeEventListener (uint id) @nogc {
  if (id == 0) return false;
  foreach (ref EventListenerInfo eli; llist) {
    if (eli.id == id) {
      needListenerCleanup = true;
      eli.id = 0;
      eli.dg = null;
      return true;
    }
  }
  return false;
}


// call this to process all queued events
// note that if event handlers will keep adding events,
// this function will never return
public void processEvents () {
  if (events.length == 0) return;
  cleanupListeners();
  while (events.length > 0) {
    auto evt = events.ptr[0];
    foreach (immutable c; 1..events.length) events.ptr[c-1] = events.ptr[c];
    events[$-1] = null;
    events.length -= 1;
    events.assumeSafeAppend;
    try {
      callEventListeners(evt);
    } catch (Exception e) {
      import std.stdio : stderr;
      stderr.writefln("EVENT PROCESSING ERROR: %s", e.msg);
    }
  }
  cleanupListeners();
}


// ////////////////////////////////////////////////////////////////////////// //
// private implementation part
private:

Event[] events; // queued events


// ////////////////////////////////////////////////////////////////////////// //
// get druntime dynamic cast function
private extern(C) void* _d_dynamic_cast (Object o, ClassInfo c);



struct EventListenerInfo {
  alias DgType = void delegate (/*Event*/void* e); // actually, `e` is any `Event` subclass
  TypeInfo_Class ti;
  DgType dg;
  uint id;
  Weak!Object srcobj;
  this (TypeInfo_Class ati, DgType adg, Object sobj) {
    ti = ati;
    id = ++lastid;
    dg = adg;
    if (sobj !is null) srcobj = new Weak!Object(sobj);
  }
}

uint lastid;
EventListenerInfo[] llist;
bool needListenerCleanup = false;


void cleanupListeners () {
  if (!needListenerCleanup) return;
  needListenerCleanup = false;
  size_t pos = 0;
  while (pos < llist.length) {
    if (llist.ptr[pos].srcobj !is null && llist.ptr[pos].srcobj.empty) { llist.ptr[pos].id = 0; }
    if (llist.ptr[pos].id == 0) {
      foreach (immutable c; pos+1..llist.length) llist.ptr[c-1] = llist.ptr[c];
      llist[$-1] = EventListenerInfo.init;
      llist.length -= 1;
      llist.assumeSafeAppend;
    } else {
      ++pos;
    }
  }
}


void callEventListeners (Event evt) {
  if (evt is null || evt.processed) return;
  foreach (ref EventListenerInfo eli; llist) {
    if (eli.id == 0) continue;
    if (eli.srcobj !is null) {
      // if our source object died, mark this listener for deletion
      if (eli.srcobj.empty) { needListenerCleanup = true; eli.id = 0; eli.dg = null; continue; }
      if (evt.source is null) continue;
      if (evt.source !is eli.srcobj.object) continue;
    }
    // the following line does `cast(ObjType)evt` using `TypeInfo_Class`
    auto ecc = _d_dynamic_cast(evt, eli.ti);
    if (ecc !is null) {
      eli.dg(ecc);
      if (evt.processed) break;
    }
  }
}


// ////////////////////////////////////////////////////////////////////////// //
// a thread-safe weak reference implementation
// based on the code from http://forum.dlang.org/thread/jjote0$1cql$1@digitalmars.com
import core.atomic, core.memory;

private alias void delegate (Object) DEvent;
private extern (C) void rt_attachDisposeEvent (Object h, DEvent e);
private extern (C) void rt_detachDisposeEvent (Object h, DEvent e);

final class Weak(T : Object) {
  // Note: This class uses a clever trick which works fine for
  // a conservative GC that was never intended to do
  // compaction/copying in the first place. However, if compaction is
  // ever added to D's GC, this class will break horribly. If D ever
  // gets such a GC, we should push strongly for built-in weak
  // references.

  private size_t mObject;
  private size_t mPtr;
  private size_t mHash;

  this (T obj=null) @trusted {
    hook(obj);
  }

  @property T object () const @trusted nothrow {
    auto obj = cast(T)cast(void*)(atomicLoad(*cast(shared)&mObject)^0xa5a5a5a5u);
    // we've moved obj into the GC-scanned stack space, so it's now
    // safe to ask the GC whether the object is still alive.
    // note that even if the cast and assignment of the obj local
    // doesn't put the object on the stack, this call will.
    // so, either way, this is safe.
    if (obj !is null && GC.addrOf(cast(void*)obj)) return obj;
    return null;
  }

  @property void object (T obj) @trusted {
    auto oobj = cast(T)cast(void*)(atomicLoad(*cast(shared)&mObject)^0xa5a5a5a5u);
    if (oobj !is null && GC.addrOf(cast(void*)oobj)) unhook(oobj);
    oobj = null;
    hook(obj);
  }

  @property bool empty () const @trusted nothrow {
    return (object is null);
  }

  void clear () @trusted { object = null; }

  void opAssign (T obj) @trusted { object = obj; }

  private void hook (Object obj) @trusted {
    if (obj !is null) {
      //auto ptr = cast(size_t)cast(void*)obj;
      // fix from Andrej Mitrovic
      auto ptr = cast(size_t)*(cast(void**)&obj);
      // we use atomics because not all architectures may guarantee atomic store and load of these values
      atomicStore(*cast(shared)&mObject, ptr^0xa5a5a5a5u);
      // only assigned once, so no atomics
      mPtr = ptr^0xa5a5a5a5u;
      mHash = typeid(T).getHash(&obj);
      rt_attachDisposeEvent(obj, &unhook);
      GC.setAttr(cast(void*)this, GC.BlkAttr.NO_SCAN);
    } else {
      atomicStore(*cast(shared)&mObject, cast(size_t)0^0xa5a5a5a5u);
    }
  }

  private void unhook (Object obj) @trusted {
    rt_detachDisposeEvent(obj, &unhook);
    // this assignment is important.
    // if we don't null mObject when it is collected, the check
    // in object could return false positives where the GC has
    // reused the memory for a new object.
    atomicStore(*cast(shared)&mObject, cast(size_t)0^0xa5a5a5a5u);
  }

  override bool opEquals (Object o) @trusted nothrow {
    if (this is o) return true;
    if (auto weak = cast(Weak!T)o) return (mPtr == weak.mPtr);
    return false;
  }

  override int opCmp (Object o) @trusted nothrow {
    if (auto weak = cast(Weak!T)o) return (mPtr > weak.mPtr ? 1 : mPtr < weak.mPtr ? -1 : 0);
    return 1;
  }

  override size_t toHash () @trusted nothrow {
    auto obj = object;
    return (obj ? typeid(T).getHash(&obj) : mHash);
  }

  override string toString () @trusted {
    auto obj = object;
    return (obj ? obj.toString() : toString());
  }
}

Learn more about D

To learn more about D and what's happening in D: