Fenestra User's Guide (Chapter 6) -- contents | previous | next


6 The `System' cluster

system cluster class diagram

The `system' cluster is a loose collection of non-GUI classes which may be useful for the construction of Windows applications. It is mainly provided for access to operating system facilities not supported by the Eiffel kernel library.

Classes in this cluster are generally prefixed with F_ as they cover common concepts which could be used by other libraries, so name clashes are much more likely than for the main GUI classes.

6.1 Low-level memory block

In some cases like reading or writing a binary file, it is necessary to access memory blocks at the byte level. The class F_MEMORY is used to represent such a memory block. For example, this is used by the library itself for access to C structures on the Eiffel side of the runtime system. Because it should also be possible to access memory blocks from other platforms, the class can take byte order into account.

Block creation and size

The creation procedure is make which takes the size in bytes of the blocks. Block size is fixed at creation time and cannot be changed during the lifetime of the object. The block is allocated at creation and garbage collected as usual. The size of the blocked is kept in the attribute size.

Parts of a block can be referenced by specifying an index to the beginning of the relevant part. An index is, in line with the Eiffel convention, beginning at 1 and lower than or equal to size. Other languages or specifications may give zero-based offsets, in which case the function offset can be used to convert this into a normal index. The validity of an index can be checked with the boolean function is_index, which is often used in preconditions.

Bytes

The most basic type accessible is the byte. It is not represented by the Eiffel class CHARACTER, which, as its name indicates, is there to represent characters, whose width is implementation dependent and whose functionality is more specialised than a byte. The byte values are therefore stored in INTEGER instances, with appropriate assertions to check that they are between 0 and 255.

The function byte retrieves the value of the byte at the position specified by its index parameter. Conversely, the procedure set_byte changes the byte value at the specified index position.

Larger integer types

The class supports more than bytes, and larger integer types can be accessed. For each larger type, a procedure prefixed with set_ to change the value mirrors the access function to read the value.

All larger integer types are stored in several bytes, so endianess is an issue. Both reading functions and writing procedures are available in three versions: the normal one using the system's default byte order, another prefixed with little_ for little endian values independently of the current processor, and big_ for big endian values.

So, there are six methods per integer type. The values are all stored in integers like byte with appropriate assertions to check the bounds. The actual types are: word for 16-bit signed integers, unsigned_word for 16-bit unsigned integers and double_word for 32-bit signed integers.

Bitwise boolean operation

Low-level applications often use integer values to store a set of boolean values, or a combination of smaller integer values using bit masks. To read these, bitwise boolean operators are needed but are not normally available in Eiffel. That's why this class includes the integer functions bit_and and bit_or and bit_xorwhich do the boolean operation on each bit of the two integer parameters.

String type

It is also possible to read a sequence of characters into an Eiffel string from a part of the block. The source string can be terminated with a null character like in C. There is a maximum length parameter, so if the string is not zero-terminated all the characters up to the maximum length will be in the Eiffel string. As usual there are set_ procedures to mirror the reading functions.

The function cstring returns the string at the specified position. With this function, the width of characters is system-dependent like the endianess for integer functions. In order to read strings made of 8-bit (byte) characters, the function byte_cstring is used There should also be a function unicode_cstring on Eiffel platforms where Unicode is supported.

Copy and subset

The function subset returns a smaller block containing a copy of the values between the specified indices. A subset block can be copied into the current block using set_subset.

The class also redefines copy and is_equal from ANY. The former requires the source to be the same size as the target, and the latter returns equality only for blocks having the same content and size.

6.2 Files and file management

Despite the fact that the Eiffel standard kernel library has a class to access files, it may be useful to have a facility closer to the operating system, to avoid limitations of the kernel class and allow extension to other kinds of stream IO.

In addition to the file access classes, this section includes a description of file management classes handling directories, file removal and related functionality.

6.2.1 IO Streams

