EROS - the Eiffel Remote Object Server framework

Finnian Reilly

Contents

Introduction to EROS

EROS is an experimental client-server application framework that is part of the Eiffel Loop collection of libraries. It was created for the purpose of writing client-server applications in which multiple clients can remotely call Eiffel routines using an original XML orientated protocol. Unlike XML-RPC there is no special XML grammar apart from a single XML processing instruction (<?call .. ?>) containing a routine call with a very Eiffel like syntax. The call instruction appears after the XML declaration and before the root tag of an optional application specific XML document. EROS has it's own network protocol based on a small set of commands (2) and message types (4).

Developing an EROS server application is done by implementing a number of descendants of the class EL_REMOTELY_ACCESSIBLE. Implementing this class defines a number of routines as being remotely callable and a number of types that can be passed as arguments buildable from XML. You can test your classes using a light weight single threaded server implemented with class EL_REMOTE_ROUTINE_CALL_REQUEST_HANDLER. You are then ready to deploy your classes in the multi-threaded server which provides the following features:

  1. A GUI for starting/shutting down the server.
  2. A set of controls for selecting between the log output of different threads in the console terminal.
  3. A display panel monitoring 11 different server traffic parameters.
  4. Allocation of a new client connection to one of an available pool of service threads.
EROS also has a framework to create client-side proxy counterparts to the remotely accessible classes on the server. This framework translates Eiffel calls into EROS XML call messages.

A signal processing demo application

An application is provided which demonstrates wave generation and fourier analysis based on Eiffeler Greg Lee's Numeric Eiffel Library library (NEL). A slightly modified form of the NEL fft module is distributed with the Eiffel Loop libraries to ease some inheritance problems. Two remote classes are implemented, SIGNAL_MATH which generates wave forms and FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE which transform the wave forms.

The NEL fft module is fine for applications that do not operate in real time. For applications like audio time-stretch players a better choice would be MIT's FFTW or the NAG math library.

Screenshot

A screenshot showing the standard EROS server control GUI and terminal console for the demo application.

http://www.eiffel-loop.com/images/EROS-server-screenshot.png

Network protocol

The EROS server exchanges plain text messages with it's clients across a network socket. Each message is either a command or a data string. A client connects to the server for a session and makes as many routine calls as it needs to before sending a quit command. The reply from each call is either an error setting command, a procedure finished acknowledgement, or in the case of a function call: either an XML document, or an Eiffel string representation of a numeric, boolean or string type. At present EROS does not support user authentication so is only suited for use within a secure network.

Command summary

  1. set_error (<code>)
  2. quit

Message type summary

  1. The string: 'procedure-ok'
  2.  An Eiffel string representation of the following types: numeric; boolean; string
  3.  An XML document as a function result
  4.  An XML document with a XML processing instruction (<?call .. ?>) containing a routine call inserted between the xml declaration (<?xml .. ?>) and the root element tag. The main document body is optional.

Remote call messages

Message examples

Here are some examples of EROS remote routine call requests. For clarity the XML declarations (<?xml ..?>) are omitted. The syntax is similar in a way to Eiffel agents in that a class name in braces is used to specify an open argument place holder. Classes SIGNAL_MATH and FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE both inherit from class EL_REMOTELY_ACCESSIBLE.

<?call {ADDRESS_BOOK}.set_email ('John Smith', 'john.smith78@gmail.com')?>

<?call {SIGNAL_MATH}.cosine_waveform (4, 7, 0.5)?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.is_power_of_two (128)?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.fft_make (128)?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.set_windower (Rectangular_windower)?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.is_valid_input_length (128)?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.is_output_length_valid?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.do_transform?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.output?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.set_input ({E2X_VECTOR_COMPLEX_DOUBLE})?>
<vector-complex-double count="128">
<col real="-1" imag="0"/>
<col real="-0.98078528040323043" imag="0"/>
<col real="-0.92387953251128674" imag="0"/>
<col real="-0.83146961230254524" imag="0"/>
..
</vector-complex-double>
The final example has a document body which is deserialized to an object of type E2X_VECTOR_COMPLEX_DOUBLE and then passed as an argument to set_input. Some of the calls (for example: is_power_of_two) are intended for use in preconditions on the client. In the set_windower call example, the constant Rectangular_windower refers to a once function in class FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE. The fft_make and do_transform examples are the only 2 examples of a procedure call. For procedures a 'procedure-ok' string is returned to the client to indicate it executed without error. All the other examples are function calls returning either a boolean string or an XML document representing class E2X_VECTOR_COMPLEX_DOUBLE. If an error occurred the returned message is a set_error command.

Logging

EROS makes use of the Eiffel Loop multi-threaded logging framework. This logger allows you to see output which is indented to reflect the state of the call stack. It shows the entry and exit from routines in a way that is designed to mimic the routines code definition. There is a global filtering mechanism that allows you to see output only from selected classes, and within a class you can further filter by routine name. You can of course turn off all output when you require the last bit of performance in your application. The logger can be optionally integrated with a Vision2 or WEL toolbar allowing control over which thread output is being output in the command console.

Server operation and user interface

Command line arguments

The server is launched with some command line arguments specifying the port to listen for connections on and the maximum number of launch-able threads to process requests. Connections in excess of that number are queued until a thread becomes available. 

Start/stop buttons

The server is started and stopped using a toggle action 'GO' button. Next to the 'GO' button are two stopped/started indicators based on street traffic light codes.

Server status monitor

The EROS server features a server status panel monitoring the following parameters in real time:
  1. Count of threads servicing requests.
  2. Count of queued connections awaiting a service thread.
  3. Count of procedures executed.
  4. Rate of procedure execution (per sec).
  5. Count of functions executed.
  6. Rate of function execution (per sec).
  7. Count of routine execution failures.
  8. Total data received in megabytes.
  9. Rate of received date as megabytes per sec.
  10. Total data sent in megabytes.
  11. Rate of sent date as megabytes per sec.

Console output controls

If application logging is turned on there will be a toolbar visible on the top left. The toolbar contains a drop down box listing all active threads. Use this drop down box to select which thread you wish to see logging output for in the console terminal. After you have selected a few different threads you can use the row of buttons to the left to navigate back to a previously selected thread as follows:
  1. Navigate to first selected thread selected by drop down box.
  2. Navigate to previously selected thread. (Shortcut: Alt + Left arrow)
  3. Navigate to previously selected thread to the right. (Shortcut: Alt + Right arrow)
  4. Navigate to most recent thread selected by drop down box.

Switching to a different thread displays only the last 50 or so lines of logged output. If you wish to see all the logged output for the currently selected thread since the application launched, use the refresh button to refresh the console terminal. (note: this will temporarily block the thread while it is refreshing)

Developing an EROS server application

An EROS server application is developed by creating one or more classes which conform to the type: EL_REMOTELY_ACCESSIBLE. These classes are then linked into the EROS server framework by implementing a routine in the root class:

Callable_classes: ARRAY [like remote_object_maker] is
--
do
Result := <<
create {EL_REMOTE_OBJECT_MAKER [FOO]},
create {EL_REMOTE_OBJECT_MAKER [BAR]},
..
>>
end

Unless an error occurs the remote routine result is either an XML document or a string representation of an Eiffel numeric or string. An XML document result is created by calling the function to_xml on a result object that conforms to the type EL_SERIALIZEABLE_AS_XML. You can either use your own serialization scheme by implementing to_xml or avail of the textual serialization framework provided by one of the modules from Eiffel Loop. The mainstay of this framework is a template substitution language called Evolicity. Evolicity is an Eiffelized version of the Apache Velocity project, a template substitution language for Java.

Test - deploy cycle

Initially you develop and test remotely accessible classes using a single-threaded command console test application. Deploying the classes is done by implementing the full multi-threaded server with a graphical user interface from class EL_REMOTE_ROUTINE_CALL_SERVER_APPLICATION

Fourier math example

The fourier math example provides a remote interface to classes SIGNAL_MATH and FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE. The root class for the application looks like this:

class
FOURIER_MATH_SERVER_APP

inherit
EL_REMOTE_ROUTINE_CALL_SERVER_APPLICATION

create
make

feature {NONE} -- Implementation

Callable_classes: ARRAY [like remote_object_maker] is
--
do
Result := <<
create {EL_REMOTE_OBJECT_MAKER [SIGNAL_MATH]},
create {EL_REMOTE_OBJECT_MAKER [FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE]}
>>
end

feature {NONE} -- Constants

name: STRING is "Fourier Transform Math"

Log_filter: ARRAY [TUPLE] is
--
once
Result := <<
["FOURIER_MATH_SERVER_APP", "*"],
["SIGNAL_MATH", "*"],
["FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE", "*"],

["EL_REMOTE_ROUTINE_CALL_SERVER_MAIN_WINDOW", "*"],

["EL_REMOTE_CALL_REQUEST_DELEGATING_CONSUMER_THREAD", "*"],
["EL_REMOTE_CALL_CONNECTION_MANAGER_THREAD", "*"],
["EL_REMOTE_ROUTINE_CALL_REQUEST_HANDLING_THREAD", "*"],
["EL_REMOTE_ROUTINE_CALL_REQUEST_HANDLER", "*"],
["EL_REMOTE_CALL_CLIENT_CONNECTION_QUEUE", "*"],
["EL_SERVER_ACTIVITY_DISPLAY_REFRESHER", "prompt_update", "update"]
>>
end
end

