5.3. Event Handling

An event is a change to which the system responds. Events include interprocess communication (message exchanging between processes) events, signals, timer events (execution of a block of code at specified time), Graphical User Interface events (clicking on the screen buttons), and datahub exceptions (a change in value of a datahub point).

Gamma provides a generalized event handling capability which processes all these events. A Gamma function that responds to an event is called an event handler, or a callback. A pair of functions, next_event and next_event_nb, invoke the event handler, that is, any callback function(s) that have been attached to the event. For example, the simple construct:

while ( t) 
{
    next_event ( );
}
	  

placed at the end of the file, together with the set of callback functions defined in the application to handle all the required events, creates a purely event-driven Gamma program. However, unlike typical event-driven systems applications (such as many GUI-based applications), the user has here the opportunity to further process the events, providing a conditional which is more complex than an infinite loop, or even choosing not to receive events. The events will not be processed (or callbacks invoked) until one of the next_event calls are made.

The result of next_event is the result of executing the callbacks, or nil if no event handler has been defined (that is, no callbacks have been attached). The result of next_event_nb (nb stands for non-blocking) is the same except that nil is also returned if no event was available.

Attaching callbacks and receiving events depends on the type of event. For example, in Photon the function PtAttachCallback is used to attach actions to clicking on buttons. Normally one does not wait for an explicit event type, that is, the next_event call will process ANY event defined within the system. The following sections describe how some common event types are handled.

5.3.1. Interprocess Communication Message Events

