wiki:signals

Signals

A simple callback is a function call that is performed when another action took place. Normally, callback can be registered to an object and will be processed by that object whenever the time is right. There are several ways to implement such callbacks. Some are more general, others only apply to a subset of possible actions in the system. The Signals implementation tries to be usable in all cases but adds one or another constraint to keep code that uses signals readable.

How to Install

Environment
4.1, 4.2 and later
4.1.1 (Win)
r2316
Sources
ConfigurationOfSignals
SwaUtilities
Signals
SI-Wrapper (optional)
SI-OB-Morphic (optional)
SI-Reflection (optional)
SI-Benchmarks (optional)
OmniBrowser (optional)
Method Wrappers? (optional)
AXAnnouncements (optional)
Misc
SwaUtilities@SqueakSource

Using Metacello, just run the following code in your workspace:

(Installer mc http: 'http://www.hpi.uni-potsdam.de/hirschfeld/squeaksource/')
   project: 'MetacelloRepository';
   install: 'ConfigurationOfSignals'.
ConfigurationOfSignals load.

If you later want to get the latest development version of Signals, run this:

ConfigurationOfSignals loadDevelopment.

The Signals project contains several sub-packages:

  • Signals-Core ... main signals implementation
  • Signals-Tests ... tests for the core implementation
  • SI-OB-Morphic ... morphic extensions to OmniBrowser
  • SI-Benchmarks ... benchmmarks for Signals and other mechanisms, e.g., Announcements
  • SI-Wrapper ... turns each message into a signal using method wrappers
  • SI-Reflection ... search and browse signals in the image

