|Gobo Eiffel Preprocessor|
When writing portable Eiffel classes, two major kinds of problems may be experienced. The first interoperability problem has already been addressed in the adaptation by inheritance and the client/supplier adaptation techniques. It is the result of incompatibilities between kernel classes and features provided by different Eiffel compilers. NICE (Nonprofit International Consortium for Eiffel) has made the first step towards a better interoperability in this area by adopting ELKS '95 as the kernel library standard. But this standard is not perfect yet, even though Eiffel compilers using FreeELKS, a free implementation of ELKS, have the advantage of being interoperatable at the source code level.
The other problem encountered when writing portable code is incompatibility between the different language dialects provided by the Eiffel compilers. The definition of the Eiffel language is controlled by the ECMA TC49-TG4 committee for the standardization of the Eiffel Language, with the specification given in Eiffel: Analysis, Design and Programming Language. However no Eiffel compiler will realistically implement the standard Eiffel language. Some compilers will implement only part of it, failing to support non-conforming inheritance or multiple generic constraints. Some compilers will add extensions to the language to support concurrency. And some compilers will have bugs in their implementation. Workarounds usually exist for these bugs, but these workarounds are very often not supported by the other Eiffel compilers. A solution could be to use only the subset of the Eiffel language supported by all Eiffel compilers. This would mean that a given feature of the Eiffel language could not be used in a portable class just because one compiler cannot deal with it. Each Eiffel compiler has its strengths and weaknesses. It would be a pain to be stopped by the weaknesses of all Eiffel compilers without even being able to take advantage of their strengths. This would probably be acceptable for a "Hello World!" program, but certainly not for more realistic large-scale library classes. The solution adopted here is to get help from a preprocessor.
Gobo Eiffel Preprocessor is a simple filter program which has been developed in Eiffel using gelex and geyacc. It takes a text file as input, interprets the preprocessing instructions contained in that file, and returns the resulting file as output. The command-line usage message for gepp is as follows:
gepp [--lines][-Dname ...][filename | -][filename | -]
where the first filename is the name of the input file and the second filename is the name of the output file. If the input filename is missing, or if - is specified instead, the gepp reads its input from the standard input. Likewise, if the output filename is missing or if - is specified instead, then the standard output file is assumed. By convention, filenames for Eiffel classes containing preprocessing instructions have the suffix .ge instead of just .e. Zero or more name definitions can also be declared in the command-line using the notation -Dname where name is the name being defined.
When the command-line option --lines (or its short cut -l) is specified, empty lines are inserted in the output file in place of the lines of the input file which have been ignored by the preprocessor. Although the output file formatting may look ugly, it is a nice way to preserve the line numbering between the input and output files, which is quite useful when compilers report errors with their line number in the files generated by gepp.
A Unix-like usage message of gepp can be invoked using one of the following commands:
gepp --help gepp -h gepp -?
and one can get gepp's version number with either:
gepp --version gepp -V
Gepp behaves much like the well-known C-preprocessor, but only a small subset of the preprocessing instructions is actually supported. Keywords making up gepp instructions typically start with the character # at the beginning of the line. The preprocessor instructions supported by gepp are as follows:
where name is the name to be defined. This is slightly different from the macro definition provided by C-preprocessors. Here, the name is just defined but no value is given. The name definition is only used as conditions in the #ifdef and #ifndef instructions. In particular, there is no macro substitution in gepp. name should be made of one or more letters (a-z), digits (0-9), underscores (_), dots (.) or minus sign (-). Also note that name is case-sensitive. Defining name is not equivalent to defining NaMe.
where name is the name to be undefined.
#ifdef Condition ... #else ... #endif
where Condition is either:
- Condition is true is name is defined, false otherwise.
- ( Condition1 )
- Condition has the same value as Condition1.
- Condition1 || Condition2
- Condition is true if either Condition1 or Condition2 is true, false otherwise.
- Condition1 && Condition2
- Condition is true if both Condition1 and Condition2 are true, false otherwise.
- ! Condition1
- Condition is true if Condition1 is false, false otherwise.
If Condition is evaluated to true, then the lines between #else and #endif in the input file will be ignored by gepp. Otherwise gepp ignores lines between #ifdef and #else.
The #else part is optional. The following conditional instruction with its #else part missing:
#ifdef Condition ... #endif
is equivalent to the same conditional instruction with an empty #else part:
#ifdef Condition ... #else #endif
#ifndef Condition ... #else ... #endif
This is equivalent to:
#ifdef ! Condition ... #else ... #endif
Gepp will replace the above line by the contents of filename and will then preprocess it recursively. The maximum number of nested include files has been set to 10 to avoid infinite loops when dealing with cyclic definitions.
Gepp can be used in three different kinds of cases when writing portable Eiffel classes. The first case is when the adaptation by inheritance and the client/supplier adaptation techniques are not applicable. This can happen due to a language, rather than a kernel class, incompatibility between Eiffel compilers. For example, in a early release of Visual Eiffel, there was a bug in the select clause. In class FOO sketched below, the compiler was reporting a validity error unless two select clauses were specified, one in NUMERIC and one in HASHABLE. However, according to the Eiffel standard and all other compilers, the class is valid if and only if one and only one select clause is specified for is_equal, either in NUMERIC or in HASHABLE, but definitely not both. The only solution to solve this incompatibility was to use the preprocessor as follows:
class FOO inherit NUMERIC redefine is_equal select is_equal end COMPARABLE rename is_equal as comp_is_equal end HASHABLE undefine is_equal #ifdef VE select is_equal #endif end feature -- Comparison ... end
The names used for each supported Eiffel compiler are, in alphabetical order:
Gobo Eiffel GE ISE Eiffel ISE
(Note that ISE, GE, SE, VE should be all in uppercase because gepp is case-sensitive.)
gepp should then be invoked as follows:
Gobo Eiffel gepp -DGE foo.ge foo.e ISE Eiffel gepp -DISE foo.ge foo.e
Gepp can also be used even when the adaptation techniques using inheritance or client/supplier relationship would otherwise work. However the main use of gepp was actually to support these techniques by automatically generating the extra "adapted" kernel clusters associated with each Eiffel compiler from a set of "template" classes such as:
class KL_INTEGER_ROUTINES inherit PLATFORM feature -- Conversion to_character (an_int: INTEGER): CHARACTER -- Character whose ASCII code is `an_int' require a_int_large_enough: an_int >= Minimum_character_code a_int_small_enough: an_int <= Maximum_character_code do #ifdef ISE Result := '%U' + an_int #else Result := an_int.to_character #endif ensure valid_character_code: Result.code = an_int end end
The corresponding class KL_INTEGER_ROUTINES for ISE Eiffel, generated with the following command-line:
gepp -DISE kl_integer_routines.ge kl_integer_routines.e
will then be:
class KL_INTEGER_ROUTINES inherit PLATFORM feature -- Conversion to_character (an_int: INTEGER): CHARACTER -- Character whose ASCII code is `an_int' require a_int_large_enough: an_int >= Minimum_character_code a_int_small_enough: an_int <= Maximum_character_code do Result := '%U' + an_int ensure valid_character_code: Result.code = an_int end end
Finally, preprocessing can also be used even when no portability issue is involved. For example, let's consider that besides the portable way to implement a particular feature, one of the Eiffel compilers provides a much more optimized mechanism, although not portable. It would be a shame to have to discard this optimization just because the code being written had to be portable. On the other hand, gepp would make it possible to take advantage of this optimized feature using #ifdef instructions as in the examples above.
The only way to write portable code in C is by taking advantage of the C-preprocessor. Gepp could be used in pretty much the same way in Eiffel. However it is against the Eiffel philosophy to use such mechanism and none of the existing Eiffel compilers provide support for such preprocessing. Eiffel is by nature an elegant and very readable language which should not be soiled with ugly preprocessing instructions. As a consequence, this technique has only been used in very rare cases where no other solutions were satisfactory.
Copyright © 1998-2016, Eric
Last Updated: 23 December 2016