Before describing concrete file classes, let's introduce the abstract stream classes associated with a continuous byte stream. The API has a general scheme for byte streams which can describe not only files but also network sockets, pipes or serial ports. For this reason, method names are more generic than the traditional operations associated strictly with files.

The classes presented here are for synchronous files, that is the operations on streams, like reading and writing, are blocking until the operations are finished, unlike asynchronous streams where the client program continues before the operation is terminated (with a system to inform the client when an operation has been completed).

Basic stream

The basic class is F_STREAM representing a simple byte stream. The stream is a stream of bytes not characters as explained above for memory blocks. The byte values are accessed through Eiffel integers.

Status

The class is deferred so the creation procedure is left to the concrete descendants. A set of boolean values documents the current status of the stream: is_alive is true when the stream is active and information can be exchanged through it, is_readable and is_writable document the current mode of the stream and do not change during the lifetime of a stream Not to be confused with the lifetime of a stream object., has_error is false if the previous read or write operation failed and has_data indicates that data is waiting to be read.

Connection

Before data can be read or written on a stream, the stream has to be connected to whatever entity it is associated with. The procedures connect and imperative_connect are used for this, the difference is that the former may fail (is_alive may be false) while the other requires a working stream in its postcondition and will fail with an exception if the system cannot open the stream. This assertion is produced by the main code and not by an assertion violation and can therefore be rescued safely using Eiffel's exception mechanism. A valid stream has to be closed after connection using disconnect.

Readers familiar with the common open/close metaphor for files may wonder why connect/disconnect is used. While the two concepts will be similar for files, the issue of having a pipe available and having it actually connected to something may be separate issues for network streams. Another issue is that the abstract connection procedure do not have any mode or configuration parameters: these will be set with other methods to be used before the connection and introduced by descendant classes.

Reading and writing

The actual read and write calls are done as usual with the procedure read_byte and attribute last_byte for reading and advancing in the stream and a write_byte for writing. Before reading, it should be checked that there is data available with the boolean has_data.

Finite stream

The basic stream class is a continuous stream without the concept of position or size. In the case of some streams like files, the data is rather static as opposed to a flow, and random access may be useful. This is the role of the abstract class F_FINITE_STREAM which extends F_STREAM to a finite stream: the stream now has a definite size accessible through the attribute size. The position is read with position and changed with the procedure set_position. A position is an integer between 1 and size included. When a read or write operation is executed, the current position is modified accordingly.

6.2.2 Binary files

This section describes the concrete implementation of binary files as streams or finite streams. These are to be distinguished from text files presented in the next section.

Binary files are implemented by the classes F_FILE and F_RANDOM_FILE, corresponding to abstract streams and finite streams. While all files can be accessed as random access, there are still two classes because many files are accessed sequentially and the operating system allows more efficient file access for sequential files when it knows the access will be sequential.

The class F_FILE effects the methods from F_STREAM and adds mainly several methods for setting the mode before opening a file. The creation procedure is make and simply sets up the object.

Opening mode

The first required information to set before opening the file with connect is the file name, set with set_name. The name must be a conforming file name and should not have invalid characters, as checked with the boolean function is_valid_name, one of the preconditions of set_name.

The file can be opened for reading or writing or both with set_mode_read (the default) and set_mode_write and set_mode_read_write. A file is normally open in exclusive mode (it can be opened only by one application at a time) unless the sharing mode is changed with set_share_read for instance.

Read and write memory blocks

In addition to effecting the methods to read and write bytes, this class adds a similar facility to read and write memory blocks, as represented by the class F_MEMORY introduced earlier, which can be useful to access some binary files. The methods are write_block and read_block and last_block. The procedure read_block takes the size of the block and if the operation is successful creates a new memory block object referenced by last_block.

Random files

The class F_RANDOM_FILE implements random access files. It effects the positioning methods from F_FINITE_STREAM by extending the class F_FILE. It should be used when random access is really needed because it is slightly less efficient than F_FILE.

6.2.3 Text files

