This is an old revision of the document!


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)

And these execution stacks are not allowed:

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

One may get away with the latter of those two, but the former really poses a problem, and this distinction really can't be made beforehand - hence the error when trying to use $input, etc. 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, resulting in the disallowed cases above. mIRC isn't really prepared to handle this, resulting in potentially undefined behaviour; in practice, mIRC does its best to save itself by delaying the execution of critical events until the original critical event is done executing - 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.

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.
critical_events.1318888295.txt · Last modified: 2018/08/17 12:36 (external edit)
 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki