sigslot
1.0.1Signals & Slots library
SigSlot for Common Lisp (EXPERIMENTAL)
SigSlot is a simple Common Lisp library that implements the Signals & Slots pattern. It is thread-safe and the performance should be enough for common uses. The typical use case is for UI interactions and decoupling of objects.
Backing the sigslot implementation is the observer pattern, which you can also use for simpler needs.
Dependencies
SigSlot depends on the following libraries:
- alexandria
- bordeaux-threads
- trivial-garbage
- trivial-main-thread
They are all available on Quicklisp.
Installation
As SigSlot is not yet available on Quicklisp, you'll have to download
the source code and put it in ~/quicklisp/local-projects/
for
Quicklisp to find it.
Another option is to use Ultralisp.
Then it's as simple as loading the library with (ql:quickload :sigslot)
.
Usage
Signals and Slots
The general usage of signals and slots is by connecting a slot
function from a target object to a signal. First we'll have to add a signal
to our class, by calling sigslot:make-signal
:
(defclass dog ()
((barked :accessor dog-barked
:initform (sigslot:make-signal))))
(setq *dog* (make-instance 'dog))
Then we'll add a function where we EMIT
our signal. We can pass any
parameters we want and the receiving slot we'll be able to pick them
up as &REST parameters.
(defun bark (dog &key loud)
(sigslot:emit (slot-value dog 'barked) dog :loud loud))
Now comes the time to receive those barkings, so we'll CONNECT
a slot
function of some object to the barked
signal of our dog object:
;; Our slot function.
(defun on-bark (target dog &rest rest)
(let ((loud (getf rest :loud)))
(format t "Dog ~a barked~@[ loudly~]" dog loud)))
Finally, we can make our dog bark:
CL-USER> (bark *dog* :loudly t)
Dog #<DOG {10022AFBD3} barked loudly
Dispatching
By default, all signals dispatch on the thread that called EMIT
in
the order in which they were connected.
On connection you can specify the dispatching method through the :DISPATCH
keyword. The options are:
:DIRECT
:QUEUED
:MAIN-THREAD
The default is :DIRECT
and works as explained above.
If you instead prefer the dispatch to occur on the scheduler thread,
use :QUEUED
. The slot function will be queued and dispatched later on a
different thread.
Another option is :MAIN-THREAD
, which causes the slot to be dispatched on
the main thread, useful for UI apps.
Automatic disconnection
As connections from objects to signals are stored with a weak reference to the object, if it becomes garbage collected, then it is automatically disconnected. Because queued slots carry a strong reference to their object, the object won't be garbage collected until all queued slots referencing it have run.
Limitations
Due to the way slot connections are stored, there can only be one slot-object connected to a single signal. That is, you can have many objects connected to a signal through a single slot, but you cannot have the same object connect through multiple slots to the same signal. You can, though, connect a single object to multiple signals through multiple slots.
This in practice might not be such a problem, as it is not common for a single object to connect to the same signal more than once. In fact, the connection is rejected in these cases.
If you still need this behaviour, you'll have to dispatch from the slot function to the other functions.
Observers
The underlying implementation of signals is through the OBSERVABLE
class, which hosts a collection of observers. To receive a
notification from an observable object, first we need to subclass from
the OBSERVABLE
class and register our observer object:
;; Subclass from the observable mixin.
(defclass person (sigslot:observable)
((age :type fixnum
:accessor person-age
:initform 0)))
;; Create an instance of our observable person.
(setq *person* (make-instance 'person))
;; For simplicity, we'll reuse the PERSON class for our observer.
(setq *boss* (make-instance 'person))
;; Register our observer object.
(sigslot:register-observer *person* *boss*)
Then to notify our observers we'll define an UPDATE
method that will be
called by NOTIFY
and then call NOTIFY
on the observable:
;; First we'll define our UPDATE method for the observer.
(defmethod sigslot:update ((self person) (object person) &rest rest)
(format t "Boss: Happy birthday. Now get back to work!"))
;; Then we'll define an :after method for setf of age, where we can
;; add our call to NOTIFY. This is just to show how NOTIFY could be
;; called when setting a property.
(defmethod (setf person-age) :after (new-age (object person))
(sigslot:notify object new-age))
And to test it:
CL-USER> (setf (person-age *person*) 46)
Boss: Happy birthday. Now get back to work!
46
Finally, we can deregister from the OBSERVABLE
object by calling:
(sigslot:deregister-observer *person* *boss*)
What about much simpler needs?
There's always the standard method combinations using :before, :around and :after, which you can use to patch-in your observer code. It is less granular than an observer and more limited, but this might be all you need without bringing in another dependency:
(defmethod (setf person-age) :after (new-age (object person))
(format t "Common Lisp rocks!"))
Running tests
The library is tested primarily under SBCL on macOS, with some minor testing on CCL and LispWorks. If you use it with success in your projects with other compilers/architectures/OSes, please let me know so I can update this document.
To run the test suite:
CL-USER> (ql:quickload :sigslot/tests)
CL-USER> (in-package #:sigslot/tests)
CL-USER> (5am:run! 'sigslot)
Running test suite SIGSLOT
Running test REGISTER-OBSERVER ....
Running test DEREGISTER-OBSERVER ...
Running test NOTIFY-OBSERVER .
Running test DO-NOT-NOTIFY-NIL-OBSERVER .
Running test MANY-OBSERVERS .....
Running test DIRECT-CONNECT ..
Running test QUEUED-CONNECT ...
Running test DISCONNECT ..
Running test DIRECT-EMIT ..
Running test NO-EMIT-AFTER-DISCONNECT .
Did 24 checks.
Pass: 24 (100%)
Skip: 0 ( 0%)
Fail: 0 ( 0%)
License (MIT)
Copyright (c) 2022 Sebastián Benítez mailto:sebastian@ds9soft.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
System Information
Definition Index
-
SIGSLOT
No documentation provided.-
EXTERNAL SPECIAL-VARIABLE *QUEUE-SIZE*
The capacity of the task queue.
This is by default set to 20. When the queue is full, the thread calling EMIT will block until there's room for one more.
-
EXTERNAL SPECIAL-VARIABLE *UNHANDLED-CONDITION-HANDLER*
The condition handler to call when an a condition is signaled and not handled in a queued slot.
The default handler will issue a WARN with the condition.
-
EXTERNAL CLASS OBSERVABLE
An OBSERVABLE object can register multiple observers that get notified when the function NOTIFY is called.
All methods are thread safe and the UPDATE method is dispatched on the same thread where the NOTIFY method is called.
-
EXTERNAL FUNCTION MAKE-SIGNAL
Creates a SIGNAL ready to be connected to.
Ideally, you would call this function from the :INITFORM of a class slot.
-
EXTERNAL GENERIC-FUNCTION CONNECT
- SIGNAL
- SLOT
- OBJECT
- &KEY
- DISPATCH
Connects a SIGNAL to a SLOT function for a given OBJECT. DISPATCH can be one of :DIRECT (default), :QUEUED or :MAIN-THREAD.
Returns T if successfully connected to SIGNAL, otherwise NIL.
-
EXTERNAL GENERIC-FUNCTION DEREGISTER-OBSERVER
- OBSERVABLE
- OBJECT
Deregister an observer OBJECT from OBSERVABLE so it no longer receives notifications.
-
EXTERNAL GENERIC-FUNCTION DISCONNECT
- SIGNAL
- OBJECT
Disconnects a SIGNAL from a slot of the given OBJECT.
Returns T if successfully disconnected from SIGNAL, otherwise NIL.
-
EXTERNAL GENERIC-FUNCTION EMIT
- SIGNAL
- OBJECT
- &REST
- REST
Emits a SIGNAL from OBJECT to all connected slots. Additional arguments can be added to the method call and are used when calling the slot.
-
EXTERNAL GENERIC-FUNCTION NOTIFY
- OBSERVABLE
- &REST
- REST
Notify an OBSERVABLE object's observers by calling UPDATE across the entire observer collection. All extra arguments specified in REST are applied to UPDATE.
-
EXTERNAL GENERIC-FUNCTION OBSERVER-COUNT
- OBSERVABLE
Returns the number of valid observers for OBSERVABLE.
-
EXTERNAL GENERIC-FUNCTION REGISTER-OBSERVER
- OBSERVABLE
- OBJECT
Register an observer OBJECT to receive notifications.
-
EXTERNAL GENERIC-FUNCTION UPDATE
- OBSERVABLE
- OBSERVER
- &REST
- REST
Called by an OBSERVABLE object to notify an OBSERVER when an event occurred.
You MUST implement a specialization of this method for the intended OBSERVER in order to receive updates.
-