The class EL_REMOTE_OBJECT_MAKER is a factory class taking a type parameter conforming to EL_REMOTELY_ACCESSIBLE. The log filter array contains a list of classes to show logging for. The class: EL_SERVER_ACTIVITY_DISPLAY_REFRESHER has logging restricted to two routines. The other classes have no restrictions on which routines are logged.
When the server is active a client can connect to the server and make calls to either SIGNAL_MATH or  FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE for the duration of a session.

Implementing remotely accessible objects

The steps for making the routines of a class remotely accessible in an EROS server are as follows:

1. Create a class

Create a new class that inherits from class EL_REMOTELY_ACCESSIBLE.

2. Implement session initialization and cleanup

Implement procedures on_session_begin and on_session_end to respectively reinitialize the object to start a new client session and do clean up at the end of a session. During a session multiple calls may be made to an object which may change the state of an object. In the next session a different client could be connected. The procedure on_session_begin allows you to reset the object to a beginning state. If you don't have a use for these procedures inheriting the class EL_DEFAULT_SESSION_EVENTS implements do nothing behavior.

3. Implement reflection information

Declare remotely callable procedures and functions by implementing the functions array and procedures array as functions. (Note: these should not be once functions.) Each array contains a list of tuples mapping a remotely callable function or procedure name to an Eiffel agent. You also need to declare an array of argument deserialization functions in the array creation_functions. These are functions which build an Eiffel argument object for a remote call from the XML data in a remote call message.

For example:

procedures: ARRAY [like procedure_mapping] is
--
do
Result := <<
["go", agent go],
["bo", agent bo],
..
>>
end

functions: ARRAY [like function_mapping] is
--
do
Result := <<
["foo", agent foo],
["bar", agent bar],
..
>>
end

