Critical events and the pause problem

AuthorSaturn 2006/09/15 16:06

First of all, mIRC's scripting engine is single-threaded. There is only one thread that executes script code… ever.1) Saying anything about "the same thread" or "a new thread" with respect to script code is utter crap.

The explanation as to why in some scenarios it seems that multiple scripts can be executed in "parallel", requires knowledge about how Windows GUI programs, and in particular message queues work. At the heart of every Windows GUI application lies the main message loop, an endless2) loop that repeatedly does the following: 1) get a message from the message queue, 2) call one or more functions that handle the received message. Typical messages include "the user pressed a key" (WM_KEYDOWN), "a window needs to be redrawn" (WM_PAINT), "a timer has gone off" (WM_TIMER), "some data has been received on a socket connection" (user-defined by WSAAsyncSelect).. There are hundreds of message types, and on top of that an application is free to process any additional messages (e.g. WM_MCOMMAND and WM_MEVALUATE) as it sees fit. The functions that process these incoming messages are implemented in part by the application, and in part by Windows - e.g. drawing a letter in an editbox upon receiving WM_KEYDOWN is done by Windows, but processing of the Enter key (received as WM_KEYDOWN message as well) is done by mIRC.

mIRC is therefore also message queue based, and, as a result, every piece of script that gets executed is the result of receiving a message. In other words, mIRC's script engine is always called (directly or indirectly) from message processing code. Message queue processing is always single-threaded, meaning that while a message is processed in a certain function, no other messages can be processed at the same time - step 1 of the above message loop is not repeated until the previous step 2 is done. This explains why a script that runs in an infinite loop also blocks the screen from being updated or keys from being processed, etcetera.

Now comes the important part. Identifiers like $input and $dialog, the WhileFix DLL, and any script-delaying COM-based snippets (/sleep, /xrun, $auth to name a few) all use a trick to process new messages while not returning from the current message-processing function: they call the message loop to process messages from within the code that is processing the current message. For this snippet we get a current execution stack that looks more or less like this:

[mirc's main message loop] ->
  [message processing code] ->
    [script engine] ->
      [wscript.shell COM handling DLL code] ->
        [embedded message loop] ->
          [message processing code] ->
            [script engine]

Let's define "event" as the arrival and processing of an incoming message from the message loop. All of mIRC's Remote events are such events (most of them being the result of receiving a WinSock "data available on socket" message for an IRC server connection), but in this context, aliases called from the commandline ("user pressed Enter key"!), and timer executions (WM_TIMER) are events too.

Now, in general, mIRC makes a distinction between two different types of events, which I call critical and non-critical events. Critical events are events that are not allowed to be interrupted by non-critical events; "interruption" as in having the message loop executed from within the event processing code, as in the example above. Critical events include all events caused by receiving data from IRC servers; non-critical events include commandline-called aliases, timers and signals. You can find out whether an event is critical or not, by (for example) trying to use $input from it - mIRC will spit out an error if the event is critical.

The reason for the distinction is that in general, critical events simply cannot be interrupted. There are various reasons for this, one of them being that mIRC's internal data structures should not be touched from certain contexts. For example, consider the "on QUIT" event, and consider what could happen if mIRC were to process an "on JOIN" message for the same nickname before updating its own internal data structure (see also /updatenl).

On the other hand, non-critical events do not have this problem, and have no problem calling the message loop from within them. Hence, these are some (simplified) examples of allowed execution stacks:

[message loop] -> [critical event]
[message loop] -> [non-critical event]
[message loop] -> [non-critical event] -> [message loop] -> [critical event]
[message loop] -> [non-critical event] -> [message loop] -> [non-critical event]
[message loop] -> [non-critical event] -> [message loop] -> [non-critical event] -> [message loop] -> (etc)

In contrast, this execution stack must never occur:

[message loop] -> [critical event] -> [message loop] -> [critical event]

Fortunately, mIRC is smart enough not to allow nesting of two critical events like that. To that end, mIRC postpones processing other critical events until the current critical event is complete. As a result, mIRC will never process incoming events from sockets to IRC servers in a nested fashion, and so the JOIN-after-QUIT case sketched above can never occur in practice.

That leaves this third category, for which it is less clear whether it should be allowed or not:

[message loop] -> [critical event] -> [message loop] -> [non-critical event]

That case would be technically possible. However, deferring the processing of incoming socket messages is never a good idea anyway. For example, if $input is used from an "on TEXT" event, and the user does not actually provide any input, then mIRC will not respond to any incoming "PING" requests from IRC servers for that duration either. The end result is that the IRC connection will time out. It is for that reason that mIRC forbids the use of script-blocking identifiers such as $input and $dialog from critical events.

Unlike $input, both the WhileFix DLL and COM-based script delaying snippets do not make the distinction between critical and non-critical events, so you can use them from critical events as well. Again, in that case no critical events will be processed from the nested message loop - so in the case of a COM-based script delaying snippet for example, no critical events will be processed until the snippet times out and returns. However, non-critical events may indeed be processed (the third category above). At that point it is entirely up to the scripter to prevent that the scripted delays cause problems for the IRC connections.

Again, all of this happens from a single thread: mIRC's main thread. If mIRC were multithreaded, everything would be different. Hence, these are issues related to mIRC's internal design, which is not documented anywhere. Snippets like the one here really push the limits with respect to what can be done without breaking the internal execution model.

1) Even identifiers like $comcall/$dllcall create a new thread, then do their work in that thread, and deliver back the results for execution in the original thread.
2) Until the application chooses to break out of it, which means that it's about to exit.