Binary files are often actually viewed as text files, or the other way round, but there are several reasons to distinguish them. A stream of bytes is not a stream of characters, especially when characters are not the same types as bytes as mentioned earlier. Moreover, in order for a text file processing class to be scalable to Unicode and 16-bit characters or other character systems, it must not make an assumption on the character type. A text file class may also need special processing for lines and end of line markers.

Text files in this cluster are handled with the class F_TEXTFILE, an extension to F_FILE. The byte-level methods it inherits are not normally used unless low-level access is needed concurrently. There are corresponding character-oriented methods. A text file is a sequential file and cannot be random access because the number of characters in the stream does not map to a number of bytes and is not predictable -- it depends on the line processing, the character width, etc.

Reading and writing text

The class adds methods for processing text, that is characters and strings. The triplet read_character and last_character and write_characters for handling individual characters, and the unsurprising methods read_string and last_string and write_string at the line level.

Reading a string simply means reading characters up to the next enf of line marker. When writing strings, separator characters should not be present in Eiffel strings, as they are added by the class. When reading strings, the Eiffel string does not contain separator characters at the end. On the other hand, separator characters are considered to be normal character by the character-level features. Mixing both types of access is possible but may have to be done carefully when reading through line boundaries.

New-line mode

An important issue is to know what is defined as a new line. In a typical text file under Windows lines are separated by CR/LF characters. Under Unix they are separated by simple LF characters. Other systems may use only CR or another separator character.

The default mode is to write CR/LF and to try to take the main variants into account automatically when reading. It means both single LF characters and and CR/LF pairs will be considered to be line separators.

The other modes are either CR/LF-only and it is selected with the procedure set_textmode_dos, or LF-only (set_textmode_unix) in which case CR characters either alone or before a LF are considered to be normal characters, or any other character used as separator like but instead of LF, with set_textmode_character which takes the character as its parameter. The procedure set_textmode_standard or reset_textmode gets back to the default.

Towards Unicode

While the class is not supporting Unicode at the time of writing, it is ready to be transparently upgraded to support it given wide character support in the Eiffel basic CHARACTER type and the corresponding STRING. Unicode and ASCII or 8-bit text files can be distinguished automatically by the library because most Unicode text files begin with a typical marking sequence: the Unicode character FEFF (in hexadecimal) is used to specify the byte-order. So the class should be able to read both types of text files transparently. An extra mode may still be required for writing, or to force a reading mode.

6.2.4 File management

After a description of individual file access, the view can be broadened to file management. There are two classes relating to this: one for managing files and directories and the other to read and possibly modify the properties of individual files. The latter is described in the next section.

The class F_FILE_SYSTEM contains various file management functions. It is a bit like a mixin class but it has some attributes, mainly holding the result of some queries, so implementation inheritance should not be used. An instance of this class is initialised with the creation procedure make.

Many file system operations depend on external conditions and therefore may fail. Operations subject to failure set the boolean attribute last_success to true when successful, or false otherwise. The operating system error code can be retrieved -- preferably only when strictly necessary as it is introducing low-level dependencies -- through the attribute last_error.

Directories

The first set of methods is for directory handling. The boolean function is_directory allows to check if a given name correspond to an existing directory. Directories can be removed or created with the procedures create_directory and remove_directory. It may be useful to check last_success because these operations may fail for various reasons such as the proposed directory name being an existing file during a creation attempt, or a directory being accessed by another process or through the network during removal attempts.

On an existing directory, it is possible to obtain a list of objects -- files and other directories -- it contains with the procedure directory_content which, when successful, initialises a list of element names referred to by the last_content attribute.

The operating system maintains a set of directories such as the current directory, used to find files without an absolute path in the file name. It is obtained from the procedure current_directory and temporary_directory for the directory where temporary files can be stored. Both procedures set the last_directory string if successful. The current directory can be changed with change_current_directory.

File management

