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.
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 Language Committee of NICE, after the specification given in Eiffel: The Language, second printing. However no Eiffel compiler will realistically implement the standard Eiffel language. Some compilers will implement only part of it, failing to support expanded types or exception handling. Some compilers will add extensions to the language to support concurrency or wide characters. 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 processing 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 or 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:
#define name
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.
#undefine name
where name is the name to be undefined.
#ifdef Condition ... #else ... #endif
where Condition is either:
- name
- 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
#include "filename"
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 Eiffel: The Language 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 -- class FOO
The names used for each supported Eiffel compiler are, in alphabetical order:
Halstenbach HACT ISE Eiffel ISE SmallEiffel SE Visual Eiffel VE
and gepp should then be invoked as follows:
Halstenbach gepp -DHACT foo.ge foo.e ISE Eiffel gepp -DISE foo.ge foo.e SmallEiffel gepp -DSE foo.ge foo.e Visual Eiffel gepp -DVE 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 is -- 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 VE Result.from_integer (an_int) #else #ifdef ISE || HACT Result := '%U' + an_int #else Result := an_int.to_character #endif #endif ensure valid_character_code: Result.code = an_int end end -- class KL_INTEGER_ROUTINES
The corresponding class KL_INTEGER_ROUTINES for Visual Eiffel, generated with the following command-line:
gepp -DVE 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 is -- 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.from_integer (an_int) ensure valid_character_code: Result.code = an_int end end -- class KL_INTEGER_ROUTINES
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-2001, Eric
Bezault mailto:ericb@gobosoft.com http://www.gobosoft.com Last Updated: 25 October 2001 |
![]() ![]() ![]() ![]() |