Portability Issues |

The following non-exhaustive list of portability issues encountered during the development of this package, along with the corresponding solutions which have been adopted, will help to understand some of the design and implementation decisions which have been made. Hopefully this list will also be useful to those of you who want to develop portable Eiffel class libraries. Feel free to contact me if you want to discuss some of the items from this list or if you experienced other interoperability problems.

[Note that this list is out-of-date and some of the problems described below might not exist anymore.]

Problem: Eiffel is a case-insensitive language. However SmallEiffel is case-sensitive!

Solution: A new command-line option `-case_insensitive` has been introduced in SmallEiffel -0.83 to make the compiler case-insensitive.

Problem: In order to avoid classname clashes between different libraries, some Eiffel compilers support class renaming in the Ace file or equivalent. But some don't.

Solution: The name of the classes have been systematically prefixed with a two-letter library code. For example the classes from the Gobo Eiffel Structure Library have been prefixed by DS (which stands for Data Structures), as in `DS_LINKED_LIST`, whereas classes from the Gobo Eiffel Lexical Library have been prefixed by LX, as in `LX_SCANNER`.

Problem: There is no Data Structure library standard. Although each Eiffel compiler provides its own Data Structure library, none of them is portable.

Solution: Although portable Data Structure libraries, such as Pylon, have been made public, none of these libraries were available when this project has been started. Therefore a (yet another) Data Structure library (the Gobo Eiffel Structure Library) has been developed as a foundation for the other libraries of this package.

Problem: According to ELKS '95, a class has to inherit from `HASHABLE` to supply feature `hash_code` in its interface. However SmallEiffel does not support class `HASHABLE` but provides a built-in feature `hash_code` in class `GENERAL` instead.

This problem has been fixed in SmallEiffel -0.82. The solution which was adopted before the release of SmallEiffel -0.82 is provided below for the interest of the reader only.

Solution: After several attempts, it was impossible to make classes using hash tables portable using either the inheritance or client/supplier adaptation techniques. Even creating a dummy class `HASHABLE` in SmallEiffel didn't work mainly because of the fact that `hash_code` was built-in in `GENERAL`.The only alternative left was to use gepp preprocessor as follows:

>     #ifdef SE
>     class DS_HASH_TABLE [G, K]
>     #else
>     class DS_HASH_TABLE [G, K -> HASHABLE]
>     #endif

>     inherit

>         DS_TABLE [G, K]
>     ...
>     end

or as in:

>     class FOO

>     inherit

>         BAR
>     #ifdef SE
>             redefine
>                 hash_code
>             end
>     #endif

>     #ifndef SE
>         HASHABLE
>     #endif
>     ...
>     feature -- Access

>         hash_code: INTEGER
>                 -- Hash code value
>     ...
>     end

Problem: Some Eiffel compilers do not handle properly inheritance from classes `ARRAY` or `STRING`. This is mainly because of some built-in features hard-coded for optimization purposes but which cannot even be renamed in descendant classes without breaking the run-time system (try to rename feature `count` from class `ARRAY``` in a descendant class with SmallEiffel to see for yourself). As a consequence, classes inheriting from `ARRAY` for implementation purposes (such as `DS_ARRAYED_LIST` for example) will not work as expected with the faulty compilers.

Solution: Instead of using implementation by inheritance, classes such as `DS_ARRAYED_LIST` have a hidden attribute of type `ARRAY` and implement their functionalities by delegation.

Problem: Class `FILE` is specified in ELKS '95 with routines to read from (`read_*`) and to write to (`put_*`) files. However each Eiffel compiler differs from the other on that matter. ISE Eiffel and Halstenbach provide an abstract class `IO_MEDIUM` as ancestor for files, sockets, etc. As opposed to ELKS, class `FILE` is deferred, one possible effective descendant being `PLAIN_TEXT_FILE`. Tower Eiffel and Visual Eiffel support class `FILE` from ELKS, but Tower Eiffel also provides class `TEXT_STREAM` as an ancestor of `FILE` (similar to `IO_MEDIUM` above). Finally, SmallEiffel does not support `FILE` at all, but instead has the notion of `INPUT_STREAM` and `OUTPUT_STREAM`, with two effective descendants `STD_FILE_READ` and `STD_FILE_WRITE`. This is a real portability nightmare. The obvious solution which is to write a class `KL_FILE`, implemented as a descendant of the various classes above provided by each compiler, is not satisfactory. For example, let's have a routine which takes a file as argument. Since the standard input and output files provided in class `STD_FILES` from ELKS are not of type `KL_FILE`, they cannot be passed as argument to this routine, making the routine rather useless. The ideal solution would be to take advantage of each compiler implementation choices, transparently allowing the use of `IO_MEDIUM` or `TEXT_STREAM` without breaking too much portability constraints.