The EROS server framework uses information from the function empty_operands in class ROUTINE (the return type of the agent to do basic type checking against the arguments sent by the client.

The result type of functions defined as being remotely callable must meet one of the following conditions:

  1. It conforms to type STRING.
  2. It conforms to type EL_SERIALIZEABLE_AS_XML.
  3. Calling the out function from the universal ancestor ANY, returns a numeric string representation of the object.
There is also a special category of remotely callable functions that are not called directly but referred to as named constants in the argument list of a remote call message. These named constants are implemented as once functions in a remotely callable class and are passed as arguments to a remote routine. More on this later.

4. Declare argument types buildable from XML data

Any argument type which is not either an expanded boolean, expanded numeric type or string type, requires an entry in the creation_functions array specifying a placeholder argument type name in curly braces followed by a function creating an object of this type from the XML call document data. It is completely up to the developer how they want to implement the creation functions but you are encouraged to try out the Eiffel Loop way of building Eiffel objects from XML. This involves implementing a descendant of class EL_BUILDABLE_FROM_XML specifying make_from_string as a creation procedure and then using this as the result type of the argument creation function.

In the example below class FOOBAR has a creation function create_FOOBAR and likewise class GOBO.

creation_functions: ARRAY [like creation_function_mapping] is 
--
do
Result := <<
["{FOOBAR}", agent create_FOOBAR],
["{GOBO}", agent create_GOBO],
..
>>
end

create_FOOBAR (str: STRING): FOOBAR
--
do
create Result.make_from_string (str)
end

create_GOBO (str: STRING): GOBO
--
do
create Result.make_from_string (str)
end

If there is some routine foo which takes type FOOBAR as an argument, a call message would look like the following:

<?xml version="1.0" encoding="UTF-8"?>
<?call foo ({FOOBAR})?>
<foobar-data>
..
</foobar-data>

Here the argument to foo is constructed from the XML document data foobar-data. Also there is no reason why a routine can't have two buildable arguments both constructed from the same document. There would however be a slight overhead of the document being parsed an extra time for the second argument.

Building Eiffel objects from XML parsing events in EROS

EROS makes use of the Eiffel Loop XML deserialization framework in order to construct Eiffel objects from the XML data in call requests. Hence forth referred to as ELXD, this framework works by traversing an XML document and comparing an auto-constructed xpath for each node to a developer defined table of XPaths mapped to Eiffel agents. When it finds a node that matches one of the xpaths, the corresponding agent is called. The agent procedure has access to the node text and name and can use this to set an object value or extend a list. This sounds like it might introduce a lot of processing overhead but it doesn't. This is because the xpath for each node is constructed according to clearly defined rules ensuring the uniqueness of each xpath. By assuming that the xpaths declared in the table conform to these rules, the matching process is simply a hash table lookup for each node's xpath. The rules are few and simple as only a small subset of the xpath language is used. The XML and xpath tables example below illustrate allowable syntax that ELXD is able to match. ELXD also allows element selection by the value of an attribute.

XPath selection and syntax rules

  1. XPaths are always relative to some predefined element context, usually the root element but there is another possibility which we will come to later.
  2. An xpath selects either an element, a text node or an attribute. Attributes are referred to using the '@' notation and text nodes using the text() function. No other text node syntax is recognized.
  3. Elements may be filtered with a simple predicate expression comparing a single attribute with a string literal.

An XHTML example

XHTML document

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta name="description" content="Free Web tutorials" />
<meta name="keywords" content="HTML,CSS,XML,JavaScript" />
<title>XHTML Example</title>
</head>
<body>
<p>Please Choose a Day:<br/><br/>
<select name="day">
<option selected="selected">Monday</option>
<option>Tuesday</option>
<option>Wednesday</option>
</select>
</p>
</body>
</html>

Data node selection xpaths relative to /html

@xmlns
@lang
title/text()
body/p/text()
body/select/@name
body/select/option/text()

Element filtering xpaths by attribute value predicate

head/meta[@name='description']/@content
head/meta[@name='keywords']/@content
body/select/option[@selected='selected']/text()

Element selection xpaths relative to /html

head
body
body/p/br
body/p/select[@name='day']

Experimental xpath using parent-of-context operator ..

../processing-instruction('foobar')

A class implementing EL_BUILDABLE_FROM_XML

To create an Eiffel class representing this XHTML document, say XHTML_DOC, you inherit from class EL_BUILDABLE_FROM_XML and redefine object_building_actions as a once function containing a list of xpath strings mapped to object construction agents. What the construction procedures do is completely up to the developer. You also need to define the name of the root element. This object contains the actual XML parser and is shared between all the instances of XHTML_DOC. The function Object_building_actions and attribute Root_node_name for the above example might look like the following:

class XHTML_DOC 

inherit
EL_BUILDABLE_FROM_XML

create
make_from_string, make_from_file, make_from_stream

feature {NONE} Implementation

Object_building_actions: EL_EIF_OBJ_BUILDING_ACTION_TABLE is
--
once
create Result.make (<<
["../processing-instruction('foobar')", agent initialize],
["head", agent create_header],
["head/meta[@name='description']/@content", agent set_header_description],
["head/meta[@name='keywords']/@content", agent set_header_keywords],
["title/text()", agent set_title],
["body/p/text()", agent add_body_paragraph],
["body/p/br", agent add_paragraph_line_break],
["body/select/@name", agent set_dropdown_list_name],
["body/select/option/text()", agent add_option_to_dropdown_list]
>>)
end

Root_node_name: STRING is "html"

EL_BUILDABLE_FROM_XML has an attribute node: EL_XML_NODE which can be accessed by the construction procedures to obtain the name and value of the current node. EL_XML_NODE has various conversion functions to obtain a node value as type: STRING, INTEGER, REAL or DOUBLE.

The processing-instruction xpath is experimental (or a solution in search of a problem) and can be used to obtain the value of a processing instruction which is a sibling of the root element (i.e. before or after the root element). For the purposes of EROS it was more convenient to extract the call processing instruction using the search functions in the standard Eiffel string class. This is the only kind of xpath where you can use the parent-of-operator represent by two dots '..'.

After you have defined all the construction procedures in XHTML_DOC and mapped them to xpaths, you can create an instance of XHTML_DOC from an XML string or file with one of the following creation procedures:

  1. make_from_string (str: STRING)
  2. make_from_file (fn: FILE_NAME)
  3. make_from_stream (in: IO_MEDIUM)

In addition to the EROS demo application, more ELXD examples are provided as part of the Eiffel Loop library in project apps/test/test.ecf. See sub-application: XML_TO_EIFFEL_OBJECT_BUILDER_TEST_APP and RECURSIVE_XML_TO_EIFFEL_OBJECT_BUILDER_TEST_APP. The latter demonstrates building from recursively structured XML which brings us to the next topic.

Multi context documents

To make complex XML documents more manageable, ELXD allows you to partition the document into multiple xpath contexts which maybe be recursively nested. Each context represents a document fragment meaning a node sub tree of the document. The number of additional contexts is completely up to the developer, so you can represent the XML using just 1 class or 20 classes (actually there is no upper limit). This is a powerful feature as it allows complete flexibility on how you want to represent the document in Eiffel. For example in our XHTHML example we might wish to deserialize the select tag in a separate context defined by a class DROPDOWN_LIST. To do this you inherit EL_EIF_OBJ_BUILDER_CONTEXT in class DROPDOWN_LIST and define object_building_actions with xpaths relative to the select tag. For example:

Object_building_actions: EL_EIF_OBJ_BUILDING_ACTION_TABLE is
--
once
create Result.make (<<
["@name", agent set_name],
["option/text()", agent add_option]
>>)
end

In the Object_building_actions function of class XHTML_DOC you add a line mapping the desired context element to an agent that creates the new context object and switches the parser into that context. For example:

    ["body/select", agent create_dropdown_list]

Procedure create_dropdown_list creates an instance of class DROPDOWN_LIST and then calls set_next_context with the new DROPDOWN_LIST context as an argument. create_dropdown_list might look something like this.

create_dropdown_list is
--
do
create dropdown_list.make
set_next_context (dropdown_list)
end

When the closing tag for the select element is encountered, the ELXD parser automatically pops back up to the root context. If you need to perform any additional tasks before leaving the context you can redefine procedure on_context_exit in class EL_EIF_OBJ_BUILDER_CONTEXT to perform those tasks. Alternatively you can perform those tasks from the parent context by redefining on_context_return. The argument supplied is the child context just returned from. You have to do an assignment test using the if {x: <class-name>} y then construct where y is of type EL_EIF_OBJ_BUILDER_CONTEXT because y could be any one of a number of different child contexts. In our example x is type DROPDOWN_LIST.

Another possibility for this context switching facility is it allows you to have a recursively defined Eiffel class which is deserializable from recursively structured XML. A recursively defined class is a class which contains attribute features which are derived from the same type as the class itself. This scenario is quite easy to manage with ELXD.

Deserializeable arguments in EROS routine calls

Having looked at the mechanics of implementing EL_BUILDABLE_FROM_XML we are now ready to see how this applies to passing an XML document as an argument to a remote routine implemented in an EROS server. The principle example provided in the EROS demo application is the routine set_input from class FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE. It sets an input vector for fourier transformation.

In the remote call request example below, set_input contains a class placeholder {E2X_VECTOR_COMPLEX_DOUBLE}. This indicates to the EROS server that the argument to set_input should be provided by deserializing the XML document data to an object of type E2X_VECTOR_COMPLEX_DOUBLE.

Although it is more usual to have only one argument buildable from XML, it is possible to have more than one. A second one might skim data from a different part of the document. However this would necessitate multiple parsing of the same document. A workaround for this inefficiency would be to merge all the arguments types by creating a wrapper class. This wrapper class would then become the one and only argument type buildable from XML. You can use the context switching facility covered in the previous section to partition the document into document node sub trees and assign each one to a different class.

Class E2X_VECTOR_COMPLEX_DOUBLE inherits from VECTOR_COMPLEX_DOUBLE and EL_BUILDABLE_FROM_XML. It adds all the ELXD construction procedures necessary to build objects from XML.

<?xml version="1.0" encoding="ISO-8859-1"?>
<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.set_input ({E2X_VECTOR_COMPLEX_DOUBLE})?>
<vector-complex-double count="128">
<col real="-1" imag="0"/>
<col real="-0.98078528040323043" imag="0"/>
<col real="-0.92387953251128674" imag="0"/>
<col real="-0.83146961230254524" imag="0"/>
etc..
</vector-complex-double>

Looking at the actual messages sent by the client in the fourier math demo application, you may have noticed an extra processing instruction after the call instruction: <?type row?> or <?type col?>. This was put in mainly to demonstrate the ELXD xpath construct:

../processing-instruction(<instruction name>)

It is used in E2X_VECTOR_COMPLEX_DOUBLE to specify whether the vector is a row or column type. This could just as easily have been implemented as an attribute in element vector-complex-double and is quite inessential to implementing EROS messages. To keep things simple it has been omitted from the above example.

In the remote class FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE, set_input is implemented by adding an entry to the implementation of the procedures array from the inherited class EL_REMOTELY_ACCESSIBLE:

procedures: ARRAY [like procedure_mapping] is
--
do
Result := <<
["set_input", agent set_input],
..
>>
end

We also have to declare a deserializer (or builder) for class E2X_VECTOR_COMPLEX_DOUBLE by adding an entry to the creation_functions array when implementing this function from EL_REMOTELY_ACCESSIBLE. The function create_E2X_VECTOR_COMPLEX_DOUBLE returns a new instance of class E2X_VECTOR_COMPLEX_DOUBLE with the make_from_string creation procedure.

creation_functions: ARRAY [like creation_function_mapping] is
--
do
Result := <<
["{E2X_VECTOR_COMPLEX_DOUBLE}", agent create_E2X_VECTOR_COMPLEX_DOUBLE],
..
>>
end

create_E2X_VECTOR_COMPLEX_DOUBLE (str: STRING): E2X_VECTOR_COMPLEX_DOUBLE is
--
do
create Result.make_from_string (str)
end

An alternative way to build Eiffel objects from XML

As an alternative to using the ELXD framework it maybe possible to use the Eiffel Loop VTD-XML API but there is a technical problem to overcome first. Just the fact of having the API linked into the application breaks the Eiffel Software network module. This is a problem at the C level and is being investigated with the kind help of Manu at Eiffel Software. It might be a bug in gcc or something else. (See EiffelStudio user group article) It has been successfully used in an application that does ftp upload so there is a good chance that a workaround for this problem may be found in time for the next release of EROS.

VTD-XML for Eiffel

VTD-XML is a non-validating, non-extractive XML processing API written in Java and a parallel version in C. The parser is extremely fast and it has one of the fastest implementations of XPath 1.0 in existence. The Eiffel Loop interface to this library simplifies it's use and allows the possibility of nested queries. There are fewer objects for the developer to manage than in the Java version. In many ways it is preferable to Expat based APIs.

Serializing Eiffel to XML with Evolicity

EROS result types which are not strings or numeric types must implement EL_SERIALIZEABLE_AS_XML. Implementing EL_SERIALIZEABLE_AS_XML only requires that you implement the function to_xml which returns an XML string representation of the result object. You are free to use any serialization scheme you wish but you are encouraged to try out the standard Eiffel Loop serialization framework which uses a text substitution language called Evolicity. This is an Eiffelized version of the Java orientated language, Velocity from the Apache open source collection.

The starting point to make use of Evolicity for serializing Eiffel data as XML is to inherit class EVOLICITY_SERIALIZEABLE_AS_XML in your EROS result type. There are 2 routines to implement:
  1. The once function Getter_functions which returns the type EVOLICITY_GETTER_FUNCTION_TABLE. This is a table mapping Evolicity variable names to features of the Eiffel class context.
  2. The once function XML_template returning a template string containing XML mixed with Evolicity code, This code contains substitution variables defined in the Getter_functions function.

Demo serializeable class E2X_VECTOR_COMPLEX_DOUBLE

The fourier math demo application contains the type E2X_VECTOR_COMPLEX_DOUBLE which is serializeable using Evolicity. A column vector consisting of the following imaginary numbers: [2.2 + 3i ; 2.2 + 6.03i ; 1.1 + 3.5i] would be serialized as the following XML document:
<?xml version="1.0" encoding="ISO-8859-1"?>
<?type col?>
<vector-complex-double count="3">
<col real="2.2" imag="3"/>
<col real="2.2" imag="6.03"/>
<col real="1.1" imag="3.5"/>
</vector-complex-double>
Class E2X_VECTOR_COMPLEX_DOUBLE uses the following Evolicity scripting template to produce this XML:
XML_template: STRING is
-- Substitution template
"[
<?xml version="1.0" encoding="ISO-8859-1"?>
<?type $vector_type?>
<vector-complex-double count="$count">
#foreach $item in $complex_double_list loop
<$vector_type real="$item.real" imag="$item.imag"/>
#end
</vector-complex-double>
]"
This Evolicity example can be understood by reference to the following rules of the Evolicity language:
  1. Evolicity identifiers must start with an alphabetical character optionally followed by a string of alphanumeric or underscore characters.
  2. The standard way to substitute Evolicity variables into the text template is to place the variable name in curly braces prefixed with the '$' symbol, for example foo or foo.bar is: ${foo} or ${foo.bar}. However if there are no identifier characters adjoining the left or right of the variable name as for example ${foo}_x, then this syntax may be abbreviated to just: $foo or $foo.bar
  3. All Evolicity directive statements are prefixed with the hash symbol '#'.
  4. Template text can be output repeatedly for each item in an Eiffel sequence container using the construct:
#foreach <iteration item variable> in <sequence> loop
<directive statements or text>
#end
    The variable declared immediately after the foreach keyword is consecutively assigned to each item in the sequence and can be referred to in the scope of the loop. The sequence must refer to an Eiffel container type conforming to SEQUENCE [ANY]. Each item in the sequence must belong to one of the following category of type:
    1. A type conforming to EVOLICITY_CONTEXT. In this case the iteration variable is a nested Evolicity context containing variables referenced by the member operator '.'
    2. Types where instances have a literal text form obtainable from the out function, for example: BOOLEAN_REF; REAL_REF, INTEGER_REF; STRING_8. References to the iterated item will be substituted into the output text.
All the Evolicity variables referenced in the demo template are defined in the following implementation of Getter_functions
Getter_functions: EVOLICITY_FUNCTION_ACTION_TABLE is
--
once
create Result.make (<<
["count", agent get_count],
["vector_type", agent get_vector_type],
["complex_double_list", agent get_complex_double_list]
>>)
end
The definitions of the getter functions are as follows:
feature {NONE} -- Evolicity reflection

get_vector_type: STRING is
-- result is either 'col' or 'row'
do
Result := Vector_types [type]
end

get_complex_double_list: SEQUENCE [E2X_COMPLEX_DOUBLE] is
--
do
create {VECTOR_COMPLEX_DOUBLE_SEQUENCE} Result.make_from_vector (Current)
end