Features to manage individual files are similar to those for directory. The boolean function file_exists is true when the name corresponds to an existing file. A file can be removed with remove_file, copied with copy_file and moved with move_file. Like previous actions, these may fail. In particular, under the Win32 API, a file cannot be removed or, likewise, moved when it is opened (by any process). This is unlike other operating systems such as Unix. One may wonder about the absence of a rename procedure, but move_file actually does the same task, although the physical operation is a copy when destination and source are on different volumes.

Disk space

The last operation is to retrieve free disk space on a given volume or disk partition identified by its `drive' name using the cunningly named procedure free_disk_space. The result is the last_size integer if the operation is successful. The size is in kilobytes (1024 bytes) because a disk volume may easily be bigger (in bytes) than a maximum integer.

6.2.5 File properties

An individual file has a set of properties like its size or some attributes maintained by the system that can be accessed through the class F_FILE_PROPERTIES. After initialising an object with the creation procedure make, the procedure set_file is called to set the file whose properties are to be inspected. The boolean is_valid is then true if the file name points to a valid file, or directory. Individual properties can then be read.

Most properties are boolean values: is_directory is true for a directory (otherwise the `file' is really a normal file). There are properties corresponding to traditional file attributes: is_read_only, is_hidden, is_archive, is_system.

The size of the file is obtained with file_size (in bytes) or file_size_kb (in kilobytes, useful if the former overflows).

The remaining properties are the various time stamps associated with a file. Not all file systems may maintain all of them correctly. There is the creation time, creation_time, the last modification time, last_write_time, and the time the file was last accessed, last_access_time; all are instances of the date and time class from the foundation library (see section (see section B.3)).

6.3 Synchronisation

The operating system provides a series of synchronisation primitives, that can be used to allow two processes working concurrently to cooperate. There are several synchronisation primitives: a mutex allows exclusive access to a resource, semaphores can be used to restrict access to a finite number of clients and events are useful for applications to communicate for instance the completion of a task.

The base object

All synchronisation objects have several characteristics in common, and so they all inherit from a base class, F_SYNCH_OBJECT.

First and foremost, all these objects have a status: they are either signalled or not. A client can wait on a synchronisation object until it gets signalled, and the controlling party can signal or reset an object.

Synchronous wait for a synchronisation event

Practically a client calls the procedure synch_wait which will lock the current process or thread until the object is signalled. When this is the case, the event procedure when_signaled is called (which can as usual be redefined or handled with set_command).

The procedure finite_wait is an alternative to synch_wait that stops waiting after the delay corresponding to its parameter if the object did not become signalled in the meantime.

This can be convenient in a GUI application where the waiting is likely to occur in a timer event and should not last for too long so as not to hang the user interface processing.

Asynchronous wait

In some cases, it is necessary to wait for several events that can happen concurrently, in this case the event object has the procedure wait for an asynchronous wait. What it actually does is queue the event with the synchronisation notifier object which is maintained by the library. This object, of type SYNCH_NOTIFIER, can be accessed through the notifier attribute of each event object -- they all point to the same notifier instance of course.

This notifier object has itself a procedure wait which will process all outstanding events which had hitherto been queued with F_SYNCH_OBJECT's wait. For the processing of one event only (from a loop that may do other things in its body for instance), there is the function wait_once. Both methods can be timed out using set_latency -- the default is no time out.

It is important to remember that the event object's wait only registers a single wait for the next occurrence of the event. If, after the event was signalled and processed, it is needed to wait again for the next signal on the same object, it is then necessary to call wait again, possibly in the event processing routine itself (corresponding to the when_signaled callback).

Naming and closing event object

The event objects are created using creation procedures documented for each particular type of event later, but there are a few things in common. These objects, like files, correspond to operating system objects and therefore their creation may fail: is_valid is true when the creation has been successful, and is used in the precondition of most operation. Once the object has been used, it should be terminated using close -- finalisation ultimately handles forgotten objects but it is preferable to close them explicitly to avoid loose objects waiting for the next garbage collection.

Event objects are referenced by name so that various processes can share events in common. The event name space is a flat space common to all processes on a system. It must not contain a backward slash character and be above the maximum name length defined by the system. The correctness of a prospective event name can be checked thanks to the boolean function is_valid_name.