Solution: From the description above, the adaptation by inheritance technique has naturally been discarded, adaptation using client/supplier relationship being a better choice when dealing with standard files as regular text files. To accommodate with SmallEiffel viewpoint, the file functionalities had to be split into two separate categories: input and output. Finally, using anchor types would ease the use of `IO_MEDIUM` and `TEXT_STREAM` while keeping portability in mind. Following are two classes taking care of input functionalities. The same kind of classes provides the output counterpart. The first class provides the anchor types to be used when requiring objects with file access facilities, and a once function giving access to adapted input features. This class should be used through inheritance to take advantage of the anchor technique.

>     class KL_IMPORTED_INPUT_STREAM_ROUTINES

>     feature -- Access

>         INPUT_STREAM_: KL_INPUT_STREAM_ROUTINES
>                 -- Routines that ought to be in class INPUT_STREAM
>             once
>                 create Result
>             ensure
>                 input_stream_routines_not_void: Result /= Void
>             end

>     feature -- Anchor types

>     #ifdef ISE
>         INPUT_STREAM_TYPE: IO_MEDIUM do end
>     #else
>     #ifdef SE
>         INPUT_STREAM_TYPE: INPUT_STREAM do end
>     #else
>     #ifdef TOWER
>         INPUT_STREAM_TYPE: TEXT_STREAM do end
>     #else
>         INPUT_STREAM_TYPE: FILE do end
>     #endif
>     #endif
>     #endif
>                 -- Anchor type

>     end

Note that the name convention used for the once function is derived from the name of the class and suffixed by an underscore character (`_`) to try to avoid name clashes with user-defined feature names. The second class uses the client/supplier adaptation technique to provide portable input features. This class should only be used through the once function of the class above.

>     class KL_INPUT_STREAM_ROUTINES

>     inherit

>         KL_IMPORTED_INPUT_STREAM_ROUTINES

>     feature -- Initialization

>         make_file_open_read (a_filename: STRING): like INPUT_STREAM_TYPE
>                 -- Create a new file object with a_filename as
>                 -- file name and try to open it in read-only mode.
>                 -- is_open_read (Result) is set to True
>                 -- if operation was successful.
>             require
>                 a_filename_not_void: a_filename /= Void
>                 a_filename_not_empty: not a_filename.empty
>             local
>                 rescued: BOOLEAN
>     #ifdef ISE
>                 a_file: PLAIN_TEXT_FILE
>     #else
>     #ifdef SE
>                 a_file: STD_FILE_READ
>     #else
>     #ifdef TOWER
>                 a_file: FILE
>     #endif
>     #endif
>     #endif
>             do
>                 if not rescued then
>     #ifdef ISE
>                     create a_file.make (a_filename)
>                     Result := a_file
>                     a_file.open_read
>                 elseif not a_file.is_closed then
>                     a_file.close
>     #else
>     #ifdef SE
>                     create a_file.make
>                     Result := a_file
>                     a_file.connect_to (a_filename)
>                 elseif a_file.is_connected then
>                     a_file.disconnect
>     #else
>                     create Result.make (a_filename)
>                     Result.open_read
>                 elseif not Result.is_closed then
>                     Result.close
>     #endif
>     #endif
>                 end
>             ensure
>                 file_not_void: Result /= Void
>             rescue
>                 if not rescued then
>                     rescued := True
>                     retry
>                 end
>             end

>     feature -- Status report

>         is_open_read (a_stream: like INPUT_STREAM_TYPE): BOOLEAN
>                 -- Is a_stream open in read mode?
>             require
>                 a_stream_void: a_stream /= Void
>             do
>     #ifdef SE
>                 Result := a_stream.is_connected
>     #else
>                 Result := a_stream.is_open_read
>     #endif
>             end
>     ...
>     end

Following is an example using portable file access:

>     class FOO

>     inherit

>         KL_IMPORTED_INPUT_STREAM_ROUTINES

>     feature

>         parse_from_file (a_file: like INPUT_STREAM_TYPE)
>                 -- Parse data from a_file.
>             require
>                 a_file_not_void: a_file /= Void
>                 a_file_open_read: INPUT_STREAM_.is_open_read (a_file)
>             do
>                 ...
>             end

>         execute
>                 -- Ask for a file name and parse it.
>             local
>                 a_file: like INPUT_STREAM_TYPE
>             do
>                 a_file := INPUT_STREAM_.make_file_open_read ("foo.txt")
>                 if INPUT_STREAM_.is_open_read (a_file) then
>                     parse_from_file (a_file)
>                     INPUT_STREAM_.close (a_file)
>                 else
>                         -- Parse from standard input.
>                     parse_from_file (io.input)
>                 end
>             end

>     end

Problem: According to ELKS '95, standard files are accessible as follows:

>     io.input
>     io.output
>     io.error

and are all of type `FILE`. However in SmallEiffel they appear as `std_input`, `std_output` and `std_error` in class `GENERAL`. Moreover, as sketched in the portability issue above with respect to `FILE`, they are declared of a different type across different compilers.

Solution: The solution adopted consists of two steps. First, to solve the typing problem, the anchor types technique described above in the portability issue about files. Then, two classes are introduced. The first class is more or less like an adaptation of the `STD_FILES` class from ELKS:

