| The |
New functionality can be added to
The user has written a new C++ class or a new C++ template class, and would like to use this class from withing the interpreter.
The user has written an interface to an external software which
comes with its own language and typesystem. In this case, one
would like to have an automatic mechanism for making the
functionality of the external software available from within
In both cases, the user has to write a “glue definition routine”, which declares the functionality provided by the glue, using a standard API. This routine can then be called from some central point (at startup or when loading a dynamically linked glue library) in order to make the glue functionality available from within the current evaluator.
The
Let us first consider writing a glue definition routine for a simple
C++ class named color. Assume also that the color
class comes with two routines which we wish to export to
|
Then the glue definition routine for color typically looks as follows:
|
The define_type instruction exports the type color to
Whenever a new type is exported to
|
The first routine computes a hash value for c, where
nat stands for unsigned int. The
second flattening routine converts c to a generic
expression; this routine will be used when printing a color. The last
two routines implement equality testing. Notice that there are several
types of equality in
|
The routine hard_eq is a fast test whether c1
and c2 are represented in the same way in memory. In
the case of pointer objects like vectors, one typically tests whether
the pointers match; in particular, two identical vectors which are
stored at different locations would not be “hard equal”.
The routine eq tests whether c1 and
c2 are syntactically equal. Typically, the rational
number
might be equal to the integer
, without being syntactically equal.
The main routines for defining new types and routines are specified in glue.hpp. Here follows a short description:
This routine declares a new C++ type C and exports as name. Whenever a new type is defined, a few other derived types are defined as well (see section ? below).
This routine exports the constant
This routine exports a constructor fun for instances of type C.
This routine exports a function fun as name.
This routine exports a type conversion routine fun: S->D (which defaults to the default converter from S to D) with a given penalty pen. In cases of ambiguity, conversion chains with the least penalty are preferred. The name of the converter should be one among "convert", "upgrade" and "downgrade", depending on the nature and transitivity properties of the converter. Upgraders are used for automatic constructors (such as integer->rational) and downgraders for type inheritance (such as circle->shape). Plain converters cannot be composed with other converters.
This routine exports a language primitive f. The argument to the primitive is a tuple which is not evaluated before f is called. The primitive should take care of the possible evaluation of its arguments itself.
Whenever a new type C is defined by the user, a few
other related types are added automatically. More precisely,
This type is used for aliases to instances of type C
(see alias.hpp). The alias<C>
type plays a similar role as the C++ reference type C&,
but there are some subtle differences. In
| Mmx] | v: Vector Generic := vector (1, 2, 3, 4, 5) |
| Mmx] | x: Alias Generic == v[4]; |
| Mmx] | v := vector (6, 7, 8, 9, 10) |
| Mmx] | x |
In
This type stands for a tuple of elements of type C. By defining a vector constructor using
|
where
|
this will allow you to enter vectors in the
|
This type is also added, for coherence.
|
The overloading will allow for both read-access and write-access using
the same syntax. However, it sometimes happens that you have a (non
constant) table which corresponds to a global environment. In that
case, any access to this table will be a write-access independently if
you really perform some modifications of the table. This may lead to
subtle errors if you really wanted to perform a read-access, since a
write-access might actually modify the table (allocating a non
existant key-value pair, for instance). This subtlety does not occur
for the
The routine define_converter allow the user to define automatic converters between different types. This facility is quite powerful, but has to be used with care: since automatic converters are applied in a very systematic way, they may even be applied in sitations which the user did not foresee.
First of all, the user has to carefully select between the three
different types of converters: upgraders, downgraders and plain
converters. These types differ in the way they may be composed: plain
converters are neither left nor right composable, upgraders are left
composable, and downgraders are right composable. Given a left
composable converter B->C and an arbitrary
converter A->B,
Typically, upgraders correspond to type constructors, such as Integer -> Rational or C -> Matrix(C). Similarly, downgraders correspond to type inheritance, such as Rectangle -> Shape, Sum_series(C) -> Series(C), etc. Plain converters are often used for converters which may involve some loss of data, such as Integer->Double.
A second important property of a converter is the correponding penalty: when several conversion schemes can be used in order to apply a function to some arguments, the scheme with the lowest penalty will be preferred (here we notice that the penalty of a conversion (A,B)->(C,D) is the maximum of the penalties of the conversions A->C and B->D). Among all possible schemes with the lowest penalty, the most specialized function will be chosen (a type T is strictly more specialized than U if there exists a converter T->U but no converter U->T). If no conversion schemes can be found to apply the function, then it will be applied symbolically.
Currently, the following penalties are provided:
This corresponds to an exact match.
This corresponds to the penalty for automatic language-related conversions, such as Alias(C)->C.
This penalty should be used for conversions T->U, where T may be viewed as a mathematical subset of U. Example: Integer->Rational. This penalty is the default one for conversions T->U when T is different from Generic.
This penalty should be used for conversions T->U which can be viewed as mathematical homomorphisms, but not as inclusions. Examples: Integer->Int or, more generally, Integer->Modular(p).
Sometimes, two distinct libraries implement the same or a similar type. In that case, it may be interesting to provide automatic converters between these types (in both ways). Using the higher penalty PENALTY_VARIANT for this kind of conversions will ensure that operations are performed in the library of the types of the arguments, unless an implementation is only available in the other library.
This penalty should be used for all conversions which, even though convenient for the user, entail some loss of information. Example: Integer->Double.
For any type T, this is the penalty of the conversion T->Generic. Hence, if the user provides a generic fall back implementation of an operation, then this will be the penalty for the application of the fall back method.
Many composite types, such as Complexify(C), come with an inclusion C->Complexify(C). Although it is generally correct to use PENALTY_INCLUSION for the corresponding penalty, many unwanted conversions may arise when C=Generic. For this reason, the default penaly for conversions of the kind Generic->T is the maximal penalty PENALTY_PROMOTE_GENERIC.
Some common sources of bugs when using overloading and automatic conversions are the following:
You specified an upgrader Generic->Polynomial(Generic), but addition on generic polynomials can not be applied so as to add one to a polynomial. The point here is that the penalty of the conversion Generic->Polynomial(Generic) should be the maximal penalty PENALTY_PROMOTE_GENERIC, which is larger than the penalty PENALTY_FALL_BACK for the symbolic addition of expressions. The solution to this problem is to implement the following two specialized additions:
+: (Polynomial (Generic), Generic) -> Polynomial (Generic)
+: (Generic, Polynomial (Generic)) -> Polynomial (Generic)
Actually, these operations may be useful for other coefficient types than Generic, since they can usually be implemented in a particularly efficient way.
You forgot to define a generic fall back method for some operation. Sometimes, your implementation may rely on the assumption that a given operation foo has no implementation, so that it will be applied symbolically. When providing an implementation of foo for some type, this assumption may suddenly be violated and provoke infinite loops or incorrect results. In that case, you should provide a default symbolic implementation for foo.
A package with a collection of types, routines and glue definition
routines should be compiled into a dynamic library, which can then be
loaded on the fly into the interpreter. Names of
In the subdirectories examples/fibonacci and examples/gaussian, you may find two simple examples on how
to glue new code to
Assume that we want to add a routine fibonacci for computing Fibonacci numbers to the glue. The file fibonnacci.cpp with the routine fibonacci and the corresponding glue definition routine would typically look at follows:
|
The very last line is added to prevent name mangling of define_fibonacci. We may now compile the file fibonnacci.cpp into a shared library libmmxfibonacci.so:
|
After putting the directory which contains libmmxfibonacci.so
in your LD_LIBRARY_PATH, you may now use the library
from within
| Mmx] | use "fibonacci" |
| Mmx] | fibonacci(37) |
To be completed.
It is possible to catch exceptions occurring in glued C++ routines
within the
In fact, it is better not to directly raise exceptions by yourself,
but rather use the convenience macros defined in basix.hpp.
Indeed,
Normal exceptions are the typical exceptions that you want to make
visible within the interpreter. Since the interpreter is much
slower than the
Low level exceptions are additional checks that you may wish to
add in critical parts of the
The two above types of exceptions both come with their corresponding macros. The following macros should be used for raising exceptions:
Raises the error message message.
Raises the error message message if the condition is not satisfied.
When compiling using –enable-verify, this macro raises the error message message if the condition is not satisfied.
Still to be written and documented.