Premiers pas avec Ravi
Le shell ravi
Le module trace
Les ports d'E/S
The C Parser
load, require, modules
Generating C++ Modules
La déclaration struct
Le type "C-object"
Generating C++ Modules
How can we add extend the Ravi shell with C or C++ code? In principle, this is very simple: we just want to call some existing code from the shell. In practice, however, we need quite a bit of so-called "wrapper code" or "glue code" which must take care of several points. Let us illustrate this with an example from the next paragraph. Suppose we have written and compiled a function called ilog2,and we want to be able to use this function from the shell, writing, for example ilog2(x+123).The following points have to be solved:
The fundamental ambition of RIG is "carefully look at the source code, don't ask the user to repeat information in the source code". This translates into "generate the interface code from the header files of user programs".
We illustrate the preceding discussion with a very simple example: a single function named ilog2 written in C. Our code is composed of a header file essai1.h and an implementation file essai1.cc.
The header file simply reads as follows:
What the user has to do: module generation
We suppose that the implementation file essai1.cc has been compiled to produce an object file essai1.o. The user now calls RIG to generate the wrapper file modessai1.cc; this file has to be compiled and link-edited with essai1.o.
These steps are carried out using the special command ravitoolin a fairly intuitive way:
The first of these commands generates a source file - the "module file" modessai1.cc which is then compiled and link-edited to produce the file modessai1.so. These commands are emitted via the command ravitool which in turn calls the C++ compiler with the appropriate options.
The result of these three commands is the file modessai1.so which is a shared object file that can be dynamically loaded into the Ravi interpreter.
Once this has been done, the module modessai1can be used in the Ravi shell.
Using the module in the shell
The module can now be used in the shell. We show this with C-syntax, and with Scheme syntax.
Loading and Use in the shell (C-syntax)
Use in the shell (Scheme-syntax)In Scheme mode, the commands of the preceding paragraph are written as follows:
Generated fileAs can be seen in this example, the classical compile-link cycle is replaced here by a cycle generate-compile-link. It is useful to have some understanding of the generated file, to understand the difficulties to come.
Remember that the source file defines one function with the header:
The generated file contains a wrapper function named Sc_ilog2(). Looking at the code, you can easily get a feeling how this function works: it fetches the arguments from the stack of Ravi's virtual machine with VS_ElemQ(- GetPar(), the argument is checked to be an integer, the function ilog2is called with the argument value, the result is pushed on the virtual machine stack.
At the end of the generated file, there is an initialisation function that puts the function name and address into Ravi's symbol table:
What can be learnt from this example
The above procedure generate-compile-link-load works for C and C++ code, in the case where no particular difficulties arise.
However, when you look carefully at existing C or C++ programs, you will realize that the interface generator's fundamental ambition - to use all information in the source code - is tough to carry through. Even worse: sometimes, important information is lacking in C++ code. And in some cases, we want the interface to do more than just call C-code (for instance with memory management).
As a consequence, some information has to be given by the user for interface generation. These are called "declarations", and usually put into a file with extension ".dcl"
Note that the implementation code of the program to be interfaced is not needed for module generation - we only need header files and compiled code, so we can do without the source files (.cc files).
Let's take the following example: I have a library $HOME/Soft/lib/libpyramid.so and the associated header files BinomialDogPyramid.h LaplacianPyramid.h DogPyramid.h in the directory $HOME/Soft/include.
Look carefully at the commands to generate such a module - they resemble closely the first example!
Comments: The ravitool --generate command has a parameter -project Imalab which specifies that the pyramid code uses data structures from the Imalab project; this introduces a large number of "declarations" that will be explained in the following paragraph on advanced topics. (Through the file Site-Module/Imalab/project.dcl).
For other libraries, of course, you will not need the -project Imalab parameter - but chances are that you need to give some special information that the interface generator cannot deduce frome the header files.
The first step in module generation is automatic program generation using the command ravitool --generate.
General format as Unix command:
ravitool --generate options source_files
This is strictly equivalent to
ravi -mod r-ig options source_files
source-files either is a (possibly empty) list of header files, or a single .ph file. If the list is empty, then the source files must be listed in the dcl-file. .ph files are explained in a later paragraph
Options are the following:
Preprocessor defaults: When the source file(s) are header files, the preprocessor is active. When the source file is a .ph file, the preprocessor is not active.
The preprocessor symbol RAVI_INTERFACE always is defined during module generation.
In the absence of declarations, RIG generates the default interface for the header files given as arguments. The default interface contains
If we want to influence the default behavior, and for many more reasons, we write declarations for the interface generator. Declarations are some kind of pragma. Declarations have the general aspect of data-in-parentheses (they are analyzed as Scheme lists). (Current work: object-attribute-value format).
Declarations can be put in two places: in a special file, the dcl-file (given with a -u argument to the ravitool command), or they may be placed inside C-comments in the source code. Such a C-comment must have the form /*Ravi declare ( declaration ...) */The C-comment may spread over several lines; the list of declarations is read with a single call to the Scheme function read.
Using declarations, you can easily augment the interface, (e.g. to include template classses, or constants), or modify the default settings, (e.g. to exclude an item, or to add methods of a struct),
There is a separate paragraph on each of the declarations for class, struct, template.
Format: (class id attr ...)
The class declarations specifies diverse informations about the interface for a class using an (attribute value) notation. An attribute name without value specifies the default value (whenever this makes sense).
The class-name id can be written as a symbol, when it is a simple class name, or as a string, when it is a template class. In the latter case, the name of the template must have been declared in a preceding declaration : (template name).
The attributes for the class declaration are:
The default values for class attributes are defined by the internal RIG-variable *class-defaults*. The default setting says that you want to export all methods of the class, and that a class usually is not abstract:
If you want to have get-functions or set-functions for some or all fields of the class, or if you want to exclude certain methods in the interface, you specify this with attributes in the class declaration.
You may also change the default setting for all class definitions in the interface - see the example in the following parapraph about the struct declaration.
Useful values for this attribute are:
The attribute export-fields is analogous to the attribute export-methods, but it gives you finer control about the generated code: you not only select the fields, but also the kind of function that is generated: read-access, write-access, or both. Accordingly, the values of this attribute may be:
Useful ways to write this attribute in a class or struct declaration are:
Default value for class fields: #f.
Print functions: attributes print-function, print-method
In the Ravi system, any C-type has a print function, which can be defined via attributes for the C++ class (or for the C-type). If you don't specify one of these attributes, then an object of the class will be printed as a hexadecimal pointer value.
Example of such a generated print function:
The struct declaration follows exactly the same syntaxe as the class declaration; however, the default settings are different, given by the internal RIG-variable *struct-defaults*:
These default values correspond to the situation you find in C, where a struct just is a collection of records. In C++ programming, very often a struct is used exactly the same way as a class - in that case you probably want to modify this setting, using the following declaration:
In the default case, access functions are generated for the fields of the struct. In the shell, these functions are named: structname-fieldname set-structname-fieldname!
The attribute is-class says that the struct is to be handled as a class, i.e. that access functions are generated as methods.
This attribute is written as:
Default value: #f.
The attribute make-constructor specifies that a constructor without arguments should be generated. This is useful when there is no constructor for this struct.
This is particularly useful for callback functions in graphical interfaces.
A C++ template is a code pattern (for a class, or for a procedure), and cannot be interfaced directly (maybe a template could be translated into a higher-order Scheme function?). However, there is no problem in interfacing templates instances. All the principles explained so far apply to template instances.
There is just one important question: which instances do we interface? And a less important question: what names do we give to these in Scheme? The rules are as follows:
Remarks - current limitations
miscellaneous information: names
The RIG preprocessor usually is active, but one must be aware that it is NOT IDENTICAL with the compiler preprocessor! In particular:
Declarations concerning the preprocessor:
Sometimes we want to influence the interface generator more than the declarations allow - in particular for troubleshooting and debugging. For these cases, the eval declaration is the universal solution: it executes Scheme code directly; of course, one has to know about RIG's internals.
(eval e) evaluates the expression e. This allows to set internal variables, to output debug information, etc. If it turns out that a variable often is used this way, then a new kind of declaration is set up - this is the way declarations evolve.