get_count: INTEGER_REF is
--
do
Result := count.to_reference
end
Note that count must first be converted to a reference before it can be bound to an Evolicity variable. The same is true of other expanded numeric and boolean types. Class E2X_COMPLEX_DOUBLE is an Evolicity context based on class COMPLEX_DOUBLE.
class
E2X_COMPLEX_DOUBLE

inherit
COMPLEX_DOUBLE

EVOLICITY_EIFFEL_CONTEXT
undefine
is_equal, out
end

feature {NONE} -- Evolicity reflection

get_real: DOUBLE_REF is
--
do
Result := r.to_reference
end

get_imag: DOUBLE_REF is
--
do
Result := i.to_reference
end

Getter_functions: EVOLICITY_GETTER_FUNCTION_TABLE is
--
once
create Result.make (<<
["real", agent get_real],
["imag", agent get_imag]
>>)
end

end
Class VECTOR_COMPLEX_DOUBLE_SEQUENCE is a an adapter class to enable iteration over the COMPLEX_DOUBLE items of class VECTOR_COMPLEX_DOUBLE (inherited by E2X_VECTOR_COMPLEX_DOUBLE). The underlying implementation of VECTOR_COMPLEX_DOUBLE is an array of doubles: ARRAY [DOUBLE]. Class VECTOR_COMPLEX_DOUBLE_SEQUENCE shares the array memory area of E2X_VECTOR_COMPLEX_DOUBLE and during iteration converts each item on the fly to type E2X_VECTOR_COMPLEX_DOUBLE.

Other examples

Understanding how class E2X_VECTOR_COMPLEX_DOUBLE works covers the basics of Evolicity and the Eiffel Loop serialization framework. To see more sophisticated examples illustrating conditional directives and other features of Evolicity, open a command terminal and use the following grep command (Linux) to locate all examples in the Eiffel Loop distribution directory. (first change to the directory containing the Eiffel Loop distribution)
egrep -l '(EVOLICITY_EIFFEL_CONTEXT|EVOLICITY_SERIALIZEABLE)' --include=*.e -r Eiffel-Loop
(Total of 25 files)
Not all of the examples are for XML serialization. Evolicity can be used for all kinds of code generating tasks. For example, one project uses Evolicity to migrate a C source code tree for compilation with a different compiler. Another creates HTML playlists from the Rhythmbox media player playlists.

Evolicity reference guide

Conditional directives

#if <boolean expression> then
   <statement block>
#else
   <statement block>
#end
Currently only a limited range of boolean expressions are possible. A future release will implement a fully complete expression parser. For the time being the following types of non-recursive expression are supported:
  1. #if $flag then
  2. where flag is of type BOOLEAN_REF.
  3. #if $a < $b then
  4. where a and b are numeric variables. Either variable could be substituted for a literal constant. Other supported numeric comparison operators are by example:
    1. #if $a > 0 then
    2. #if $a = 0 then
    3. #if $a /= 0 then
  5. #if <expr> and <expr> then
  6. where <expr> is a numeric comparison or boolean reference variable.
Not ideal but enough to cover 80% of situations. If you need to do something more complicated you can implement it as an Eiffel function returning a boolean and then reference the result as a boolean variable in your Evolicity template.

Sequence iterators

#foreach <variable> in <sequence variable> loop
<directive block>
#end
Repeats the block of directives for each item in the sequence. The sequence variable must refer to an Eiffel type conforming to SEQUENCE [ANY]. If the sequence item conforms to type EVOLICITY_CONTEXT then the dot operator '.' can be used to reference variables in the item context. Otherwise it is assumed that the item can be converted to a string by calling the out function from the universal parent class ANY.
In the following example the variable number_spellings is of type LINKED_LIST [STRING] and contains the strings: 'one' ; 'two' ; 'three'.
#foreach $item in $number_spellings loop
$loop_index of $number_spellings.count: "$item"
#end
Output:
1 of 3: "one"
2 of 3: "two"
3 of 3: "three"
loop_index is a predefined variable (see below)

Variable creation

Evolicity does not support the creation and assignment of new variables in an Evolicity template. All variables must be defined in some Eiffel context in the Getter_functions table or else using the generic context EVOLICITY_CONTEXT_IMPL. (see below)

Non Eiffel contexts

Not every Evolicity context has to reference features of an Eiffel class. You can also use a generic context using the class EVOLICITY_CONTEXT_IMPL. Use the procedures: put_variable and put_integer_variable to add variables. There is also a convenience creation procedure to initialize the context from a hash table of type HASH_TABLE [STRING, STRING].

Loop indexes

Every foreach loop has a special loop index referred to in an Evolicity template as: $loop_index. The loop index is predefined and does not need to be declared in the Getter_functions table.

Referencing sequence counts

It is possible to refer to the count function of any iterable container conforming to type SEQUENCE [ANY]. This is useful if you want to output some conditional section of text for the last item of a sequence. Sequence count variables are predefined and do not need to be declared in the Getter_functions table.

Nested XML templates

It is possible to reference the to_xml function of a nested object conforming to type EVOLICITY_SERIALIZEABLE_AS_XML. For example you might have an Evolicity template like the following:
#foreach $item in $list loop
$item.to_xml
#end
Here each list item conforms to type EVOLICITY_SERIALIZEABLE_AS_XML. The to_xml variable is predefined and does not need to be declared in the Getter_functions table of contexts in the sequence list.

You can redefine the function XML_tabbed_indent in the nested class in order to tabulate the text returned by to_xml so that it matches the correct indentation of the outer class template. XML_tabbed_indent is defined as an integer with a value of 0 by default. Setting it to a higher value causes each line of the output of to_xml to be prefixed with that number of tabs. However the first line is not indented because it is assumed that the outer template will already have been indented for the first line of text.

Once function constants as EROS routine arguments

In addition to numeric constants, string constants and objects deserialized from XML, EROS provides for one other kind of argument to an EROS routine, namely constants which are the result of a once function in the target object class.

In the fourier analysis demo you can specify one of two possible windowing functions represented as instances of the class RECTANGULAR_WINDOWER_DOUBLE and DEFAULT_WINDOWER_DOUBLE. A reference to each is supplied in class FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE as the result of once functions Rectangular_windower and Default_windower. When implementing the deferred function functions from class EL_REMOTELY_ACCESSIBLE you must specify a name to agent mapping for each once function that can be referenced in a remote procedure call. This is in addition to the mappings for remotely callable functions.

functions: ARRAY [like function_mapping] is
--
do
Result := <<
..
["Rectangular_windower", agent Rectangular_windower],
["Default_windower", agent Default_windower],
..
>>
end

The procedure set_windower can then be remotely called with either a rectangular or default windower as follows:

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.set_windower (Default_windower)?>

