Interfacing with C#
When you compile a Cell program, the compiler does not generate a binary executable. Instead, it generates a text file containing code in the chosen output language. We'll examine the C# code generator here. If you define a
Main(..) procedure in you Cell code, the generated code can then be handed over to a C# compiler to generate an executable file. That's how for instance the Cell compiler itself is built, and also the simplest way to build a program that tests your automata. But if you don't define a
Main(..) procedure, the compiler will just generate a set of classes, one for each type of automaton in your Cell code, that can be used to instantiate and manipulate the corresponding automata from your C# code. The compiler will produce two files,
interfaces.txt. The former contains the implementation of the classes generated by the compiler, while the latter documents their interfaces in pseudo-C# code. In this chapter we'll go through the interface of the generated classes and explain how to use them, and what each of their methods is for.
When using C++ as a target language, it is recommended that you don't use the generated classes directly, but create a wrapper class for each of them, and use those instead. With C# the interface of the generated classes is much nicer, and creating wrapper classes is probably not necessary. A better idea is probably to derive your own classes from the generated ones, and add new methods to them. Even though the compiler does its best to map the Cell types to native C# types, there are times when you'll need to manually convert the data that is exchanged between the two languages, and you really don't want to repeatedly perform those conversions all over your codebase: it's much better to have all of them in just one place. If a method of the generated classes requires some manual data conversion, the best thing to do is probably to define an overloaded version of the same method, or a similar one, in the derived class, and have it take care of all data conversions before and/or after invoking the generated one.
The most annoying and time-consuming part of using the classes generated by the Cell compiler from your own C# code is of course converting data back and forth between the C# and Cell representations. There's a number of simple Cell data types that are mapped directly to a corresponding C# type. They are shown in the following table:
|(T1, T2, ...)||Tuple<T1', T2', ...>|
|[T1, T2]||Tuple<T1', T2'>|
|[T1, T2, T3]||Tuple<T1', T2', T3'>|
The above table should be mostly self-explanatory, but there's a couple things that need explaining:
- Symbols are mapped to strings containing their textual representation. For example,
nothingis mapped to the C# string
- Tagged types can be mapped directly to a C# type only if the tag is a known symbol and the type of the untagged value in turn has a direct mapping to a C# type. In this case, the tag is simply ignored and the mapping of the untagged value is used. A type like
<person_id(Nat)>, for example, will be mapped to a
longin C#, and the generated code will take care of adding or removing the
person_idtag as needed.
- The current mapping is not yet taking advantage of one important feature of C#, named tuples, which can be used to map Cell records. That will be fixed soon.
When dealing with more complex data types that are not in the above table, the mapping depends on the direction data is moving in. When passing data from C# to Cell you're expected to pass a string that contains the textual representation of a Cell value. That's neither elegant nor particularly efficient, but at least it's simple and straightforward. When data moves in the other direction, from Cell to C#, it is returned as an object of type
CellLang.Value. Its declaration is shown here:
This interface is implemented by a number of concrete classes each of which is used to represent a particular type of Cell value: symbols, integers, floating point numbers, sequences, sets, binary and ternary relations and tagged values. These concrete classes are hidden from the user, and they can be manipulated only through their common "fat" interface, whose methods can be divided into three groups. The first one comprises all the
bool Is*() methods, which are used to discover the type of the value represented by the target object. Then there's a group of methods that are used to actually access the data held by those objects:
Each of these methods is actually implemented only in some of the classes that can hide behind the
Value interface, and if used with the wrong concrete class will just throw an exception.
long AsLong(), for example, can only be used if the target object actually holds an integer value, which can be checked using the
bool IsInt() query method.
The last method,
void Print(TextWriter writer), is used to generate the textual representation of the value, which is written to the provided
Let's take a look at the interface of the classes produced by the compilation of a relational automaton. We'll start with a very simple one,
As you can see, the generated C# class has the same name of the Cell automaton it derives from, and is nested inside the
Generated static class of the
CellLang namespace. The first three methods,
Execute(..), are the same for all relational automata. All other methods are just accessors that are specific to a particular automaton, and can be used to read pieces of its state, or to invoke its methods.
ReadState() method is the equivalent of the
read instruction in Cell: it takes a snapshot of the state of the automaton and returns it as a
CellLang.Value object. Saving the state of an automaton to a file in text form can be done with the instruction
tw is a valid instance of
SetState(..) is used to set the state of an automaton instance, and is the equivalent of the
write instruction in Cell. It can be used at any time in the life of the automaton instance, any number of times. The new state has to be provided in text form. Here's an example:
If the provided state is not a valid one,
SetState(..) will throw an exception. In that case, the automaton instance will just retain the state it had before, and will still be perfecly functional.
Execute(..) is used to send the automaton a message, which has to be passed in text form. A few examples:
Errors handling works in the same way as with
SetState(). If an error accours an exception will be thrown, but the automaton will remain fully operational, and its state will be left untouched.
The two properties
Counter-specific accessors that return the values of the
updates member variables respectively. Note that the types of such variables is just
Int, which can be mapped directly to
long in C#, with no need to use
Let's now take a look at a more complex automaton,
Supply. Here's the declaration of the generated C# class:
The first three methods of the
Execute(..), are the same as before. The two properties,
NextSupplierId, are just accessors for the corresponding member variables, just like
Counter. All the other methods are new, and are either accessors for some mutable relation variable or wrappers for a Cell method. A first set of methods, of the form
bool In*(..), checks whether a relation contains a given tuple (or value, for unary relations):
A second group of methods, like
UnitPrice(), return the entire content of a given relations, as an array of tuples, or just an array of values in the case of unary relations.
Binary relations with a key on the first column (that is, maps) also have accessors that return the value corresponding to a given key, provided that the relation/map contains such key:
The last group of methods (
LowestPriceSuppliers(..)) are just the compiled C# version of the corresponding Cell methods of the
Switch as our first example. This is the interface of the corresponding generated class:
The first thing to note here is the two enumerations
Output, whose elements are the uppercase version of the names of the inputs and outputs of
Switch. These are used in conjunction with the methods
ReadOutput() as shown here:
As an alternative to
ReadOutput(..), which can operate on any input or output and use the textual representation of a value or
CellLang.Value respectively as an exchange format, the generated class also provides another set of methods each of which can manipulate a single input or output, but that are more convenient to use in most cases. The above code snippet can be rewritten as follow:
SetState(..) methods work in the same way as with relational automata, but with the limitations we've already discussed for time-aware automata. The property
ChangedOutputs provide you with a list of outputs that have changed (or have been active, in the case of discrete outputs) as a result of the last call to
The last thing we need to see is how to deal with time-aware automata. We'll use
The only differences here, apart from the input setters and output getters which are obviously specific to each automaton type, are the two extra methods
SetElapsedMillisecs(..) and the fact that
Apply() now returns a boolean value. The former are the equivalent of the
elapsed instruction in Cell, and the value now returned by
Apply() has the same meaning as the one returned by the
apply instruction in a Cell procedure. Here's an example of how to update an instance of