Important Notes for development version users: Support for wrapped signals removed in Signals-mt.32. So there is no need to load `SI-Wrapper´ right now.

How to Use

Terms

A signal is a method that can be called to trigger a callback. Then the signal will be emitted.

A callback is the method that is called or the process that is involved in performing this method call in response to an emitted signal.

A connection is a pair of signal and callback. The pair is used to lookup all callbacks whenever a signal is emitted. A signal can have multiple connections.

Implementing Signals

A signal is an object's method that is able to trigger callbacks. It is implemented like this:

MyCounter>>valueChanged: newValue
   self emit.

Each signal can have an arbitrary number of arguments. The emit-call triggers the callback. In this way, any method could be used as a signal. This could be very confusing for the developer, therefore it is not recommended. Calling self emit not inside a method context but a block context works as well.

Obviously, a signal is emitted by calling the method that is meant to be the signal:

self valueChanged: 42.

You must not emit an object's signal from the outside. The following fails if not executed within the same instance as myCounter:

myCounter valueChanged: 42.

One could argue that this is an unnecessary constraint, but it is the idea of the signals, that an object decides itself when a signal is emitted. Allowing the signal sending from everywhere could lead to less readable code.

See the advanced usage section and read about public signals if you want to bypass this constraint for some reason.

Basic Connections

The simplest form of a connection is created from within the UI process of Squeak, e.g., using the workspace, in the following way:

self
   connect: myCounter
   signal: #valueChanged: 
   to: Transcript 
   selector: #show:.

This more or less mediator approach can be changed if an object itself connects to a signal:

self
   connect: myCounter
   signal: #valueChanged: 
   toSelector: #show:.

Note on garbage collection: You need to store a reference to the receiver of the callback. Otherwise the garbage collector will collect the receiver and the connection is lost.

Basic Signal Processing

In the simplest way, a signal is emitted from within the UI process of Squeak, e.g., in response to a Morphic #mouseDown: event:

"The signal."
MyMorph>>mouseDownReceived: evt
   self emit.
   
"The default event handling routine in Morphic."
MyMorph>>mouseDown: evt
   self mouseDownReceived: evt.

Having this, the signal is processed synchronously and blocking, which means that all callbacks were made after returning from the call that emitted the signal.

Arguments and Patterns

Each callback should expect at most arguments as the signal contains. By default, trailing arguments will be truncated and ignored:

"Works."
self connect: s signal: #a: toSelector: #b:.
"Works. Auto-truncate."
self connect: s signal: #a: toSelector: #c.
"Does not work."
self connect: s signal: #a toSelector: #b:.

It is possible to choose and reorder specific arguments from the signal with a pattern. A pattern is an array of numbers that maps argument positions. It must have exactly as many elements as the callback needs:

self
   connect: aSender
   signal: #a:b:c:
   toSelector: #d:e:f:
   pattern: #(3 1 2). "c->d, a->e, b->f"
   
self
   connect: aSender
   signal: #a:b:c:
   toSelector: #d:e:f:
   pattern: #(4 1). "Error!"

The automatic truncation can be used to avoid patterns like #(1 2) or #(). Arguments can be duplicated, i.e., #(1 1).

Disconnection

A connection can be removed from the sender:

self "the sender"
   disconnectSignal: #valueChanged:
   from: aReceiver
   selector: #processValue:.

You can also remove all connections from a specific receiver, all connections from a specific signal and any connection that was ever created.

Although, connections will be destroyed if either sender or receiver is deleted automatically, sometimes it could be useful to disconnect a signal to avoid endless loops.

Using Processes

The connection and disconnection of signals is thread-safe.

Signals can be emitted from within any Squeak process. Normally, the creator of a connection should not bother where the signal is emitted but where the callback is processed to avoid considering thread-safety of involved data structures.

When creating a connection from within the Squeak UI process, callbacks will be processed within that process no matter where the signal was emitted. This is a special case of so called queued connections.

Queued Connections

Sometimes it is necessary to specify the process that is involved during signal processing. Except for the UI process, any connection made from within any other process will be handled in the process where the signal is emitted. This could lead to unexpected behavior if all involved data structures are not thread-safe!

"Here: Make you data structures thread-safe!"
[anObject
   connect: myCounter
   signal: #valueChanged:
   toSelector: #processValue:] fork.

To encounter this problem, connections can be queued:

Transcript
   connect: myCounter
   signal: #valueChanged:
   toSelector: #show:
   queue: aSharedQueue.

Using a queue, an emitted signal causes the queue to be filled with the callbacks stored into blocks that have to be evaluated by a process frequently.

The Squeak UI process has such a queue already that will be processed in the main world cycle frequently: WorldState>>deferredUIMessages. Using this, basic connections from within the UI process will be queued if the signal is emitted from within any other process than the UI process automatically. Otherwise they are processed synchronously and blocking.

Any queued connection can be blocking which means that the signal emitting process will be suspended until the callback waiting in the queue is processed:

Transcript
   connect: myCounter
   signal: #valueChanged:
   toSelector: #show:
   queue: aSharedQueue
   blocking: true.

Of course, anyone can resume a suspended process in Squeak, but this implementation of blocking connections was quite simple and should work for the most cases.

Avoiding Deadlocks

When working with queued connections, deadlocks can appear, e.g., if a process waits for a signal to be processed by the same process that normally looks for the queue frequently. This can happen especially in the UI process and fixed as follows:

[self signalWasSent "non-blocking"]
   whileFalse: [
      WorldState deferredUIMessages next value "blocking"].

This approach works with any queue. It is similar to processEvents() in the Qt Framework. In Squeak, the whole world can be kept responsive by calling World>>doOneCycle frequently.

If you create a blocking connection and a signal is emitted with a queued callback to be processed in the same process, nothing will happen until the process is resumed from the outside. You should not do that.

Waiting for Signals

The SignalSpy can be used to wait for signals explicitly. Normally, it should be used in tests and not in the application itself:

spy := SignalSpy
   onSender: aSender
   signal: #signalEmitted.
signal := spy waitForNextSignal.

The signal spy catches all registered signals emitted from any process. Wait operations are blocking so be sure that the signals are emitted in another process. You can wait for a specific signal using: #waitForSignal:.

If you want to test for some properties of the next signal besides its name, use #waitForNextSignalSatisfying::

spy waitForNextSignalSatisfying: [:signal | |arguments|
   arguments := signal second.
   arguments first = #foobar].

The structre stored by the SignalSpy is an array with two fields: signal name and signal arguments.

Awareness

There is an extension to the OmniBrowser? Framework that shows a small icon in front of signals in the message list:

Trying to load the Signals package into a Squeak image without OmniBrowser? installed results in a warning that can be ignored safely.

A connection creation needs symbols - one for the signal and one for the callback. Therefore it is possible to use the Squeak Browse Senders way to investigate them:

self
   connect: aSender
   signal: #valueChanged: "Will be found."
   to: aReceiver
   selector: #value:. "Will be found."

Unfortunately the signal emitting place, which is just a message send, and other message sends that are not callbacks will be found as well. To solve this problem, some reflective methods were added to SystemNavigation to allow browsing for connections and signal sends:

SystemNavigation default
   allConnectionsOn: #valueChanged:
   from: aClass. "Here is the connection code
                 somewhere."
   
SystemNavigation default
   allSignalEmitsFrom: aSenderClass
   signal: #valueChanged:.

Advanced Usage

Public Signals

To bypass the constraint that a signal cannot be emitted from the outside but must be emitted from the instance that has the signal, you can use public signals. These types of signals just do not have that constraint and are implemented as the following:

MyCounter>>valueChanged: newValue
   self emitAlways.

Use them with caution because they could lead to less readable code.

Connect Signal to Signal

Signals are just normal methods. Therefore they can be used as callbacks as well. If the receiver is not identical to the sender of the signal you need to use a public signal as target:

self
   connect: aSender
   signal: #valueChanged "Signal #1"
   toSelector: #switchToggled. "Signal #2 - Has to be public if sender <> receiver!"

Automatic Connections

It is possible to create methods that look like they want to be connected to a senders signal:

MyDialog>>onNameEditTextChanged: aText
   self availabilityLabel contents: 
      (self checkAvailability: aText).

Such a method can benefit from its signature if there is an instance variable named nameEdit which sends the signal #textChanged:. Automatic connections can be created from such patterns after the referenced instance variables were initialized:

MyDialog>>initialize
   super initialize.
   nameEdit := MyNameEdit new.
   self createAutoSignalConnections.

If the sender does not send this signal the connection will silently not be created.

Wrapped Signals

Sometimes it could be useful to re-use a arbitrary message that an object understands as a signal, e.g., to be notified if a morph changes its position without having to subclass that morph like this:

MyMorph>>position: aPosition
   super position: aPosition.
   self emitAlways. "Would have to be a public signal."

Or like this:

MyMorph>>position: aPosition
   super position: aPosition.
   self positionChanged: aPosition.

MyMorph>>positionChanged: newPosition
   self emit.

To avoid this, it is possible to use any arbitrary message as a signal as long has the sub-package SI-Wrapper is installed which needs the package/project Method Wrappers? as well:

"Installs a method wrapper on #position:."
self connect: aMorph signal: #position: to: Transcript selector: #show:.

There will be only one method wrapper installed for each message that will be used in a connection for the first time. If no connections use that message as a signal, the wrapper will be removed from that message.

Advanced Patterns

It is possible to access the sender of a signal itself and use it as an argument for the receiver. This is achieved with a 0 in the pattern and can reduce the number of needed methods because the control flow may consider the sender, e.g. different actions are triggered and show themselves on a log:

self
   connect: anAction
   signal: #triggered
   toSelector: #logAction:
   pattern: #(0).

self
   connect: anotherAction
   signal: #triggered
   toSelector: #logAction:
   pattern: #(0).

Patterns can have default values that will be send if the signal is processed:

"self changed: #counterValue."
self
   connect: myCounter
   signal: #valueChanged:
   toSelector: #changed:
   pattern: #(=counterValue).

You can send whole objects as default value:

self
   connect: myCounter
   signal: #valueChanged:
   toSelector: #addMorphBack:
   pattern: {#=. Morph new}.

Patterns can mix index references to the sender's arguments and default values: #(2 1 =foobar). As you see, the magic happens just because of the escape symbol #=. Every value that comes after it, will be treated as itsself and not as an index reference.

Hint: If you store an object into a pattern, the garbage collector will collect that object if it is not referenced somewhere else. In that case, nil will be supplied as argument.

How to Extend

General Implementation Approach

  • one central repository SignalConnectionsRepository stores all connections in the system
  • self emit looks into the call-stack to retrieve arguments and do other checks
  • the repository uses weak data structures to not interfere with the garbage collector

Next Possible Steps

  • arguments processor to transform arguments before doing the callback
  • detect recursions on connection level to prevent endless loops, e.g., count and reset a number in each connection
  • Test-Runner coverage testing is broken because signals will not be emitted in the correct context and fail

Other Observer Mechanisms

Object Dependents

Every object in Squeak has a list of dependent objects. You trigger an event with #changed: or #changed:with: and all dependents receive a #update: resp. #update:with: call. Dependents are managed using #addDependent: and #removeDependent:.

Squeak processes will not be considered and data structures have to secured to avoid synchronization problems. Using this mechanism, objects are connected completely and not only in special cases because there is no filtering possible.

Note on performance issues: As you see in the benchmarks, setting up thousands of bindings is quite slow. Therefore you should subclass Model instead of Object if you want to write faster code. Model uses a custom implementation of dependents. But it is not possible for Morphs because they subclass from Object directly.

Object Events

Squeak has a built-in callback mechanism that is lightweight and decouples sender and receiver in a convenient way.

self "sender"
   when: #valueChanged
   send: #value:
   to: aReceiver.

However, events can be triggered from everywhere and new event names can be made up at any time so that that it can be confusing to the developer to realize all possible events from an object:

self "sender"
   triggerEvent: #valueChanged
   with: 42.

"Surprisingly new event."
self
   triggerEvent: #surprised
   with: 23.

"Some other object triggers my event. Strange..."
MyConfuser>>confuseCounter
   myCounter
      triggerEvent: #valueChanged
      with: 42.

Although, it is possible to handle events that no one processes separately.

Morphic Callbacks

Default mouse or keyboard input events can be connected using #on:send:to:. This avoids the need to implement, e.g., #mouseDown:, or #keyStroke: but messages with more readable names.

This approach is only limited to these standard events and cannot be used so create arbitrary connections.

Announcements

AXAnnouncements@SqueakSource

Bindings

Bindings@SqueakSource

Feature Comparison

Every callback implementation has another notion of the terms involved. For clarification, we use Event as the thing that can be triggered in notion of a Sender and a Callback that is processed in notion of a Receiver if the necessary Binding was configured correctly.

Morphic
Callbacks
Announce-
ments
Object
Dependents
Object
Events
Bindings Signals
Allows definiton of new Events ?
Available Events are defined explicitly ?
Events are defined for a specific class/subclass ?
Bindings are at Event level ?
Bindings are at Sender/Receiver level ?
Events are first-class objects ?
Automatic truncation of arguments ?
Explicit argument count check at binding-setup-time ?
Explicit argument count check at event-trigger-time ?
Access to sending object at binding-level ?
Access to sending object at event-level ?
Event encodes number of available arguments ?
Unhandled Events can be processed separately ?
Event triggering can be limited to the sender ?
Provides synchronisation mechansism for thread-safety ?
Explicit Event/Callback check at binding-setup-time ?

It is not guaranteed that the table is correct except for the Signals column.

Benchmark

One sender was bound to 10000 receivers. Then one event was triggered and processed synchronously. The benchmark code is in the Signals package.

Test-System: Core2Duo @ 2.54 GHz, 4096 MB DDR2 RAM, Windows 7 Professional, Squeak 4.1

Message
Sends
Announce-
ments
Object
Dependents
Object
Events
Bindings Signals
Bindings creation time 0 ms
0 %
44 ms
20 %
39765 ms
17832 %
64204 ms
28791 %
? 223 ms
100 %
Event triggering time 1 ms
4 %
4 ms
17 %
2 ms
8 %
78 ms
325 %
? 24 ms
100 %

Acknowledgments

The signals mechanism was inspired by the signals/slots concept in the Nokia Qt Framework.

To date the following people contributed to this project:

  • Marcel Taeumel
Last modified 6 years ago Last modified on 01/26/12 17:54:06

Attachments (1)

Download all attachments as: .zip