<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.set_windower (Rectangular_windower)?>
For any named constants used like this, the context referred to is always the remote class being called. A more explicit but currently unsupported syntax would be as follows:
<?call {FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.set_windower (
{FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE}.Default_windower
)
?>
Another hypothetical use case for named constants might be to specify a color code constant in a drawing routine for a scalable vector graphics application.

Implementing remote routines

Writing an EROS remote routine is pretty much the same as writing a normal routine. The only difference is if one of the argument types or result types is NOT one of the basic types: integer (any width), real (any width), boolean or string. In the case of a non-basic argument type it must be buildable from XML using a creation function listed in the creation_function array. For non-basic result types the type must conform to EL_SERIALIZEABLE_AS_XML and implement the to_xml function serializing the data as XML.

For an argument which is not a basic type there is one other possibility which is that it could be a named constant referring to a once function. In that case the argument type will be the same as the once function result type. (See section: Once function constants as EROS routine arguments)

Result type checking and return message to the client

When the EROS server receives a result to send back to the client it first checks whether the type conforms to EL_SERIALIZEABLE_AS_XML. If it does it calls the to_xml function and sends the resulting string back to the client.

Failing that, EROS next checks whether the type conforms to type STRING in which case this is the result returned to the client.

Failing that EROS assumes that the result can be serialized to a string by calling the the out function from the universal ancestor class ANY. Numeric and boolean results fall into this category.

Procedure return message

By definition a procedure does not have a result but EROS still needs to let the client know that a procedure call request succeeded. To accomplish this the acknowledgment string 'procedure-ok' is returned to the client as though it were a result.

Eiffel Loop logging

EROS makes extensive use of the Eiffel Loop logging framework. The beauty of this framework is that the output is indented to show the entry and exit from routines. Each entry and exit to a routine is documented with a header and trailer output text based on the class name and routine name. The following is some sample output from a test program for the Eiffel Loop VTD-XML API. The test function executes an xpath query looking for http urls in an XML document.

 1>     VTD_XML_TEST_APP.test_bio_2 (
1> argument (1) = "//value[@type='url' and contains (text(), 'http://')]"
1> ) is
1> doing
1>
1> EL_XPATH_ROOT_NODE_CONTEXT.context_list is
1> doing
1>
1> end -- EL_XPATH_ROOT_NODE_CONTEXT
1> http://iubio.bio.indiana.edu/grid/runner/
1> http://iubio.bio.indiana.edu/grid/runner/docs/bix.dtd
1> http://www-igbmc.u-strasbg.fr/BioInfo/ClustalW/
1> http://geta.life.uiuc.edu/~gary/programs/fastDNAml.html
1>
1> end -- VTD_XML_TEST_APP
The code which produced the above output is as follows:
class
VTD_XML_TEST_APP

inherit
EL_SUB_APPLICATION

feature -- Basic operations

test_bio_2 (xpath: STRING) is
-- list all url values
local
node_list: EL_XPATH_NODE_CONTEXT_LIST
do
log.enter_with_args ("test_bio_2", << xpath >>)
node_list := bio_info_root_node.context_list (xpath)
from node_list.start until node_list.after loop
log.put_line (node_list.context.string_value)
node_list.forth
end
log.exit
end

feature {NONE} -- Implementation

bio_info_root_node: EL_XPATH_ROOT_NODE_CONTEXT
Note that each logged routine must start and finish with a paired call to enter_with_args and exit and that the first argument to enter_with_args matches the name of the routine. The log object maintains a logging call stack. A call to enter_with_args pushes a new routine onto the stack and exit pops the entry. The second argument is of type ARRAY [ANY] and is used to log any routine arguments. Routine enter_with_args calls the out function from the universal ancestor class ANY for each of the array items and lists them each on a separate line as argument (1), argument (2) etc.

Comment on Java

This type of logging would be difficult to implement in Java as multiple return instructions could appear anywhere in the routine. (Not that I haven't tried, in fact this framework originally started life as a Java framework) This is another argument in favor of languages like Eiffel which disallow arbitrary returns mid routine. Also Java is littered with all kinds of exception handling which greatly increases the number of exit paths from a routine (not to mention making the code more difficult to read). Eiffel has exception handling too but the philosophy of Eiffel is to head off unbridled use of exceptions by extensive debugging with the use of contracts instead of relying on exceptions to show you the bugs. But I digress.

Enter and exit variations

A number of variations exist for the enter and exit procedures in the log object:
  1. If you do not wish to log any routine arguments you can use the form:
  2. log.enter ("test_bio_2")
  3. If you wish to suppress the routine header and trailer output text you can use the form:
  4. log.enter_no_header ("test_bio_2")
    ..
    log.exit_no_trailer

Managing exceptions

In order to maintain the integrity of the logging routine stack it is important to balance every call to log.enter with a call log.exit on exiting a logged routine. However if your routine has a rescue clause and an exception occurs, these exit calls are skipped not only in the current routine but also in all sub routines before the point where the exception was thrown.  If you wish to recover from the exception by doing a routine retry you need a way to restore the logging routine stack back to what it was before the first log.enter call at the start of the routine. You can accomplish this by saving the state of the logging stack to a local variable before the log.enter call and use this variable to restore the logging stack in the rescue clause. The following code illustrates:
my_routine is
-- Exception handling routine
local
log_stack_pos: INTEGER
do
log_stack_pos := log.call_stack_count
log.enter ("my_routine")
..
log.exit
rescue
log.restore (log_stack_pos)
..
retry
end

Including logging in your application

There are a number of ways to include logging in your application. The first is to inherit EL_LOGGED_APPLICATION in your root class and implement the function Log_filter (see below). You must then make sure that init_logging is the first routine called in the application entry make procedure. A slightly simpler way is to inherit from class EL_SUB_APPLICATION in your root class. This class has a make procedure already defined which calls init_logging, you only have to implement the procedures initialize and run. The routine make must be listed as a creation procedure. Inheriting from class EL_SUB_APPLICATION has some incidental benefits including:
  1. Graceful handling of the ctrl-c program interrupt with the possibility of putting application cleanup into a redefinition of procedure on_operating_system_signal.
  2. Integration with the Eiffel Loop multi mode application framework. This framework allows you to select from different applications by a command line switch. Useful for managing many small applications that hardly justify the disk resources of a separate project. Not to mention the CPU resources of a finalize. Have you ever looked at the amount of diskspace taken up by EIFGENs. Even the time to delete one is substantial.
To including logging facilities in any class, inherit from class EL_MODULE_LOG and add an entry for that class in the log filter array. (see below)

By default logging is not active in the application. It must be turned on using the -logging command line switch. If you don't wish to rely on the command switch you can turn logging on from within your root class by including the call:
logging.activate
If you wish to use this in any other class you must inherit from EL_MODULE_LOGGING. The following call globally disables all logging except for calls made to the log_or_io object:
logging.deactivate

Log output filtering

The logging framework offers a simple way to filter the output by class and routine. The root class of your application should inherit class EL_LOGGED_APPLICATION and implement the routine Log_filter as a once function returning an array of tuples. The Log_filter for class VTD_XML_TEST_APP is implemented as follows:
feature {NONE} -- Constants

Log_filter: ARRAY [TUPLE] is
--
once
Result := <<
["VTD_XML_TEST_APP", "test_bio_1", "test_bio_2", "-test_bio_3"],
["EL_XPATH_ROOT_NODE_CONTEXT", "*"],

-- Output suppressed by prefixing wildcard with hyphen
["EL_XPATH_NODE_CONTEXT_LIST", "-*"],

["EL_XPATH_NODE_CONTEXT", "*"],
["EL_VTD_EXCEPTIONS", "*"]
>>
end
Each tuple in the array consists of the name of the class you wish to enable logging for, followed either by a list of individual routines or a wildcard character '*' to indicate that any and all routines are to be logged for that class. You can disable logging for any particular routine by prefixing the name with a hyphen. In the case of a wildcard, prefixing with a hyphen disables all logging for that class. The class filter is compared only with the generating class name so all child classes in a particular inheritance tree must be listed individually.

User step through logging

For debugging purposes you may wish to pause execution on the exit of each logged routine. The following call causes the application to stop execution on the exit of every logged routine and prompts the user to press enter to continue:
logging.set_prompt_user_on_exit (true)
The logging object is available in the root class or by inheriting EL_MODULE_LOGGING.

Logging threads

Logging a separate thread requires the following steps:
  1. Inherit both EL_IDENTIFIED_THREAD_I and EL_MODULE_LOG_MANAGER in the class inheriting THREAD. Alternatively you can inherit EL_IDENTIFIED_THREAD which already inherits class THREAD.
  2. Call procedure add_visible_thread in the first line of your execution procedure to register the thread with a name.
execute is
--
do
log_manager.add_visible_thread (Current, "My thread")
..
end
By default it is the log output of the main thread that is visible in the console terminal. To change the logging output visible in the console to another thread call redirect_thread_to_console with the thread's index. The index of the main launch thread is 1. Subsequently added threads have indexes of 2, 3, 4 etc. Use function is_valid_console_index to check if the index is valid.
log_manager.redirect_thread_to_console (index)
It is this index which is displayed as part of the log output prompt. If you are not sure what the index of the thread is you can obtain it from the thread name with a call like:
my_thread_index := log_manager.thread_index ("My thread")

Controlling console output in Vision2 applications

The logging framework includes an extension to Vision2 that allows thread output to the terminal console to be controlled by a toolbar. The toolbar contains a dropdown box allowing you to select the thread log output to view in the console terminal. There are also some history navigation buttons to switch between previously selected threads.
To make use of this extension, inherit from the generic class EL_LOGGED_APPLICATION_WITH_CONSOLE_MANAGER in your root class and supply a main window class parameter conforming to EL_TITLED_WINDOW_WITH_CONSOLE_MANAGER. The main window of your application must inherit EL_TITLED_WINDOW_WITH_CONSOLE_MANAGER. A drop down box is automatically populated with a list of logged threads as they are added. This thread selection list is located on a toolbar named console_manager_toolbar. Other developer components can be added to the right.
If logging is not enabled with the -logging command switch then the console manager toolbar will be empty. If you wish to display the console thread switching controls with logging disabled use the command switch -console. You may wish to do this if you are using the "always on" logging object log_or_io. (See below)

Logging procedures

Access to the logging routines is through feature log of class EL_MODULE_LOG. The log object conforms to type EL_LOG which has numerous procedures for writing to the log as well as some useful functions.

Always on logging

Class EL_MODULE_LOG also has a special logging object named log_or_io. This is used in the same way as the usual log object with the difference that the output will still be written to the console even if logging is globally disabled. It can be used to write to the console instead of the usual io medium from class ANY.

Log files

All log files are put in a sub directory logs of the current working directory. If you are making your application loggable using EL_SUB_APPLICATION then these log files are automatically deleted when the application exits. If you want a chance to inspect the log files in an editor before they disappear there are a number of ways to do this:
  1. Use the command line switch -keep_log_file. The log files will not be deleted and will not be overwritten during subsequent application runs. It is recommended to delete them manually.
  2. Redefine the boolean attribute Ask_user_to_quit from class EL_SUB_APPLICATION to return the value true. The user will then be prompted to hit the return key before the application exits and the log files deleted.
If you are using the Eiffel Loop multi application mode framework then the log files are placed in the following subdirectory of the user home directory derived from the executable name and sub application name.
/<user>/home/.<executable name>/<sub app name>/logs
For example if the executable is named foo and the sub application is bar then for user joeblogs the log directory path is:
/joeblogs/home/.foo/bar/logs

Commenting out log lines

Allthough having logging turned off is usually sufficient to maximize performance of the application it may sometimes be desirable to comment out all the logging lines. An autoedit utility application is included for that purpose in the toolkit project. The best strategy is to comment out logging calls by hand in performance critical sections.

Future enhancements

At present changes to the log filtering necessitates a recompilation of the code. However an enhancement planned for a future release will allow the default log filtering to be overridden by an XML logging configuration file.

Developing an EROS client application

Non-Eiffel clients

To write an EROS client in a language other than Eiffel requires an understanding of the EROS network protocol and how to correctly format the remote call messages. The rules are few and simple.

Establishing and closing a connection

To establish a socket connection with the server use a standard network socket class which can send and receive text messages. The client must know in advance what port the server is listening is on. Once the connection is established it is kept alive for the duration of the client session during which any number of remote calls can be made. The client can send either a remote call message or a quit command which causes the server to close the connection and terminate the session.

Sending request messages and commands

The EROS server expects the end of all client messages to be delimited with the ctrl-z EOF character (ASCII DEC 26) and replies likewise. There are only 2 types of client message to the server as follows:

  1. A remote routine call request formated as an XML document starting with the standard XML declaration instruction <?xml ..?> and followed by a 'call' processing instruction <?call ..?>

  2. A quit command to end the session.
There are 4 types of server message to the client as follows:
  1. An XML document starting with the standard XML declaration instruction <?xml ..?>. This is sent in reply to a function call request returning a result that is not a basic numeric or string type.
  2. An error setting command with the syntax: set_error (<error number>): <error message>. The error numbers and their meanings can be found in class EL_REMOTE_CALL_CONSTANTS. The error message is easily parseable in the client by locating the colon and space after the closing bracket.
  3. The string 'procedure-ok' to indicate successful execution of a procedure.
  4. A function result string representing one of 4 basic result types:
    1. A boolean string: 'true' or 'false'.
    2. An integer string
    3. A floating point string using Eiffel syntax
    4. A string of characters (that does not look like any of the preceding message possibilities).

Remote call message syntax

EROS remote call messages are contained in an XML processing instruction and placed immediately after the XML document declaration <?xml ..?>. The syntax of a call message processing instruction from left to right is as follows:

  1. Processing instruction beginning symbols <?
  2. The keyword call
  3. White space
  4. The name of a remotely accessible Eiffel class enclosed by curly braces {FOO}
  5. The routine selection dot-notation dot
  6. A routine identifier
  7. Optional white space
  8. An optional comma separated argument list enclosed by a left and right parenthesis. An argument is one of the following:
    1. A integer constant in Eiffel syntax
    2. A floating point constant in Eiffel syntax
    3. A singly quoted string literal in XML syntax
    4. An identifier referring to a constant in the remote class being called. (once function)
    5. The name of an Eiffel class which is deserializeable from the XML in the document body, enclosed by curly braces {BAR}.
  9. Optional white space
  10. Processing instruction ending symbols ?>
It is possible for the XML document body to be left blank (ie. with no root element) in which case the routine call either has no arguments or only one the argument types 1 to 4 mentioned above.

Eiffel clients

Client toolkit

EROS has a small toolkit of 2 classes that make it easy to write proxy classes for remote classes.

  1. The first is EL_NETWORK_STREAM_SOCKET, a descendant of the standard EiffelStudio NETWORK_STREAM_SOCKET with the addition of put_string and read_string routines for sending and receiving complete XML files as a single string. This is made possible by the automatic addition of the EOF delimiter, ctrl-z (DEC 26) in put_string. The procedure read_string reads fixed lengths blocks until it finds one with ctrl-z. This class is used in conjunction with the class EL_REMOTE_PROXY.

  2. Class EL_REMOTE_PROXY is used to create proxy classes for remote classes. To make a proxy class, create a class named with the suffix '_PROXY' appended to the name of the remote class and inheriting class EL_REMOTE_PROXY. So for example the proxy class for class FOO is FOO_PROXY. If this naming convention is not adhered to, the correct name of the remote class will not be derived. There are no deferred routines to implement, you just cut and paste the routines you want to call remotely minus the body, ie. just the routine interfaces. In place of the body you make a call to the procedure call. This routine takes care of creating the remote call processing instruction and inserting it into a XML document and sending the request to the server. The resulting reply is accessed as result_string.

Common ancestor for proxy and remote class

When creating a proxy class for a remote class it is useful (but not essential) to define an abstract interface (a deferred class with all deferred routines) for all the remotely callable routines and uses this as a common ancestor for the proxy and remote class. If both the proxy and remote class are linked into the same project, you can use the descendant feature viewer in EiffelStudio to switch between the proxy version and the remote version of a routine.

Implementing proxy routines

To implement a proxy routine, copy and paste the routine declaration from the remote class into the proxy class. The body of the remote routine will be replaced by a call to procedure call followed by some code to check if any remote error occurred.

In the case of a proxy function, a successful call will return a string accessible through result_string, an attribute of class EL_REMOTE_PROXY. It must first be converted to an Eiffel type before it can be assigned to the function Result.

Procedure call take 2 arguments as follows:

  1. A string constant containing the name of the remote procedure.
  2. A tuple listing any Eiffel arguments. (ie. list the arguments in square brackets [a, b, ..]) Allowable argument types include any expanded numeric or string type as well as the special string type EL_EIFFEL_IDENTIFIER which is used to reference a once function in the target object. Any complex type should implement an XML serialization scheme and conform to both EL_SERIALIZEABLE_AS_XML and EL_BUILDABLE_FROM_XML. It will be serialized as an XML document with the call instruction inserted after the XML declaration. If there are no arguments, specify an empty tuple: [].

The following example code illustrates all possible argument types. Class BAR conforms to both EL_SERIALIZEABLE_AS_XML and EL_BUILDABLE_FROM_XML:

class FOOBAR_PROXY

inherit
EL_REMOTE_PROXY

feature -- Access

foo (x: INTEGER; y: DOUBLE; s: STRING color: EL_EIFFEL_IDENTIFIER; bar: BAR): INTEGER is
--
do
call (foo_, [x, y, s, color, bar])
if has_error then
io.put_string ("ERROR: ")
io.put_string (Error_messages [error_code])
io.put_new_line
else
Result := result_string.to_integer
end
end

foo_: STRING is "foo"

end

The following call to foo:

foo (5, 1.2, "game", Color_red, bar)
will be translated into an EROS call message something like:
<?xml version="1.0" encoding="ISO-8859-1"?>
<?call {GOBO}.foo (5, 1.2, 'game', Color_red, {BAR})?>
<bar>
etc..
</bar>

Note that the bar argument has been replaced with a class place holder representing the deserialized XML document.

Converting the result_string to a result

For numeric and boolean types use the conversion functions of class STRING to assign a value to the routine result. If the type of Result is a class STRING then no conversion is required. Assign result_string directly. The only other possibility is that the result_string is an XML document in which case the result type should be deserializeable from XML by implementing the ancestor class EL_BUILDABLE_FROM_XML. If for example you have a class FOO implementing EL_BUILDABLE_FROM_XML, the code to assign to Result will look like the following:
foo: FOO is 
--
do
call ("foo", [])
if has_error then
create Result.make
else
create Result.make_from_string (result_string)
end
end

The multi-mode application framework

The Eiffel Loop multi-mode application framework allows you to compile a number of different (but perhaps related) applications into a single application and select one of them to run by a command line switch. This framework also has an optional facility to create self installing applications.

Command line options

Multi-mode applications respond to a number of special command line options:
  1. If no options are specified the application lists all the sub applications printing both it's command word option and description. At the end the user is prompted to select one application for launch by it's menu number. Hitting return without selecting a number exits the menu. If the application requires a command line argument it is better to launch the application using it's specific command word option unless you are happy with the application default value for the argument.
  2. If the first word option is the specific word option for a sub application, that sub application is launched.
  3. If the first word option is: -install it activates a self-install process. The application executable is copied into the user bin directory and selected sub applications will have a desktop launch menu entry or a file context menu created for them.
  4. If the first word option is: -uninstall the application is uninstalled.

Standard multi-mode applications

There are 2 steps to combine a number of applications into one using this framework:

Step 1

For each individual sub application you wish to combine, inherit the class EL_SUB_APPLICATION implementing the procedures initialize and run and the function Log_filter. Specify make as the creation procedure.

Step 2

Create a root class inheriting class EL_MULTI_APPLICATION_ROOT and implement the function Application_manifest to return an array of application containers conforming to the generic type EL_APPLICATION_LAUNCHER [EL_SUB_APPLICATION]. Each item in the manifest should be a reference to the result of a once function returning an instance of EL_APPLICATION_LAUNCHER using the name of the sub application as a class parameter. For example the following code makes the sub applications FOO and BAR launchable from root class APPLICATION_ROOT with the command line switches -foo and -bar.
class
APPLICATION_ROOT

inherit
EL_MULTI_APPLICATION_ROOT

create
make

feature -- Applications

foo: EL_APPLICATION_LAUNCHER [FOO] is
--
once
create Result.make ("foo", "Description of FOO application")
end

bar: EL_APPLICATION_LAUNCHER [BAR] is
--
once
create Result.make ("bar", "Description of BAR application")
end

feature {NONE} -- Implementation

Application_manifest: ARRAY [EL_APPLICATION_LAUNCHER [EL_SUB_APPLICATION]] is
--
once
Result := << foo, bar >>
end

end

Self installing multi-mode applications

It is possible to use the multi-mode framework to create applications that are self installing, meaning that the application will copy it's own executable into the user bin directory and create either a desktop application launch menu entry or a file context menu launcher which will launch the application passing the currently selected file (or directory) path as an argument. The installation process is invoked by launching the application with the command line switch -install. Use -uninstall to undo the install. Currently it is only implemented for the Gnome desktop but a future release will provide an implementation for KDE, Windows XP and Windows 7. (In fact it may already work for KDE as it is rumored that KDE and Gnome share a common menu standard but I haven't had time to test this conjecture).

Creating a self installing multi-mode application is the same as creating a standard multi-mode application except that you use the creation procedure make_install instead of make for each application container instance of type EL_APPLICATION_LAUNCHER. The difference between make_install and make is that make_install takes an extra argument of abstract type EL_APPLICATION_INSTALLER which is the parent class of the following platform independent installers:

  1. Class EL_CONTEXT_MENU_SCRIPT_APPLICATION_LAUNCHER creates a file context menu launcher. Under Gnome this will create an entry in the nautilus-action Scripts submenu. Under Windows this will generate an entry in the Send file sub menu.
  2. Class EL_MENU_APPLICATION_LAUNCHER creates a desktop launcher under the application menu.
  3. Class EL_MENU_CONSOLE_APPLICATION_LAUNCHER creates a console application desktop launcher under the application menu.
Instances of the above abstract classes are created by the following factory functions available in class EL_MULTI_APPLICATION_ROOT:
  1. context_menu_script
  2. application_menu
  3. console_application_menu    

context_menu_script

This creation function takes two arguments:
  1. A file context menu path specified with the Unix forward slash path separator. Under Gnome this will be the path of a nautilus-action script relative to the user nautilus-action script directory. Under Windows this will be the path of a "send file" application shortcut relative to the user "send file" directory. Any directory steps in the path represent file context sub menus.
  2. The name of the command line word option which will precede the file or directory path command line argument.

application_menu

This creation function takes two arguments:
  1. An array representing a sub menu path. Each item in the array is of type EL_APPLICATION_MENU representing a sub menu. There are two factory functions to create a sub menu item:
    1. submenu is used to specify a standard system defined application sub-menu, for example: Sound & Video under the Gnome desktop or Accessories under Windows XP.
    2. custom_submenu is used to create a user defined sub menu and takes 3 arguments:
      1. The menu name
      2. A menu comment that displays as a mouse cursor roll over.
      3. A file path to a menu icon. It is recommended to obtain the path using function desktop_menu_icon of class EL_IMAGE_PATH_ROUTINES. A global instance of this class is available as Image_path from class EL_MODULE_IMAGE_PATH. Function desktop_menu_icon finds the icon in a standard directory defined by the Eiffel Loop scons build system. See comments in the source code.
  2. An application launch menu entry item defined by class EL_APPLICATION_MENU_ENTRY. You can create an instance of this class using the factory function menu_launch_entry which takes 2 arguments:
    1. A menu name
    2. A file path to a menu icon. The same comment applies as to a custom menu icon path.

console_application_menu

This creation function is intended for console applications and is much the same as the preceding application_menu except the return type is EL_MENU_CONSOLE_APPLICATION_LAUNCHER which creates a menu launch entry that launches the application in a console terminal. If you assign the result of this function to a local variable you can override the default size and position of the console using the procedures set_terminal_position and set_terminal_dimensions. The former procedure takes integer arguments in pixel units and the latter as character counts.

Examples

Nearly all the Eiffel Loop example projects contain multiple applications.

The EROS server project includes three sub-applications related to thread programming:
  1. Full multi-threaded fourier transform EROS server
  2. An animation demonstrating use of a class for generating events at regular intervals
  3. A developer dummy application to maintain the code of a media player.
The EROS client project includes two sub-applications:
  1. EROS test client for fourier transform application
  2. Light weight fourier transform EROS server as a single threaded console application.

Building and compiling the EROS demo application

Environment requirements

So far EROS has only been tested on Ubuntu Linux 8.01 using EiffelStudio 6.3. Many of the modules from which EROS has been constructed have been tested and in some cases developed on Windows XP but there are still a number of classes which require a Windows implementation. Support for Windows is planned for a future release.

Obtaining the Eiffel Loop libraries

All of the C libraries and third party Eiffel libraries have been included with the Eiffel Loop package. Besides a gcc compiler the only other things you need are SCons Ver 1.2 and Python 2.5 or later. Python is required not only for SCons but also for some of the Eiffel Loop modules which use Python libraries via the PEPE module.

Download link

http://www.eiffel-loop.com/download/Eiffel-Loop.1.0.tar.gz

Installing Eiffel Loop

EROS is distributed as part of the Eiffel Loop libraries so first unpack the distribution archive to a preferred location. Eiffel Loop has it's own scons based build system supported by a Python module. A setup script setup.py is provided in the Eiffel Loop root directory to install this Python module. Open a command terminal and run this command from the Eiffel-Loop directory.
sudo python setup.py install --install-scripts=~/bin
As well as some extensions to Scons it will also install a script that configures the project environment for EiffelStudio based on a dictionary table in a project file project.py. The script is named: launch_estudio.py

If you omit the --install-scripts option from the setup command the script will be installed in /usr/bin.

Each example project directory contains in addition to the ecf files, the two files: project.py and SConstruct. The SConstruct is a standard file which is the same for all Eiffel Loop projects. The file project.py contains environment variable configuration table.

The scons extension module

Eiffel Loop has a Python module eiffel_loop.scons.eiffel for building Eiffel finalized and frozen projects. It does not do dependency checking on Eiffel source files. It only compares the ecf project file and the F_code or W_code target executable. To force a rebuild either delete the executable or resave the ecf project file so it appears to scons as modified.

Environment variables

The eiffel_loop.scons.eiffel builder constructs a project environment using the environ dictionary specified in the standard file project.py located in the project directory.

Precompiles

Any Eiffel precompiled libraries are built using the standard SConstruct located in the Eiffel-Loop/precomp directory.

C libraries

The eiffel_loop.scons.eiffel builder scans the project for C libraries. If the library does not exist but there is a SConstruct file in the parent directory then that SConstruct is included in the build.

Graphics

The eiffel_loop.scons.eiffel builder scans the project for three kinds of special variable tags describing the location of graphics for use with the class EL_IMAGE_PATH_ROUTINES:
<variable name="icons" value="graphics/icons"/>

<variable name="desktop-icons" value="graphics/desktop-icons"/>

<variable name="images" value="graphics/images"/>
The paths in the value attribute are relative to the project directory. Under Unix these graphics directories will be installed in the directory:
/<user>/home/.local/share/<executable name>
The graphics will be installed in one of three sub directories: icons, images and desktop-icons.

Absolute paths for these graphics can be obtained by calling one of the functions icon, image or desktop_menu_icon from class EL_IMAGE_PATH_ROUTINES. Each of these functions takes a file path argument relative to the standard directories: icons, images and desktop-icons. A global instance of EL_IMAGE_PATH_ROUTINES is obtainable from class EL_MODULE_IMAGE_PATH as Image_path.

Building the demo applications

Finalized applications

To build a finalized application open a command terminal and set the current directory to: Eiffel-Loop/apps/example/thread.
First build the server:
scons finalize=yes project=server.ecf
Then the client:
scons finalize=yes project=eros-test.ecf
The first time the projects are built, some precompiled Eiffel libraries and C libraries dependencies will be built. The precompiled Eiffel libraries especially take some time to build.

Frozen (or debug mode) applications

To build the demo projects for use in the EiffelStudio IDE use the following commands:
scons freeze=yes project=server.ecf
scons freeze=yes project=eros-test.ecf
This will build "workbench mode" applications and install any associated graphics.

Opening projects in EiffelStudio: dos and don'ts

Do not launch EiffelStudio directly but instead use the script launch_estudio.py which is installed as part of the Eiffel Loop setup process. This script imports the table setting up the project environment variables from the script project.py found in each project directory. To use the script, open a command terminal and change to the project directory and type launch_estudio.py followed by the name of the project configuration file:
launch_estudio.py server.ecf
OR
launch_estudio.py eros-test.ecf

Problem compiling

Make sure Python module eiffel_loop.scons.eiffel has been installed from the python-support directory. If not follow the instructions for installing Eiffel Loop using setup.py.

Make sure you are using SCons Ver 1.2. It may work with an earlier version but this hasn't been tested.

Installation

If you are using the Gnome desktop it is possible to automatically create application menu entries for the client and server by launching the applications from the project directory with the -install command switch:
EIFGENs/classic/F_code/el_server -install
EIFGENs/eros_test/F_code/el_eros_test -install
The applications will be installed in the users home bin directory (~/bin) and launch menus created under the menu path Applications/Programming/Eiffel Loop. The server menu launcher has maximum threads set to 3. Once it is installed you can manually change this using the desktop menu editor in the system preferences (Windows or Gnome). The el_server app will also install a menu launcher for a physics lesson animation. The el_eros_test app will also install a menu launcher for a light weight console version of the fourier transformation server.

To uninstall the menus use the -uninstall command switch.

Launch

Eiffel Loop makes a lot of use of a framework for packing multiple applications into one executable. The idea is that you put like applications into one project and select them with a command switch. You can find out what applications are in an excutable by running it without any arguments. A list of command options are printed. Besides the EROS server fourier math application, the el_server executable also contains an animation to demonstrate some other features of the Eiffel Loop thread module. Use the following command to start the EROS fourier math server listening on port 8002 (the default is 8001) and with a maximum of 10 threads to handle requests (the default is 30):

el_server -fourier_math -logging -console -port 8002 -max_threads 10
The logging switch turns on the logging. The console switch activates the thread console output switcher. (Recommended) You should now see a small GUI for managing thread log output. Click on the go button and the server is ready to handle requests. Project eros-test.ecf contains a test client for the fourier math demo. It runs some basic tests followed by some randomly generated tests for the number of seconds specified by the the duration argument. The test client is launched with this command:
el_eros_test -test_client -logging -duration 20

Project eros-test.ecf also contains a light weight fourier math test server. It is single threaded and doesn't have any GUI. This is useful for development purposes as it starts up more quickly than the full EROS server which has a GUI. It is recommended that all EROS server applications are developed like this initially before being deployed as a full multi-threaded server. Use ctrl-c to gracefully exit the server. The launch command is:

el_eros_test -test_server -logging

EROS demo application

The demo application has 2 remote classes, SIGNAL_MATH and FAST_FOURIER_TRANSFORM_COMPLEX_DOUBLE. The former generates cosine wave form vectors and the latter does fourier transformations. The client has proxies to these classes which it uses to generate a series of random wave forms, print them out and then get the fourier transformations of these wave forms.

The EROS server kernel architecture

EROS is constructed from a number of modules provided in the Eiffel Loop libraries. A summary of the EROS architecture is follows.

Thread handling

The core of the EROS server is built from two classes found in the Eiffel Loop threading module, EL_MANY_TO_ONE_CONSUMER_THREAD and EL_ONE_TO_MANY_THREAD_PRODUCT_QUEUE. These two classes work in tandem to allow the productions of one particular thread to be consumed by an army of consumer threads. Class EL_ONE_TO_MANY_THREAD_PRODUCT_QUEUE takes two generic parameters, the first being the type of the product objects being consumed, and the second being a product consumer conforming to the type EL_MANY_TO_ONE_CONSUMER_THREAD. Class EL_MANY_TO_ONE_CONSUMER_THREAD takes one generic parameter, the product type. Class EL_ONE_TO_MANY_THREAD_PRODUCT_QUEUE manages the product distribution and creation of consumer threads in a 'delegator' thread. The delegation algorithm is as follows: the producer thread places an item on the queue. If there is an available consumer, the delegator assigns the product to that consumer, or else the delegator creates a new consumer thread and assigns it the product. Class EL_ONE_TO_MANY_THREAD_PRODUCT_QUEUE has an attribute specifying the maximum number of consumers that can be created. If the maximum number of consumers has already been reached and all are unavailable, then the delegator waits for one to become free. When each consumer has finished consuming a product it notifies the delegator that it has become available.

The delegator is an instance of class EL_DELEGATING_CONSUMER_THREAD which inherits from class EL_CONSUMER_THREAD. The latter class is designed to be used in a one producer, one consumer construct. The delegating consumer consumes products only by giving it to another thread to consume, hence the name.

For the purposes of the EROS server, a product is defined as a client connection. The consumption of the client connection product is defined as being a session of servicing multiple requests until such time as the client sends a quit message. The client connection consumer is of type EL_REMOTE_ROUTINE_CALL_REQUEST_HANDLING_THREAD inheriting from EL_MANY_TO_ONE_CONSUMER_THREAD. Once the request handling thread has served (consumed) a client connection it is made available again for another connection and relaunched.

Client connection handling

Client connections are managed by class EL_REMOTE_CALL_CONNECTION_MANAGER_THREAD. The manager thread polls for new connections every 250 millisecs. This latency is divided by 2 a maximum of 5 times if the connection manager receives a cluster of new connections. The minimum latency is about 16 millisecs. New connections are placed on the session queue for servicing by the class EL_REMOTE_CALL_CLIENT_CONNECTION_QUEUE. This class inherits the class EL_ONE_TO_MANY_THREAD_PRODUCT_QUEUE described in the thread handling section.

Connection servicing

Once a connection has been assigned to a thread, the class EL_REMOTE_ROUTINE_CALL_REQUEST_HANDLER is responsible for reading requests from the connection socket, parsing the requests and sending replies. It takes as a creation argument, an array of factory objects for remotely accessible objects. The principle routine serve takes a stream socket as a parameter, and keeps replying to requests until such time as it receives a quit command. If it finds a message starting with an XML declaration, it assumes that there is a processing instruction containing a request immediately after the declaration.

The test client demo application also contains an example of a simple test server as a sub application. This single threaded test server* makes direct use of EL_REMOTE_ROUTINE_CALL_REQUEST_HANDLER without the overhead of thread management or controller GUI. Using this code is recommended for developing and debugging a new EROS application.

(* The project is still compiled as multi-threaded)

Request parsing

EROS makes use of the Eiffel Loop parsing and lexing module to parse the remote call request contained in the XML processing instruction. This module is also used to implement the templating (or text substitution) mini language Evolicity used for textual data serialization. The complete call request parser is implemented in class EL_ROUTINE_CALL_REQUEST_PARSER shown below. The grammar is fairly self explanatory with a small amount of explanation of the constructs all_of and one_of. The all_of construct returns a text match if each pattern in the list returns a match in succession. The one_of construct starts of with the first pattern in the list and works its way down until either one of the patterns matches or the end of the list is reached. It's a bit like a series of nested or else constructs in Eiffel: if a or else (b or else ..) then. It returns on encountering the first match.

The infix operator #with is used to assign a match handling procedure to a particular textual sub pattern. It works by calling procedure set_action_on_match and returns the current pattern object. (It does shoot a hole in the CQS thing but makes life easier) If the sub pattern is successfully matched within the context of the complete pattern being fully matched, then the agent is called with the matching text as an argument. The agents are called in the same order as the matches occur. In the case of a pattern with nested sub patterns, the agent for the parent pattern is called before the child pattern agents. This is a simplified account of the match handling rules but sufficient to understand the EROS request parser.

The root of the grammar is found in function create_pattern which starts with the keyword call. The request parser makes use of some patterns defined in the inherited class EL_EIFFEL_PATTERN_FACTORY . The TP suffix used to name the lexing classes, stands for Textual Pattern.

class
EL_ROUTINE_CALL_REQUEST_PARSER

inherit
EL_LEXICAL_PARSER
rename
make as make_parser,
match_full as parse
redefine
reset_parser
end

EL_EIFFEL_PATTERN_FACTORY
export
{NONE} all
end

create
make

feature {NONE} -- Initialization

make is
--
do
create routine_name.make_empty
create argument_list.make (3)
make_parser
end

feature -- Access

routine_name: STRING

class_name: STRING

argument_list: ARRAYED_LIST [STRING]

feature {NONE} -- Syntax grammar

create_pattern: EL_MATCH_ALL_IN_LIST_TP is
--
do
Result := all_of (<<
string_literal ("call"),
white_space,
class_object_place_holder #with agent on_class_name,
character_literal ('.'),
c_identifier #with agent on_routine_name,
maybe_white_space,
optional (argument_list_pattern)
>>)
end

argument_list_pattern: EL_MATCH_ALL_IN_LIST_TP is
--
do
Result := all_of (<<
character_literal ('('),
maybe_white_space,
argument,
while_not_pattern_1_repeat_pattern_2 (
right_bracket,

-- pattern 2
all_of (<<
maybe_white_space,
character_literal (','),
maybe_white_space,
argument
>>)
)
>>)
end

argument: EL_FIRST_MATCH_IN_LIST_TP is
--
do
Result := one_of ( <<
class_object_place_holder #with agent on_argument,
singley_quoted_string #with agent on_argument,
double_constant #with agent on_numeric_argument,
integer_constant #with agent on_numeric_argument,
boolean_constant #with agent on_boolean_argument,
identifier #with agent on_identifier_argument
>> )
end

class_object_place_holder: EL_MATCH_ALL_IN_LIST_TP is
--
do
Result := all_of ( <<
character_literal ('{'),
class_identifier,
character_literal ('}')
>> )
end

boolean_constant: EL_FIRST_MATCH_IN_LIST_TP is
--
do
Result := one_of (<<
string_literal ("true"),
string_literal ("false")
>>)
end

singley_quoted_string: EL_MATCH_ALL_IN_LIST_TP is
--
do
Result := all_of ( <<
single_quote,
while_not_pattern_1_repeat_pattern_2 (single_quote, any_character)
>> )
end

single_quote: EL_LITERAL_CHAR_TP is
--
do
create Result.make_from_character ('%'')
end

right_bracket: EL_LITERAL_CHAR_TP is
--
do
create Result.make_from_character (')')
end

feature {NONE} -- Parsing match events

on_class_name (matched_text: EL_SOURCE_TEXT_VIEW) is
--
do
class_name := matched_text
class_name.remove_head (1)
class_name.remove_tail (1)
end

on_routine_name (matched_text: EL_SOURCE_TEXT_VIEW) is
--
do
routine_name := matched_text
end

on_argument (matched_text: EL_SOURCE_TEXT_VIEW) is
--
do
argument_list.extend (matched_text)
end

on_numeric_argument (matched_text: EL_SOURCE_TEXT_VIEW) is
--
do
argument_list.extend (once "(" + matched_text.string + once ")")
end

on_boolean_argument (matched_text: EL_SOURCE_TEXT_VIEW) is
--
do
argument_list.extend (once "<" + matched_text.string + once ">")
end

on_identifier_argument (matched_text: EL_SOURCE_TEXT_VIEW) is
--
do
argument_list.extend (once "[" + matched_text.string + once "]")
end

feature {NONE} -- Implementation

reset_parser is
--
do
Precursor
routine_name.clear_all
argument_list.wipe_out
end

end

Contacting the author

You can contact Finnian Reilly, the developer of the Eiffel Loop library collection, with questions and feedback.
finnian at eiffel hyphen loop dot com