Programming for non programmers
David Holden continues his series.In this session we're going to write a program that interacts with the operating system in a more complex way and also learn how to make it into a crude multi-tasking application.
Who else is out there?
In a multi-tasking environment there will be a number of programs running simultaneously. These are normally referred to as Tasks or, in the context of the desktop or Wimp environment, Wimp Tasks.
These are managed by a program called the Task manager, and it is possible to ask this about them. To do this we call a SWI with th rather long name of TaskManager_EnumerateTasks. This name follows the usual convention with the first part describing the part of the OS that 'owns' the SWI and the part after the '_' the nature of the call.
This SWI will tell us about all the currently active Wimp tasks. This is quite a lot of information, and this information will be returned in a block of RAM. It is up to the calling program to make a block of RAM available for this, and to tell the Task Manager where it is and how big it is. These values are passed in registers R1 and R2 when we call the SWI, with R1 holding a pointer to the RAM or buffer as it usually called and R2 the amount of RAM or buffer length. For the way we are going to use this call R0 should hold zero.
So the syntax of the call when used from Basic would be -
SYS "TaskManager_EnumerateTasks",0,<pointer to buffer>,<buffer length>
It is up to the calling program to make sure that the buffer is big enough to hold all the data. When the call returns then, in the way we have used it, the registers will contain the following information.
R1 = Pointer to first unused byte in the buffer
The information returned about the Tasks will be placed in the buffer. This consists of a series of 16 byte blocks, each of which is made up of four four byte words.
The first word holds the Task Handle. This is a unique number assigned to a Task by the Wimp when it starts up. It is the 'ID' by which a Task is identified. Tasks can't just be addressed by their name because there could be more than one copy of a program running at once. For example, you might be running two copies of !Paint. They would both be called 'Paint' but each would have a different Task Handle.
The second word is a pointer to the Task Name.
The third word is the amount of memory (in bytes) used by the Task.
The fourth word holds various 'flags' that give specific information about the Task.
As the only way we can find out the name of the Task is via the pointer we may as well begin by writing a function to extract the name. Although we have used Functions and Procedures before I'm going to introduce a new feature, Local Variables.
Within any Function or Procedure you can use Local Variables. These variables are created when the routine is called, and destroyed when it exits at the '=' or ENDPROC terminator. They have no existance outside the routine. One advantage of this is that you don't have to try to think of a unique name for a variable. If you did create a Global variable to use in your Function which happened to have the same name as a variable used elsewhere in the program then it wouldn't actually be a different variable, which could have disastrous results as its 'twin' mysteriously changed value.
It is good practice to make all the variables within a Function or Procedure Local. If data is passed as parameters and the Function or Procedure does not alter any of the Global variables then once the routine has been thoroughly tested it can be regarded as a 'black box' and used in other programs. You just pass it any necessary parameters and it does its job or returns the answer; it won't interact with any other parts of your program.
So let's write a Function to return a String pointed to by a Pointer. We shall make it completely self-contained.
DEFFNget_string(ptr%) LOCAL a$ a$ = "" WHILE ?ptr%>13 a$ = a$+CHR$(?ptr%) ptr% = ptr% + 1 ENDWHILE = a$
The pointer to the string will be an integer and it is passed as a parameter in the brackets. This will be assigned to the variable 'ptr%'. It doesn't matter whether the parameter is a real number, a variable, or an expression, from the point of view of this function it will 'arrive' as the integer variable 'ptr%', and ptr% will be a Local Variable.
The first line of the Function creates a Local Variable 'a$'. Local Variables must be declared in this way at the very beginning of the routine, they can't just be created as you go along. If thy are not declared after the LOCAL keyword then they won't be Local Variables. You can create as many variables, of any type, as you need. As we shall see later they are declared one after the other, separated by commas, or you can use several lines, beginning each with the word LOCAL.
After we declare a$ the next line makes sure it's an empty or null string. This is actually completely unnecessary here, because as I have pointed out earlier strings are automatically set in this way when they are created. However, the next part is going to add characters to this string, so we must be absolutely sure that it will be empty first, and if the value of a variable is important before a section of code is enacted it's good to get into the habit of explicitly setting it to the required value even if it should already have that value.
By now you shouldn't have any difficulty understanding the WHILE loop which follows. It adds each character pointed to by ptr% to a$, increasing ptr% each time, until it encounters any character less than or equal to ASCII 13. This is a 'catch all' value because a string might be terminated with a CR (13) a LF (10) or a NULL (0), and this routine will work with any of these. It will also terminate if it encounters a Tab character, ASCII 9, but if the string is likely to have Tab characters we could add code to deal with that. For the present, we can assume that it will just have 'normal' characters.
The final, closing, line returns the string.
There's one further refinement we can add. BBC Basic V has a shorthand way of adding or subtracting to or from a variable. Instead of this
ptr% = ptr% + 1you can use -
ptr% += 1Using this the Function becomes -
DEFFNget_string(ptr%) LOCAL a$ a$ = "" WHILE ?ptr%>13 a$ = a$+CHR$(?ptr%) ptr% += 1 ENDWHILE = a$
Now we have a routine to extract the names we can write the main body of the program which, initially, is going to discover all the tasks that are running and print their names. I have allocated 1000 bytes for the buffer to hold the information. At 16 bytes per Task this allows for over 60 so it's more than enough for any eventuality.
REM Program to print names of all active Tasks ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM buffer% 1000 SYS"TaskManager_EnumerateTasks",0,buffer%,1000 TO ,end% p%=buffer% WHILE p%<end% PRINT FNget_string(p%!4) p%+=16 ENDWHILE END
The Function FNget_string would be added after this.
When you Run this program (Prog_6_1 on the CD) it will print out a list of the names of all the currently active tasks.
It first calls the SWI which fills the buffer with the information about all the active tasks, assigning the value returned to the variable 'end%' to tell us where the end of this data is. The loop then works its way through the data, terminating when the pointer 'p%' has reached the end. At each stage it calls FNget_string and PRINTs the name returned. The parameter passed to the Function is the pointer in the second word of each block, or at 'p%+4', hence what is actually passed is the contents of this word or 'p%!4'.
Make sure you understand exactly how this use of pointers operates. It can seem a bit confusing, but you will need to grasp the concept as, when you begin to program the Wimp, you will find that a lot of the information is passed back and forth using pointers in registers.
If we want to do anything with this information we are going to have to do more than just display it, we are going to have to store it, because although the pointers are correct at the time the call is made they could change. We will have to make copies of the names and their respective Task Handles. The easiest way to do this is to store them in two arrays. The changes below copy the names and Task Handles to these arrays instead of displaying them. Don't forget that with this and all the following program you will also require FNget_name. I'm not going to repeat it each time for simplicity.
REM Program to get and store names and Handles of all active Tasks ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM buffer% 1000 DIM task_names$(50), task_handles%(50) SYS"TaskManager_EnumerateTasks",0,buffer%,1000 TO ,end% p%=buffer%:num_tasks%=0 WHILE p%<end% task_handles%(num_tasks%)=!p% task_names$(num_tasks%)=FNget_string(p%!4) p%+=16 num_tasks%+=1 ENDWHILE END
We now have two arrays, a string array task_names$ and an integer array task_handles% in which to store the names and handles. There is a new variable 'num_tasks%'. This is set to zero at the start of the loop and is used as the index when we store the Names and Task Numbers in the arrays. At the end of the loop it is incremented ready to address the next element in the arrays. When all the data has been dealt with and the loop exits num_tasks% will have 'counted' how many times the loop has been used and so how many tasks there are. Because it starts at '0' and is incremented after storing the data then Task No. 1 will have its data stored in the zero'th element of the array, Task No. 2 in the 1st element, and so on. I could have avoided this offset by incrementing num_tasks% before storing the data. That would have placed the data for Task No. 1 in elements No. 1, but it would have wasted the first (0) elements of both arrays. In this instance that wouldn't have been particularly important, but my intention is to reinforce my earlier remarks about the fact that all arrays (and hence pointers and indexes into arrays) actually start at zero.
So what happens when you Run this program? Outwardly, nothing. All we have done is to store the information, not display it. Let's add some more code to extract the names from the array and PRINT them.
REM Program to get and store names and Handles of all active Tasks ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM buffer% 1000 DIM task_names$(50), task_handles%(50) SYS"TaskManager_EnumerateTasks",0,buffer%,1000 TO ,end% p%=buffer%:num_tasks%=0 WHILE p%<end% task_handles%(num_tasks%)=!p% task_names$(num_tasks%)=FNget_string(p%!4) p%+=16 num_tasks%+=1 ENDWHILE FOR count%=0 TO num_tasks%-1 PRINT tasknames$(count%) NEXT END
When you Run this program you should see the same result you got with Program 1. The biggest difference is that this time we have stored all the data so can do something with it other than just display it.
The first thing I want to do is number the Tasks. This is very simple. We can use 'count%' to do this and we only need change one line.
PRINT STR$(count%+1)+" "+tasknames$(count%)
I have added 1 to the index (count%) because I want to number the Tasks from 1 but the data in the array is counted from zero.
What this should produce is the same list of Tasks as before, but with a number, starting from 1, beside each. This should give you a clue as to where we are going next. The idea is to enable you to kill any one of the listed Tasks by entering the number. However, before we can do this we have to add code to make the selection, which is nothing new, we've done that before, but there are two other essential things that are new.
To kill a Task we will have to use the Wimp Message system. This is a way that tasks communicate with each other. It is a very flexible system, and although some aspects are clearly defined others are open-ended to enble the system to cope with concepts that weren't envisaged when it was designed. The message we are going to send here is clearly defined - we are going to send a message to a particular Task and tell it that it must Quit. This is exactly what happens when you select 'Quit' from the menu for a Task from the Task Manager window.
However, before we can do this we have a problem. The Wimp Message system is exactly that, it's a system for passing messages between Tasks which are multi-tasking under the Wimp. Until now our programs have worked in a Command Window. This might appear on the desktop, but it is not multi-tasking. In fact, while this window is open the whole desktop is frozen - nothing happens, nothing will move but the mouse pointer. We are, effectively, 'out of the loop'.
Using a Task Widow
Luckily there is a very easy way to get even the simplest program into the multi-tasking Wimp system. We can run it in a TaskWindow. A TaskWindow is what you get when you press CTRL-F12. You can type '*' commands here, or Run programs. Because the TaskWindow is a part of the Wimp system, any program run from one operates under the TaskWindow's umbrella and becomes a sort of pseudo muti-tasking Wimp program.
You could Run the program by opening a TaskWindow and typing the program's name to Run it, but there's an easier way. You can make something that looks like a 'real' Wimp program that will run in a TaskWindow when you double-click on it.
As you are probably aware all normal programs are placed in a directory whose name begins with a '!'. This is called an application directory. If you double-click on an application directory the Wimp looks inside the directory for a file called !Run and runs that. Normally this would be an Obey file, which is essentially a text file with its filetype set to &FEB. These are more properly called command scripts and will consist of a series of '*' commands that are enacted by the OS. Naturally one of these (usually the final one) would be the command to run the 'real' program.
As a TaskWindow is (essentially) a Wimp program, you can Run it from the !Run file of an application directory.
First make a directory called (say) !Prog. Don't forget the '!'. Now you will need to create an Obey file called !Run. Open an !Edit window and type the single line.
From the menu which opens when you click MENU over the !Edit window follow 'Misc' to 'Set Type' and enter 'Obey' and press RETURN. Now Save the file with the name !Run into your !Prog directory.
Now when you double-click on !Prog it should open a TaskWindow. This will be exactly the same as you would get if you pressed CTRL-F12. Any text after the command TaskWindow is passed to the TaskWindow when it is invoked. This will then be interpreted and acted upon when the TaskWindow opens. Most parameters should begin with a '-' character ( a minus sign). The one used here tells the TaskWindow to open immediately and display any output. If we put some text in double-quotes then this will be interpreted as a a '*' command and enacted. Try altering the !Run file to.
Taskwindow "BASIC" -display
You should find that the TaskWindow will now open and run the Basic interpreter so you see its start-up message and the '>' prompt.
One last wrinkle and we're nearly done.
Put a copy of the last program we wrote (named Prog_6_3 on the CD) in the !Prog directory and change its name to 'Program'. Now we want to tell the TaskWindow to run this program. The problem is, where is it? We 'know' it's in the !Prog directory, but that could be anywhere, on any type of drive. Luckily there's an easy way.
Just as Basic can create and store variables, so the OS creates and stores its own variables. Many of these can be accessed by programs. Every time an Obey file is Run the OS sets one of its variables called Obey$Dir with the full pathname of the filing system, drive and directory where this Obey file was. As the file we are using to invoke the TaskWindow is an Obey file, as soon as it's Run the Obey$Dir variable is set to the directory it is in. We can therefore use the variable to tell the TaskWindow where to find the program we want to run. To make sure the TaskWindow will understand that Obey$Dir is an OS variable we enclose it in the '<' and '>' characters.
So, with the program copied to the !Prog directory and renamed as 'Program, change the !Run file to
Taskwindow "<Obey$Dir>.Program" -display
This should work just as before , but this time it will be running in a TaskWindow instead of a Command Window. This is a much better way to run simple programs. As the TaskWindow is multi-tasking you can do other things while the program is running. If there is an error you can edit the program while the error message is still being displayed by the TaskWindow.
Now you know how it's done you can run and test all future programs in this way.
Time to die
However, to get back to our specific program, a program run in a TaskWindow, although it's not a 'proper' Wimp program, is effectively multi-tasking so we can pass messages to other Wimp programs.
Let's add some more code to select the Task we want to kill.
REM Program to get and store names and Handles of all active Tasks ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM buffer% 1000 DIM task_names$(50), task_handles%(50) SYS"TaskManager_EnumerateTasks",0,buffer%,1000 TO ,end% p%=buffer%:num_tasks%=0 WHILE p%<end% task_handles%(num_tasks%)=!p% task_names$(num_tasks%)=FNget_string(p%!4) p%+=16 num_tasks%+=1 ENDWHILE FOR count%=0 TO num_tasks% PRINT STR$(count%+1)+" "+tasknames$(count%) NEXT REPEAT INPUT '"Select Task to kill or '0' for none - " select$ selection%=VAL(select$) UNTIL selection% <= num_tasks% END
This lets us make a choice, and if we choose '0', the program should just exit without doing any more. So we can add a line to do just that
IF task_to_kill% =0 THEN END
If it gets past this then the number entered must correspond to one of the Tasks.
We now need to send the message telling the Task selected to quit. The 'address' for this message will be the Task's Task Handle, which we can read from the array. The message data is passed in a message block, so we will need a block of RAM allocated for this. In this case we could re-use the RAM at buffer%, but it would normally be better to have some space dedicated to this purpose, so that is what I shall do. At present all Wimp messages can't be longer than 256 bytes, so there is no need to allocate more than this.
At the start of the program add the line.
DIM msg_block% 255We can now set up the message block and send the message.
task_to_kill%=task_handles%(selection%-1) !msg_blk% = 256 msg_blk%!12 = 0 msg_blk%!16 = 0 SYS "Wimp_SendMessage",18,msg_blk%,task_to_kill%
The first line takes the number entered and extracts the corresponding Task Handle from the array. We have to subtract 1 from this number because you will recall that the numbers are all one higher than te array indexes.
The next three lines place data into the message block. The first word is the size of the block. The next two words aren't used in this context, so can be ignored.
The third word word (at +12) is the message reference number. This is used when messages are passed back and forth between programs so that they don't get confused. The number is supplied by the Wimp, and a program which originates a message should use zero, as is done here.
The fourth word is the message action. In this case it's zero, and the 'action' which corresponds to this is that the Task receiving the message must quit.
The message block therefore describes what we want to happen. It's an original message, and the Task that receives it is required to quit.
The final line sends the message.
R0 is an 'event code'. It essentially menas 'this is a message'. You will learn more about these codes later. R1 holds a pointer to our message block. R2 holds the Task Handle of the Task the message is addressed to.
The complete program is therefore
REM Program to store Names and Task Handles of all active Tasks ON ERROR PRINT REPORT$ + " at line " + STR$(ERL) : END DIM buffer% 1000 DIM task_names$(50), task_handles%(50) DIM msg_block% 255 SYS"TaskManager_EnumerateTasks",0,buffer%,1000 TO ,end% p%=buffer%:num_tasks%=0 WHILE p%<end% task_handles%(num_tasks%)=!p% task_names$(num_tasks%)=FNget_string(p%!4) p%+=16 num_tasks%+=1 ENDWHILE FOR count%=0 TO num_tasks% PRINT STR$(count%+1)+" "+task_names$(count%) NEXT REPEAT INPUT '"Select Task to kill or '0' for none - "select$ selection%=VAL(select$) UNTIL selection%<=num_tasks% IF selection%=0 THEN END task_to_kill%=task_handles%(selection%-1) !msg_block%=256 msg_block%!12=0 msg_block%!16=0 SYS"Wimp_SendMessage",18,msg_block%,task_to_kill% END DEFFNget_string(ptr%) LOCAL a$ a$="" WHILE ?ptr%>13 a$=a$+CHR$(?ptr%) ptr%+=1 ENDWHILE =a$
For this program to work it must be run in a TaskWindow as previously described. You should then see something like this.
If you try to run this program by double-clicking on the Basic file, that is, not running it in a TaskWindow then you will get an error message 'Invalid Wimp operation' because your program is not part of the Wimp's multi-tasking system
A word of warning. Before you run this program be aware that it is potentially extremely dangerous. It can't actually hurt your computer, but it could kill off Tasks that you must have running for it to work properly, so if you enter an invalid number or if you type in the program and make a mistake - beware!
An obvious addition would therefore be to tell the user the name of the Task that had been selected and to ask for confirmation before sending the message. See if you can add code to do this. I've put one possible method in the file ANSWER.
You might also like to re-write the program bypassing the 0'th element of the array so that the first index used was always '1'. This would actually make the program simpler that the method used, but as this is meant to be a tutorial I deliberately chose the slightly more awkward method.