
Home - Forum - Code - Articles - Links - Files
FB6 has introduced several very powerful concepts to MUF over its development. One of these new ideas are the MUF Events that coders have access to now. This article is an extended explanation on what MUF events are, how they work, and some actual examples of them in MUF code.
Almost everything in this article is pertinent to both FB6 and ProtoMUCK, with the exception of Socket Events and any references to Proto-specific features. Though each type of event generally works the same, there are particular details about each one that need to be understood. In ProtoMUCK, there are essentially 6 types of events, though I will only cover 4 in this article. READ events, Timer events, Socket events, Passed events, Exit Events, and MCP-GUI events. I have yet to play around with MCP-GUI support enough to explain MCP events properly. The entire MCP-GUI support will be saved for a future article.
EVENT_WAITFORIn order to for a MUF program to receive an event, it must enter an EVENT_WAITFOR state. An EVENT_WAITFOR state is very much the same as a SLEEP state, in that the program is paused and pushed into the timequeue, until it is time for it to resume. The difference is that programs leave the SLEEP state once the allotted time is reached, and programs leave the EVENT_WAITFOR state once an expected event occurs. The EVENT_WAITFOR prim has the following syntax:
event_waitfor ( arrStrings -- ? strID )
It takes an array a that is a list array of strings representing the event IDs that the program is to wait for. Every event of every type has an ID that goes with it. The IDs are not necessarily unique to the individual event instance, but they can be used to identify the type of event that was received. If the array a is empty, the program will accept the first event that comes along to it. When I want the program to accept any event that comes to it, I use the EVENT_WAIT prim instead, which is essentially an in-server $define of: 0 array_make event_waitfor. The ? in the EVENT_WAITFOR syntax can represent a variety of things, and its actual data type will depend on the type of event that was received.
READ eventsWe will start with read events, since they are the most basic of the four we will cover. The following short program will serve as our example for READ events:
: main
(* This program does nothing but wait for an event.)
(* If it is a READ event, it gets the input. )
event_wait "READ" strcmp not if ( the user has entered something )
read .tell ( echo the user's input back to the user )
else
"Unexpected event received." .tell
then
;
READ events happen -any- time the user of a FOREGROUND or PREEMPT program enters anything at all. They can be used to detect that the user has entered something, but the READ prim itself must still be used to obtain that input.
In the case of READ events, EVENT_WAITFOR will return descr "READ" on the stack. "READ" is the ID string for all READ events, and descr will be the descriptor that actually entered the input. Note that this example program isn't particularly useful in and of itself. Programs 'sleep' when encountering a READ prim anyway, so there's no real efficiency benefit to the above approach to getting user input. However, used along with other events, READ events become much more interesting.
For example, Timer events. Timer events are simply a 'delayed event' that gets sent back to the program in which they were created once their time runs out. Timers are made simply by:
seconds "identifier" TIMER_START
Once the timer runs out, a Timer event is triggered, thus releasing a program from EVENT_WAITFOR if the program was waiting for Timer events. Using Timer and READ events, we can create a custom version of the TREAD prim as follows:
: timed-read
10 "TIMEOUT" timer_start ( Will return a timer event after 10 seconds )
event_wait "READ" strcmp not if (* user has entered something )
read .tell
else
"Input timed out after 10 seconds." .tell
then
;
In the case of Timer events, EVENT_WAITFOR will return systime "TIMER.identifier" on the stack. systime will be the time that the timer fell due, not when it was received by EVENT_WAITFOR. In our example above, the ID string would be "TIMER.TIMEOUT". The above program simply sits in an EVENT_WAITFOR state until either a read event occurs or the timer event falls due after ten seconds and takes the program out of its EVENT_WAITFOR state.
At this point it may be useful to explain that events will queue up on a program if they are not immediately handled by EVENT_WAITFOR. The number of pending events for a program can be obtained using the EVENT_COUNT prim. Or if you are looking to see if a certain event in particular is pending, you can use EVENT_EXISTS to specify the event ID you are looking for. Some events (timer events and passed events) will queue each unique instance that comes along, where as others (read events and socket events) will only queue up one instance at a time, waiting until the one is handled before adding any future events of that type.
Altering the above program slightly:
: timed-read
10 "TIMEOUT" timer_start
event_wait "READ" strcmp not if
read .tell
12 sleep ( After getting the user input, sleep 12 seconds )
event_wait ( The Timer event that fell due earlier will be caught )
else
"Input timed out at 10 seconds." .tell
then
;
This could lead the problems if Timer events are not properly taken care of. For example, if we are using a loop to read in input, it's possible that a timer from a previous iteration of the loop falls due, making it so that the EVENT_WAITFOR exits well before the 10 second timeout. To keep this from happening, there is the TIMER_STOP prim which simply accepts an ID string of the timer to stop. The "TIMER." part is assumed, so you simply use the ID that you made up for the timer. Fixing our example to clean up the timeout timer, we would finish it as:
: timed-read
10 "TIMEOUT" timer_start
event_wait "READ" strcmp not if
read .tell
"TIMEOUT" timer_stop ( stop the timer now, no longer wanted )
else
"Input timed out at 10 seconds." .tell
then
;
The next type of event is Socket events (feature of Proto 1.80 and newer). Socket events are very much like READ events, in that they inform the program that there is a MUF socket with pending information.
For example:
: read-loop[ sock:theSock -- s0 ... sn n ]
(* keeps pushing received strings onto the stack, ending with the count )
var count
begin ( loops until NBSOCKRECV returns -1, indicating end of input )
theSock @ nbsockrecv swap 0 < if count @ exit then
count ++
repeat
;
: main ( s -- )
":" split atoi SOCKOPEN "noerr" strcmp if ( socket not connected )
"Connection down." .tell exit
else ( connection made )
var! MySock
event_wait "SOCKET.READ" strcmp not if ( a socket event. )
read-loop
else
"Unexpected event." .tell
then
then
;
In the case of Socket events, EVENT_WAITFOR will return socket "SOCKET.READ" for normal MUF sockets and socket "SOCKET.LISTEN" for listening sockets, where socket is the specific MUF socket that has the pending information. Notice that once it's made known that the socket has information waiting to be received, I loop in the read-loop until there is no more information to be read in. This is the most efficient way of handling getting information from a MUF socket since the program will sleep until there is actual data waiting to be received, and then the NBSOCKRECV can be used in a loop until all that data is read in at once. It is not efficient to just read in 1 line at a time from NBSOCKRECV and go back to EVENT_WAITFOR, since that means the MUF program gets put back into the timequeue between every single line of input.
In the case of listening sockets, a Socket event indicates that there is a pending connection waiting to be accepted via SOCKACCEPT.
When working with Socket events, it is important to remember that only one MUF instance can catch events from each unique MUF socket. By default, this instance is the process in which the socket was made. In other words, if you open a MUF socket in one routine, then fork off a second routine to work with the socket, that second routine won't catch Socket events on that socket by default. In order to control which process recieves the events on a socket, the use of the SET_SOCKOPT prim with the argument of HOMEINSTANCE is used.
The following example should help explain this a little better:
: do-childfunc[ sock:theSock -- ]
( This function is only called from the forked off instance. )
( Initially, it is unable to catch socket events on theSock. )
theSock @ HOMEINSTANCE set_sockopt ( now this child process will catch future socket events on this socket. )
begin
event_wait "SOCKET" instr if
do receive loop, etc
then
repeat
;
: main
"someserver.net" 8888 sockopen pop
( now just a connected socket on the stack )
fork not if exit then ( parent process closes )
do-childfunc
;
Socket events are a @tuneable feature (socket_events), so if you want to use them, make sure they are enabled. They are still being adjusted and worked on, so the exact implementation may vary slightly by the time Proto 1.80 is actually released.
It is possible for a process to be notifed when another process finishes running. The WATCHPID prim makes it so that the process that uses it will be sent an event when the process it is watching for terminates.
Here's an example of using this feature with the FORK primitive:
: main
...
fork not if
do-childfunc exit
else
watchpid ( parent process will now get an event when the child ends )
then
event_wait "PROC.EXIT" instr if
"Child function has ended." .tell
else
"Unexpected event recieved." .tell
then
;
In the program above, the instance forks at the FORK prim and the child process simply calls the do-childfunc function. The parent process uses the WATCHPID prim using the child process's PID and then enters an EVENT_WAITFOR state.
When the child function ends, the parent process recieves an event.
EVENT_WAITFOR returns pid "PROC.EXIT.pid" on the stack. For example, if the initial process had a PID of 100, and the child process a PID of 101, then event_waitfor would return 101 "PROC.EXIT.101" when the child process exits.
As of ProtoMUCK 1.80, the pid pushed onto the stack will be replaced by -1 if the watched for process aborts rather than exiting cleanly. For example, if EVENT_WAITFOR returned -1 "PROC.EXIT.101", that would indicate that the child function had encountered an abort error before completing.
Something to keep in mind when working with Exit events: If you use WATCHPID on a PID that doesn't exist yet, then the event gets sent back immediately.
So it's important to keep in mind that processes created by QUEUE or FORK or
similiar prims don't technically exist immediately, but rather get pushed
into the timequeue as the next process to get run when the current one
finishes its timeslice. As a result, the above example program wouldn't
actually work as you would expect, because the WATCHPID would trigger an
immediate event that EVENT_WAITFOR would then catch. In order to allow the
expected PID to actually exist, it is necessary to force the current program
to complete its timeslice so that the new process can be created.
Fortunately, the sleep prim will do this for us.
Changing the above program to actually work as expected:
: main
...
fork not if
do-childfunc exit
else
0 sleep ( necessary to allow the child process to be added to the timequeue. )
watchpid
then
event_wait "PROC.EXIT" instr if
"Child function has ended." .tell
else
"Unexpected event recieved." .tell
then
;
Exit events can be particularly useful if there's a program that you want to make sure is always running no matter what, since you could have another simple process that simply wakes up when the PID in question exits and requeues it before going back into a waiting state on the new PID.
The final event type is the real meat of 'Inter-Process Communication'. They are events that are sent from one running process to another via the EVENT_SEND prim.
EVENT_SEND has the syntax:
event_send ( intPid strID ? -- )
Where intPid is the PID to send the event to, strID is the ID string for this event, and ? is any stack data object you want to send to the other process. For example:
(** Program 1, running under PID of 123 **)
: main
...
event_wait "USER" instr if (a passed event)
"data" array_getitem ( OurResult from Program 2 is now on the stack )
...
then
...
;
(** Program 2 running under PID of 321 **)
: main
...
var! OurResult
123 "THERESULT" OurResult @ event_send
...
;
In our program above, Program 1, running under the PID of 123, performs a series of operations and eventually hits the EVENT_WAIT, where it pauses.
Program 2 performs a series of steps, such as calculating a result, reading something in from a socket, building an array, or any number of other operations, and places the final result in the OurResult variable. It then uses EVENT_SEND to send the result to process #123 (program 1's instance).
Program 1 then resumes, now with the stack object from program 2 accessible.
In the case of 'Passed Events', EVENT_WAITFOR returns: dictionary "USER.identifier". The dictionary contains the following fields:
DATACALLER_PIDCALLER_PROGSo in our above example, EVENT_WAITFOR would return
{"data":value of OurResult, "caller_pid":321, "caller_prog":program 2} "USER.THERESULT".
By using passed events, data can be exchanged among running processes. MUF arrays can be passed perfectly fine via EVENT_SEND, making it possible to send as much data to the other process as desired. For example:
depth array_make 123 swap "THESTACK" swap event_send
would send the entire stack of the currently running process as an array to the PID of 123.
At first glance, many may not recognize the power of MUF events, but it is a good idea to keep them in mind for advanced MUF projects. They have been the perfect answer to many of the more challenging MUF questions coders have come to me with over time, and I encourage programmers to consider their utility before just dismissing them.
This article was compiled from a conversation I had with a friend, man EVENTS, man TIMER_START, man TIMER_STOP, man EVENT_WAITFOR, man EVENT_SEND, man EVENT_COUNT, and man EVENT_EXISTS. If you need further help with using MUF events, feel free to post your questions to The MUF Den message board or visit the ProtoMUCK mailing list.