Queueing+event+interrupts

//by Richard Russell, June 2006; amended June 2009 and December 2011//

//**Consider using the EVENTLIB library instead of the code listed in this article.**//

The **ON CLOSE**, **ON MOUSE**, **ON MOVE**, **ON SYS** and **ON TIME** statements //interrupt// your program when one of the specified events occurs. Perhaps the most obvious way to use these statements is to cause a procedure (an //interrupt service routine//) to be called when the interrupt happens, as follows:

code format="bb4w" ON SYS PROCsys(@wparam%,@lparam%) : RETURN code It is important that any event parameters in which you are interested (**@msg%**, **@wparam%** and/or **@lparam%**) are read //in the statement immediately following the **ON** statement// otherwise they might have changed as a result of a subsequent interrupt. For example the following code will __not__ work reliably:

code format="bb4w" ON SYS wp%=@wparam%:lp%=@lparam%:PROCsys(wp%,lp%):RETURN : REM Don't do this! code Although **wp%** //will// contain the @wparam% parameter relevant to the ON SYS call, **lp%** //may not// contain the relevant value of @lparam%, because another interrupt may have occurred in between.

So with care it is possible to ensure that every event is processed by your program, with no chance of any being missed, but //**not necessarily in the order in which they occurred!**// To see why, consider the following code:

code format="bb4w" ON SYS PROCsys(@wparam%) : RETURN

DEF PROCsys(W%) PRINT W%     ENDPROC code Suppose two ON SYS interrupts happen in quick succession, with @wparam% values of 1 and 2 in that order. The first will result in PROCsys being called, but before the PRINT statement gets a chance to be executed the second interrupt will occur. So the actual sequence of events will be:

resulting in the following output:

code 2        1 code So the events were processed in the opposite order to that in which they occurred!

Often this won't matter, and indeed in many circumstances it will be irrelevant because there is no likelihood of two interrupts happening sufficiently close together. For example this will be the situation if you are using **ON SYS** only to respond to menu selections, since you should only get one interrupt for each selection. In such a case you can safely use an alternative approach where the interrupt simply sets a global variable that you can poll elsewhere:

code format="bb4w" ON SYS Click% = @wparam% : RETURN

Click% = -1 REPEAT temp% = INKEY(1) IF temp%=-1 SWAP temp%,Click% CASE temp% OF         WHEN ....          WHEN ....          REM. etc.       ENDCASE UNTIL FALSE code This routine conveniently uses **INKEY** as both a delay (to avoid using too much CPU time) and to monitor for keypresses, which is handy for implementing keyboard shortcuts for menu items.

However life isn't always this easy! Occasionally it may be necessary to ensure that every event is registered, even if two or more occur in very quick succession, **and** to ensure that the events are processed in the order in which they happen. An example might be handling events from a dialogue box. One way of dealing with this is to implement a First In First Out **queue** of events which can be //written// as the events occur (however fast) and //read// as they are processed, even if relatively slowly.

At first thought this doesn't sound too difficult - creating a FIFO queue in software is straightforward - but there is a major difficulty: we must transfer the event into the queue **//in just one statement//**. If we don't it won't work: either data could be lost or the events could be processed in the wrong order! Achieving this sounds like a tall order, but it can be done.

Method 1: Using an array
For a queue with six entries the code for writing into the queue would be as follows:

code format="bb4w" DIM Q%(6)

ON SYS Q%=Q%(0)+1,@wparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5) : RETURN code This may need a little explanation. To do it all in a single statement we use the ability to load an entire array from a comma-separated list of values. The clever bit is that some of the values we load into the array depend on elements of that same array, as follows:
 * Q%(0) = Q%(0)+1
 * Q%(1) = @wparam%
 * Q%(2) = Q%(1)
 * Q%(3) = Q%(2)
 * Q%(4) = Q%(3)
 * Q%(5) = Q%(4)
 * Q%(6) = Q%(5)

Hopefully you can see what is happening here. Elements Q(1) to Q(6) act as a **shift register**: each time an event occurs the previously-stored data is shifted one place along the queue (the data in element Q%(6) is discarded) and the new data is stored in Q%(1). Element Q%(0) is loaded with the value Q%(0)+1, in other words it is **incremented**. This zeroth element of the array acts a **pointer** to the oldest event stored in the queue.

So by using this cunning method we manage to store each event into the queue, and increment a pointer, in just one statement! How, then, do we read the data out? This is the code:

code format="bb4w" WHILE Q%(0) event% = Q%(0)<=DIM(Q%,1) AND Q%(Q%(0) AND Q%(0)<=DIM(Q%,1)) Q%(0) -= 1 REM Do something with event% ENDWHILE code Firstly we examine **Q%(0)**, which is the pointer. If this is zero the queue is empty and we need take no further action. If it is non-zero there is at least one event in the queue. The next line reads the //oldest// event in the queue, by using **Q%(0)** as the subscript; the comparisons ensure that a 'Bad subscript' error doesn't occur if the queue overflows.

Note that as Q%(0) is accessed //and// the queue's contents retrieved in the same statement, there is no possibility of reading the wrong event. Even if another interrupt has occurred since Q%(0) was tested in the previous line it makes no difference: the oldest event will always be read.

Finally the next line simply decrements the pointer, so it points to the next event to be read (if any), and leaves the other elements in the array unchanged. Again, it doesn't matter if another interrupt occurs between the previous statement and this one.

The array can, of course, be any length (within reason). If events occur more quickly than they can be processed the queue will eventually fill and the oldest event(s) will be discarded; in that case **event%** will be set to zero.

If you need to know the value of **@msg%** and **@lparam%** as well as **@wparam%** you can extend the technique as follows. To write into the queue: code format="bb4w" DIM Q%(18)

ON SYS Q%=Q%(0)+3,@msg%,@wparam%,@lparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5),...,Q%(15) : RETURN code To read from the queue: code format="bb4w" WHILE Q%(0) lpar% = Q%(0)<=DIM(Q%,1) AND Q%(Q%(0)-0 AND Q%(0)<=DIM(Q%,1)) wpar% = Q%(0)<=DIM(Q%,1) AND Q%(Q%(0)-1 AND Q%(0)<=DIM(Q%,1)) msg% = Q%(0)<=DIM(Q%,1) AND Q%(Q%(0)-2 AND Q%(0)<=DIM(Q%,1)) Q%(0) -= 3 REM Do something with msg%, wpar% and lpar% ENDWHILE code The maximum length of queue that can be fitted into one line is 11 events (33 values): code format="bb4w" DIM Q%(33)

ON SYS Q%=Q%(0)+3,@msg%,@wparam%,@lparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5),...,Q%(30) : RETURN code To create a longer queue split the line using line-continuation characters (//BBC BASIC for Windows// version 5.91a or later only): code format="bb4w" DIM Q%(99)

ON SYS Q% = Q%(0)+3,@msg%,@wparam%,@lparam%,Q%(1),Q%(2),Q%(3),Q%(4),Q%(5),Q%(6),Q%(7),Q%(8),Q%(9),Q%(10),Q%(11),Q%(12),Q%(13),Q%(14),Q%(15),Q%(16),Q%(17),Q%(18),Q%(19),Q%(20),Q%(21),Q%(22),Q%(23),Q%(24),Q%(25),Q%(26),Q%(27),Q%(28),Q%(29),\ \ Q%(30),Q%(31),Q%(32),Q%(33),Q%(34),Q%(35),Q%(36),Q%(37),Q%(38),Q%(39),Q%(40),Q%(41),Q%(42),Q%(43),Q%(44),Q%(45),Q%(46),Q%(47),Q%(48),Q%(49),Q%(50),Q%(51),Q%(52),Q%(53),Q%(54),Q%(55),Q%(56),Q%(57),Q%(58),Q%(59),Q%(60),Q%(61),Q%(62),Q%(63),\ \ Q%(64),Q%(65),Q%(66),Q%(67),Q%(68),Q%(69),Q%(70),Q%(71),Q%(72),Q%(73),Q%(74),Q%(75),Q%(76),Q%(77),Q%(78),Q%(79),Q%(80),Q%(81),Q%(82),Q%(83),Q%(84),Q%(85),Q%(86),Q%(87),Q%(88),Q%(89),Q%(90),Q%(91),Q%(92),Q%(93),Q%(94),Q%(95),Q%(96) : RETURN code Using this technique you can make the queue as long as you like, within reason. However, requiring a very long queue is suggestive that you may be able to find a better solution by restructuring your program. For example you may be able to increase the frequency with which you poll the queue, or use an interrupt approach rather than polling.

Method 2: Using a string
This method is easier to understand than the foregoing one, and the maximum practical queue length is greater (over 5000 events), but it is more expensive of CPU time and memory.

The code for writing into the queue is as follows:

code format="bb4w" Queue$ = "" !^wParam$ = ^@wparam% : ?(^wParam$+4) = 4 ON SYS Queue$ += wParam$ : RETURN code Here **wParam$** is a global string variable containing four characters, corresponding to the 4-byte (32-bit) value of **@wparam%**.

This is the code for reading the data out:

code format="bb4w" WHILE Queue$<>"" event% = !!^Queue$ Queue$ = MID$(Queue$,5) REM Do something with event% ENDWHILE code If events occur more quickly than they can be processed the queue will eventually fill and a **String too long** error will result.

If you need to know the value of **@msg%** and **@lparam%** as well as **@wparam%** you can extend the technique as follows. To write into the queue: code format="bb4w" Queue$ = "" !^iMsg$ = ^@msg% : ?(^iMsg$+4) = 4 !^wParam$ = ^@wparam% : ?(^wParam$+4) = 4 !^lParam$ = ^@lparam% : ?(^lParam$+4) = 4 ON SYS Queue$ += iMsg$ + wParam$ + lParam$ : RETURN code Here **iMsg$**, **wParam$** and **lParam$** are global string variables containing the values of **@msg%**, **@wparam%** and **@lparam%** respectively.

To read from the queue:

code format="bb4w" WHILE Queue$<>"" event$ = LEFT$(Queue$,12) Queue$ = MID$(Queue$,13) event% = !^event$ msg% = event%!0 wpar% = event%!4 lpar% = event%!8 REM Do something with msg%, wpar% and lpar% ENDWHILE code Note that the use of the temporary string **event$** is important because the memory address of the string **Queue$** alters as it changes length, and so could change between reading the **msg%**, **wpar%** and **lpar%** values.