6.3.1 Mutex

A mutex (a shortcut for MUTually EXclusive) is an object that allows at most one process to access a resource. This means that only one process can own a given mutex. A client can wait on a mutex, and when no other process owns this mutex, it is signalled, and the client owns it, until it releases, that is frees it for use by some one else.

The mutex class is F_MUTEX. It is created using make for a new mutex or open an exisiting one. Alternatively, make_existing to open an exisiting mutex already opened by another party.

After a successful wait, the boolean is_owned becomes true. Once the client is finished with its event mutex, it can call release, possibly as a conclusion to the processing of the when_signaled event.

6.3.2 Semaphores

Semaphores are useful to restrict access to a resource to a limited number of clients, like a global counter. The semaphore has a maximum value set at creation time. Every client that waits on it gets a signalled object if the count is greater than zero, or when it becomes so. The value of the counter is decremented when a semaphore is used -- similarly to a mutex being owned -- and incremented when the semaphose is released.

The actual creation procedures for the class F_SEMAPHORE are make_existing to open an existing semaphore, or make to create a new one (or open an exisiting one). The latter takes the maximum count of the new semaphore in addition to the name. The maximum count is set when the semaphore is created -- at the system level, not the particular instance of the semaphore access object.

When the semaphore has been signalled and the event processed, release is called to free it.

6.3.3 Synchronisation event

The last type of synchronisation object is the event, not to be confused with the events of the GUI system of the callback procedures of the library. This system event is a synchronisation object that can be set by one process to inform another that a task has been completed for instance -- the system itself uses it to signal the end of an asynchronous IO operation among other things.

The class is F_EVENT. It has four creation procedures: make and make_existing are like their counterparts for other synchronisation objects. The status of the corresponding objects does not change unless explicitly requested by the client. Conversely, make_auto creates a semaphore that is automatically reset (not signalled) after a single client has captured the signal event.

Unlike the other creation procedures, make_anonymous creates an unnamed object that is used inside a single process or which is communicated to other process through handle inheritance for example instead of by name. It otherwise behaves like make.

Finally, the features to change the signalled status of the event are set and reset.

6.4 Time and timers

Two classes are available regarding time management: one for retrieving the current system time and the other for managing timer events allowing cooperative multitasking inside a single program thread.

6.4.1 System time

The class F_CLOCK enables access to time information maintained by the operating system. The current time is retrieved with the procedure get_local_time for time in the local time zone or get_utc_time for absolute UTC time (also known as Greenwich Mean Time, GMT). The procedure changes the attribute last_time which is an instance of P_DATE_TIME from the foundation library (see (see section B.3)).

Another system time information is the system `tick', the number of milliseconds since the system was started. Note that the counter wraps around zero as the maximum integer is reached so it cannot be relied upon to be an always increasing value. The value is stored in the integer attribute last_tick and retrieved with the procedure get_tick.

6.4.2 Timers

In some cases long operations may make the user interface unresponsive to user actions because while the operation is conducted user interface events are not processed. Under multitasking operating systems like the current versions of Windows, it does not block the whole system or user interface, but an application not responding swiftly may be confusing for the user and it can accumulate unprocessed events in its event queue which will not make sense when interpreted too long after the actual actions.

One solution is to divide the task in smaller pieces and use a timer to send regular timer events in the stream of normal user events The other solution is to use multiple threads though this may raise difficult issues with the current Eiffel technology, especially if portability is desirable. In any case, even when threads are available, many long tasks are still better handled using timers as it avoids the significant overhead of threading, and it is easy and natural to implement in an object-oriented environment.. The class F_TIMER provides this facility. It has a simple creation procedure, make. The timer itself does not start at creation time, it has to be set up before.

First, there is a single event which may be, as usual, processed by redefining the procedure when_executed or using a command object with set_command. The delay between events is set with set_delay in milliseconds. The GUI timer system is not in any way real time and the delay will be respected when possible, the actual delay may be longer when the application or system is busy processing other events.

After the event is attached and the delay set the actual timer can be started with start and afterwards stopped with stop. The boolean value is_active gives information on the current status.

6.5 System registry access

A program configuration and setup information is generally stored in the system registry maintained by the operating system, which is a tree containing keys, which themselves contain a set of named values. It also contains system setup information and the configuration information of third party programs -- it replaces the INI files of earlier versions of Windows. The registry can be viewed and edited with the Registry Editor program provided with Windows.

The class to access it is REGISTRY_KEY which as its name indicates allows access to one key (at a time) in the registry. There are several default roots for the registry tree corresponding to creation procedures: make_user is for keys in the current user subtree, make_machine for the system subtree shared by all users on the same computer.

Opening a key

Once an instance of the class has been created, a key may be opened using open for an existing key or create to make a new key or open an existing one. The key name is relative to the base tree corresponding to the creation procedure. Directories are separated by `\' characters.

