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. TowerEiffel and Visual Eiffel support class FILE from ELKS, but TowerEiffel 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)
Copyright © 1997-2005, Eric
Bezault mailto:ericb@gobosoft.com http://www.gobosoft.com Last Updated: 21 February 2005 |