Gamma uses a send/receive/reply (SRR) mechanism to provide interprocess communication via messages. In this context, a message is any valid Gamma or Lisp expression passed synchonously from one process to another, using the send function. The engine treats the message as a null-terminated character string in Lisp syntax (Gamma's internal representation), which it parses and evaluates. Any expression may be transmitted in this way, including function definitions, function calls, variable names and complex blocks of code. The reply returned is the result of the evaluation.

This process is transparent to the application. The Gamma engine evaluates the incoming message inside an implicit try/catch block to ensure that externally originated expressions cannot accidentally affect the running program. Any errors that occur while the message is being evaluated will be indicated in the return value for the message, but the overall running status of the engine will not be otherwise affected.

For more information see the Interprocess Communication section in the Special Topics chapter.

5.3.2. Timers

A timer is a Gamma expression that is submitted for evaluation at specified time in the future. Timer related events are set up through the every and after functions which provide a relative time delay, and the at function which accepts an absolute time. These functions accept timing parameters and a block of code that will be evaluated when the timer expires. The code can be simply a function name, or can be an entire expression to be evaluated.

A timer will only be handled during a call to next_event, next_event_nb or flush_events. If a timer expires while another operation is being performed, the engine will evaluate the timer code at the next event handling instruction.

In Gamma, by default, timers are internally handled by using proxies. A proxy is non-blocking system message that does not require a reply. Gamma can act on a proxy immediately, or delay a little while in order to finish what it is currently doing. It is this 'little while' that becomes the limit of the accuracy of the timers in Gamma when they are driven from proxies. You see, most programs written in Gamma are run through an event loop. The maximum time a proxy-based timer can be delayed is the execution time of the longest path of code attached to an event. Since this quantity is totally dependent on how your code is written and the speed of your CPU, it is difficult to put an exact number on the practical accuracy of Gamma's proxy-based timers. The maximum resolution of the timers is equal to the OS tick size setting.

5.3.2.1. Setting a timer

A timer is created when any of the following functions are called:

The after timer is used to evaluate a piece of code after a given amount of time. It is a one-shot timer that [quot ] goes away [quot ] after being fired.

The every timer is used to evaluate a piece of code at a specified interval.

The at timer evaluates the associated code when the current time matches the profile created by six arguments to the function.

For syntax and examples see the Reference Manual.

5.3.2.2. Canceling a Timer

All timer functions return a timer_id that can be used to cancel the timer. To do so, the cancel function is called, using the timer-id for its argument.

5.3.2.3. The TIMERS variable:

On startup of Gamma a variable called TIMERS is initialized. This variable is an array of all the timers currently in the system. Initially, its value is [lsqb ] [rsqb ] (an empty array) but as timers are added to the system this array grows.

Gamma> TIMERS;
[]
Gamma> a = every(3,#princ("Hello\n"));
1
Gamma> >TIMERS;
[[860700531 176809668 3 ((princ "Hello\n")) 1]]
	  

The TIMERS variable was initially an empty array. Once the first timer was added the TIMERS variable contained information on the first timer. The contents of each element can be summarized as:

second nanosecond repeat function timer_id
		

The second is the number of seconds since Jan. 1, 1970 which is compatible with the clock and date functions. The nanosecond is the fraction of the second. Combining these two times gives an accurate time that identifies the next time the timer will fire. The repeat is nil if the timer is a once-only [quot ]after[quot ] timer or an [quot ]at[quot ] timer. Otherwise, this is the period of an [quot ]every[quot ] timer. The next item is the code that is evaluated when the timer fires. The last item in this sub-array is the timer-id.

This array grows as timers are added to the system:

Gamma> b = every(5, #princ("Test\n"));
2
Gamma> _timers_;
[[860700531 176809668 3 ((princ "Hello\n")) 1]
[860700563 494732737 5 ((princ "Test\n")) 2]]
Gamma> 
		

An important point to remember is that the TIMERS variable is an array, and therefore can be referenced as such. To reference the first element in the TIMERS array, use TIMERS [0] just like a regular array reference. Altering the TIMERS array can cause unpredictable behavior, and should be avoided.

5.3.2.4. Blocking timers from firing

Sometimes a segment of code is written which must be executed non-stop, without an interruption from timers. To accomplish this, the code is wrapped between block_timers and unblock_timers functions. In this way the code will be safe from interruption from timers. This blocking is not necessary unless timers have been bound to signals rather than proxies.

5.3.2.5. timer_is_proxy function

This function controls how timers are fundamentally handled within Gamma. By default, timers are handled by the processing of proxies. This allows Gamma to delay the timer, if necessary, when a critical system process is occurring.

Calling the timer_is_proxy function with nil makes all timers operate by using signals. In the QNX 4 operating system, for example, SIGALRM is used, and the attached code is run as a handled signal. Running timers via signals has some very dramatic consequences. First and foremost, when running in this mode all timer code must be signal safe. This status must be analyzed with caution, as most code is not signal safe. This mode should be avoided except in very rare circumstances.

5.3.3. Symbol Value Events (Active Values)

Gamma has the capability to generate an event when a variable is modified in any way. A variable that can trigger an event is called an active value. The code that is executed when the variable changes is called the set expression that is effectively a callback.

The add_set_function permits attaching an expression to any defined variable. (It is an error to attach a set expression to a constant symbol.) When the value of a symbol changes, either by being declared within a sub-scope or by being explicitly changed using [equals], [colon ] [equals], [minus ][minus ], or [plus ] [plus ], the Gamma engine checks the symbol for the existence of an associated set expression. If a set expression exists, then it is executed directly after the value of the symbol is changed. This set expression can be any valid Gamma code, and is evaluated within its own sub-scope. The symbols value, previous and this are defined within this sub-scope to be the current value of the symbol, the previous value of the symbol, and the symbol itself respectively. If an active value causes another active value to change, then that new active value's set expression is also evaluated. This provides a very simple and powerful means by which forward chaining algorithms such as those in expert systems can be implemented.

Active values provide a very powerful way of constructing an event-driven application with a high degree of cohesiveness. Often, there is some functionality that is related to a derived variable, not an event itself. The function can be attached to that internal variable, decoupling it from the event, and generating clearer and more concise code. In the following example, we wish to attach an action to an alarm condition, which in turn is generated by a change in a measured variable (e.g., the temperature). The # sign protects an expression from evaluation, causing it to be treated as a literal. (We discuss Deferring Expression Evaluation in more details later, in the chapter on Advanced Types and Mechanisms).

Gamma> temp = 35;
35
Gamma> hi_limit = 40;
40
Gamma> function check_temp (tp)
     {if (tp > hi_limit) alarm_hi_on = t;
      else alarm_hi_on = nil;}
(defun check_temp (tp)
  (if (> tp hi_limit)
    (setq alarm_hi_on t)
    (setq alarm_hi_on nil)))
Gamma> function print_alarm ()
     {if (alarm_hi_on == t) princ("Alarm is on: Temp is over ",hi_limit, ".\n");
      if (alarm_hi_on == nil) princ("Alarm is off.\n");}
(defun print_alarm ()
  (progn (if (equal alarm_hi_on t)
            (princ "Alarm is on: Temp is over " hi_limit ".\n"))
         (if (equal alarm_hi_on nil)
            (princ "Alarm is off.\n"))))
Gamma> add_set_function(#temp,#check_temp(temp));
(check_temp temp)
Gamma> add_set_function(#alarm_hi_on, #print_alarm());
(print_alarm)
Gamma> temp = 38;
Alarm is off.
38
Gamma> temp = 39;
39
Gamma> temp = 42;
Alarm is on: Temp is over 40.
42
Gamma> 

5.3.4. Cascade DataHub Point Events (Exception Handlers)

The Cascade DataHub is a data collection and distribution center designed for easy integration with Gamma. A point is a name for data in the Cascade DataHub. An exception handler is a Gamma expression that is attached to a point. The Cascade DataHub can asynchronously transmit any number of point values to a Gamma program, which will then automatically update the value of a symbol named the same as the datahub point.

The add_exception_function and add_echo_function functions permit an application to bind an expression to a datahub point exception in a similar way to add_set_function above. If an application can both write a point and receive exceptions from that point, then the datahub will [quot ]echo[quot ] the exception originated by the application. The two functions make it possible to distinguish between these two situations. Only the originating task will see a point exception as an echo, while all other tasks will see a normal exception.

In addition, the programmer may attach any number of expressions to the symbol, to be evaluated when the datahub point changes. The expressions are evaluated within a sub-scope, with the special symbols: value, previous and this defined to be: the current value of the point, the previous value of the point and the point name itself as a symbol. An exception handler is only triggered during a call to next_event, next_event_nb or flush_events.

5.3.5. Windowing System Events

Gamma's GUI support offers PtAttachCallback for Photon and AttachCallback for X Windows that permit attaching callbacks to any GUI event. Like the other event handling functions, the user can in fact bind any Gamma expression for execution when the event occurs.

5.3.5.1. GUI Event Handlers (Callbacks)

A GUI event handler, also known as a callback, is an arbitrary expression attached to a particular callback of a widget. A callback may occur whenever a call is made to next_event, next_event_nb, and flush_events. If the appropriate GUI event has occurred, then the Gamma engine automatically evaluates any callback handlers that deal with the event attached to the particular widget. This results in essentially asynchronous program flow, where the callback may occur, from the user's perspective, at any time during the program execution. In reality, the GUI event is only handled if the system has been instructed to deal with one or more incoming events.

5.3.6. Signals

Signals are the traditional method of asynchronous communication between tasks, in which no data is transferred. A signal handler is an expression attached to an operating system signal, which is delivered asynchronously to the running program. A signal handler is attached by a call to the signal function.

A signal pre-empts any activity except garbage collection, and causes control flow to enter the signal handler. The signal handler should not call non-reentrant functions. It is safe for a signal handler to make a call to the error function, which will throw flow control to the nearest error handler.

Gamma supports the following signals:

SIGABRT, SIGBUS, SIGCHLD, SIGCONT, SIGDEV,
SIGEMT, SIGFPE, SIGHUP, SIGILL, SIGINT,
SIGIO, SIGIOT, SIGKILL, SIGNAL_HANDLERS,
SIGPIPE, SIGPOLL, SIGPWR, SIGQUIT, SIGSEGV,
SIGSTOP, SIGSYS, SIGTERM, SIGTRAP, SIGTSTP,
SIGTTIN, SIGTTOU, SIGURG, SIGUSR1, SIGUSR2,
SIGWINCH
	  

For the description of signal values see the signal entry in the Reference Manual.

5.3.6.1. block_signal & unblock_signal

There are times when certain portions of code must not be interrupted by certain or all signals. Use the block_signal and unblock_signal functions to protect a process.