Microsoft recommends placing all application setup information in a directory named ``Software\<company>\<program>\<key-name>'', generally in the current user subtree (or the machine tree if it's for a service or for setup information common to all users). The function standard_name makes a recommended key name from the company, program and key names. The restrictions on key and key directories names are similar to those on file names.

Both open and create may fail, for instance if the user does not have the permission for the operation, so it is sensible to check the boolean is_open after an attempt.

A key must be closed using the procedure close after usage.

Reading and writing values

Inside a key, there is a set of named values. Each value has a type such as string or integer. The access methods are similar to a standard IO routines from the kernel library: for each type there is a reading procedure, an attribute for the value which has just been read that is valid only when the last read was successful and has_value is true, and finally a procedure to write a value. The read and write methods take the name of the value as their first argument.

The types available are string (read_string and last_string and write_string), integer (read_integer and last_integer and write_integer). There is also a pseudo-boolean type (read_boolean, etc) which is actually stored as an integer as the registry has no native boolean type.

Listing values and keys

It is also possible to inspect a key to find out all named values it contains, or all subkeys it has -- the key space is a tree so, like a filesystem directory, a key can have subkeys.

The list of named values is obtained through the function values while the list of subkeys comes from subkeys. Both functions return a list of strings, which will be empty in case of error or if there are no corresponding values or subkeys.

Deleting a subkey

A key and all it's values can be deleted using delete_subkey, the name is relative to the currently opened key and must be a direct subkey, not a whole branch of tree.

6.6 Multimedia

Under Windows there is a generic interface for multimedia operations such as playing a sound or video file. It is possible to send multimedia commands by sending MCI (Multimedia Command Interface) command strings to the system, which will interpret them. The syntax and the sets of commands, whose range depends on the installed devices and drivers, is outside of the scope of this manual and is documented in the Windows API reference [devlib] and other references on Windows multimedia programming.

The class F_MULTIMEDIA implements this interface to multimedia commands. The class is a mixin class without attributes which can be used as an expanded local variable or through implementation inheritance.

The procedure send_command takes the command string and transmits it to the system. If the operation is successful, the boolean last_result will be true, otherwise it will be false and the string last_error will contain a short error message.

6.7 Miscellaneous

There are a few odd functions which do not fit anywhere else and have been grouped into the mixin class F_SYSTEM. This somewhat artificial grouping was preferred to creating a set of single-method classes.

Resource string

It is possible to store strings in the resource section of the executable file. Unlike normal resources identified by a string (section (see section 3.10)), these are identified by an integer value. The function string_from_resource is used to retrieve resource strings.

Pause

The program can be suspended for some time without taking processor time with the procedure sleep which takes the length of the pause in milliseconds.

Process Identifier

The integer process_id is the unique value associated with the current process by the operating system.

Program execution

Another program can be executed using execute which takes the command line, the first word of which is the name of the executable file.

Beep

An audible warning can be made with the procedure beep if the computer has a device for it.

Debug output

It is possible to send debug output strings, using the API's debugger interface, which can be viewed with a debugger or a monitor program.