>     class KL_STANDARD_FILES

>     inherit

>         KL_IMPORTED_INPUT_STREAM_ROUTINES
>         KL_IMPORTED_OUTPUT_STREAM_ROUTINES

>     feature -- Access

>     #ifdef SE
>         input: STD_INPUT
>     #else
>     #ifdef TOWER
>         input: TEXT_STREAM
>     #else
>         input: FILE
>     #endif
>     #endif
>                 -- Standard input file
>             once
>     #ifdef SE
>                 Result := std_input
>     #else
>                 Result := io.input
>     #endif
>             ensure
>                 file_not_void: Result /= Void
>                 file_open_read: INPUT_STREAM_.is_open_read (Result)
>             end

>     ... Same thing for output and error ...

>     end

Note that `input` could not be declared as`like` `INPUT_STREAM_TYPE` since it is a once function. The second class is used to access these standard files through a once function in the same way as `io` from `GENERAL`:

>     class KL_SHARED_STANDARD_FILES

>     feature -- Access

>         std: KL_STANDARD_FILES
>                 -- Standard files
>             once
>                 create Result
>             ensure
>                 std_not_void: Result /= Void
>             end

>     end

Now, wherever one would have used `io.input`, `io.output` or `io.error`, one can use `std.input`, `std.output` or `std.error` in a portable way as in the following example:

>     class FOO

>     inherit

>         KL_SHARED_STANDARD_FILES

>     feature

>         parse_from_file (a_file: like INPUT_STREAM_TYPE)
>                 -- Parse data from a_file.
>             require
>                 a_file_not_void: a_file /= Void
>                 a_file_open_read: INPUT_STREAM_.is_open_read (a_file)
>             do
>                 ...
>             end

>         execute
>                 -- Ask for a file name and parse it.
>             local
>                 a_file: like INPUT_STREAM_TYPE
>                 a_name: STRING
>             do
>                 std.output.put_string ("Enter a filename: ")
>                 std.input.read_line
>                 a_name := std.input.last_string
>                 a_file := INPUT_STREAM_.make_file_open_read (a_name)
>                 if INPUT_STREAM_.is_open_read (a_file) then
>                     parse_from_file (a_file)
>                     INPUT_STREAM_.close (a_file)
>                 else
>                     std.error.put_string ("Cannot open file ")
>                     std.error.put_string (a_name)
>                         -- According to ELKS, the following line should
>                         -- be written "std.error.put_new_line", however
>                         -- this routine was named `new_line' in ISE Eiffel
>                         -- 4.2 and Halstenbach 2.0. At least, the following
>                         -- line is portable.
>                     std.error.put_character ('%N')
>                         -- Parse from standard input instead.
>                     parse_from_file (std.input)
>                 end
>             end

>     end

Problem: Some useful features are missing in ELKS '95. Most often these features are already provided by some compilers, but not by all compilers and probably under different names or signatures. For example, features such as `is_integer` in class `STRING` would be useful (specially as a precondition for `to_integer`).

Solution: The solution adopted is the same as when a feature specified in ELKS is not supported by some compilers, which is to use client/supplier adaptation. For the example above, class `KL_STRING_ROUTINES` will provide the following portable feature:

>         is_integer (a_string: STRING): BOOLEAN
>                 -- Is a_string only made up of digits?
>             require
>                 a_string_not_void: a_string /= Void
>     #ifdef VE || TOWER
>             local
>                 i: INTEGER
>                 c: CHARACTER
>     #endif
>             do
>     #ifdef VE || TOWER
>                 from
>                     i := a_string.count
>                     Result := True
>                 until
>                     not Result or i = 0
>                 loop
>                     c := a_string.item (i)
>                     Result := c >= '0' and c <= '9'
>                     i := i - 1
>                 end
>     #else
>                 Result := a_string.is_integer
>     #endif
>             end

Note that a simple implementation had to be provided when missing.

Problem: Sometimes, creation procedures are not portable across compilers. This is for example the case with the creation procedure `make` from class `STRING`. ELKS says that`make``(``n``)` allocates space for at least `n` characters, but keeps `count` null. However, Visual Eiffel sets `count` to `n` in that case.

Solution: The solution adopted is similar to the client/supplier adaptation of regular features. The only difference is that the adapted routine will be a factory function with the same arguments as the original creation procedure instead of a routine whose extra first argument is the target of the call. The class `KL_STRING_ROUTINES` will hence have the following function:

>         make (n: INTEGER): STRING
>                 -- Create an empty string. Try to allocate space
>                 -- for at least n characters.
>             require
>                 non_negative_n: n >= 0
>             do
>     #ifdef VE
>                 create Result.make (0)
>     #else
>                 create Result.make (n)
>     #endif
>             ensure
>                 string_not_void: Result /= Void
>                 empty_string: Result.count = 0
>             end

and the usual string creation using `create`:

>     str: STRING
>     create str.make (10)

is replaced in portable code by:

>     str: STRING
>     str := STRING_.make (10)