ContRap
The symbolic tool for software rapid prototyping
Documentation
Developer level

ContRap already comes with a collection of different plugins. You should read the following section, if you want to add new plugins to ContRap.

Plugins

Plugin pattern essentially requires that the interface of a group of classes or methods is identical. With this approach it is possible to implement a highly flexible system with functionality, which can be added or removed at runtime. ContRap exports besides functions also native C/C++ data types.

Exported objects inside the user-mode of ContRap are in fact pointers to a C/C++ struct or a class instance. All exported data objects are wrapped by a weak dynamic typed smart pointer implemented by the SPtr and DPtr hierarchy. You can export any C/C++ data type in ContRap.

Functions are exported by wrapping the native C/C++ function call with a ContRap interface procedure. Each wrapper function has the same interface, which is given here for completeness reasons. You will never need to work with this interface, when you desgin ContRap plugins.

SO_EXPORT extern "C" crp::DPtr _name_hash_call(
  const crp::SPtr<crp::List>& _arguments, // List of function arguments
  const crp::SPtr<crp::Scope>& _scope, // Evaluation scope
  crp::Engine* _engine); // Current engine

The function you want to wrap can have nearly an arbitrary interface. Type conversion between the above general interface of the wrapper function to the real types of the wrapped C/C++ function is done automatically.

Classes are exported by exporting the class member functions and the class data type separately. You can export complete C++ classes or parts of class interfaces with ContRap. Separation of the data structure from the class interface allows the exported classes to be customized by functions at runtime. The added functions can even origin from a different C/C++ library.

To export functions and data types you write special C/C++ files, which are run thorough a meta-compiler to automatically create the wrapper functions for ContRap. The design of the C/C++ language extension follows the rule to minimize changes in an existing source to run with ContRap. It is usually only one macro keyword prefixed to a function declaration to turn it into a ContRap plugin. Data types are exported by declaring them in one additional line of code respectively.

Libraries

ContRap plugins are collected in dynamic libraries. One library can contain several plugins. It can contain also other functions and C/C++ classes. ContRap library names generally start with the prefix ''crp'' (e.g. ''libcrpio.so'' on Linux).

Before you can start developing plugins you have to create a ContRap library. The preferred way is to run the library assistant of the ContRap IDE. This assistant uses the command-line tool ''crpproject'' to create the library.

The syntax of the project generator is:

crpproject -p project_name [-n library_name] [-s source_path] [--svn] [--loud] package_name ...

Project name stands for the name of the CMake project. The library name is exact the name you will have to call later when you load the library. Per default the library name is equal to the project name. This source path specifies the location of the library to create. Per default the source path is the current path. With ''--svn'' you choose to integrate your project into an existing SVN hierarchy. In this case the CMake scripts are fetched per external properties of subversion. In the other case the scripts have to be checked out by calling svn update explicitly each time an update is required. The switch ''--loud'' turns more detailed verbose logging on. The remaining input is considered as a list of packages for custom CMake configuration.

A typical call to ''crpproject'' has the form:

crpproject -p MyPlugins --loud --svn -s /home/Me/ContRap -n myplugins qt qwt

With this command you create a CMake project ''MyPlugins'' in a library called ''crpmyplugins'', which uses the Qt- and the Qwt-Frameworks and is integrated into an existing subversion tree.

ContRap supports package configurations, which can be delivered with a custom library. The ''crpproject'' utility automatically creates the CMake scripts customized for your package configuration. To see what packages are installed on the system type

crpproject --list

You can read how to create a plugin library from scratch in the Appendix C.

Data type export

ContRap implements a flexible mechanism for working with native C/C++ objects. This includes for example garbage collection, implicit conversion, and automatic copy functions in container data types. To realize this ContRap must be supplied with a detailed type information for the exported objects at compile time. This is implemented by a ''meta-registration'' concept.

To declare a data type, which can be used in the user-level mode, you have to use in your library one of the macros in the objectfactory.h file.

The most common macro for objects is the ''CONTRAP_OBJECT'' macro. A typical registration call has the form:

CONTRAP_OBJECT(QImage,QPaintDevice,q_image)

The first argument is the exact type name. The second argument is the name of a base class in the class hierarchy. The last argument is a C/C++ valid unique identifier name for the type, which has technical reasons.

Function export

To export a function you simply write C/C++ code with the environment you prefer, e.g. with Eclipse. The only essential difference to your normal C/C++ code are the ContRap macros, which you write in front of functions, classes, or variables. The resulting file will be inspected by the meta-compiler of ContRap to create the neccessary ContRap-speciefic methods.

For C functions and for non-member functions you have to write ''CONTRAP_FUNCTION'' in front of the function

CONTRAP_FUNCTION String append(const String& a, String* b) {
  return a+*b;
}

Notice that if a parameter is not a pointer or a reference, the corresponding value will be copied on entrance or on return from the procedure respectively.

If you define an input parameter of type ''Scope'' the parameter will not appear in the parameter list of the exported function inside the ContRap user-level. Instead, you will receive in this parameter the current Scope of the interpreter. Analogously, if you define an input parameter of type ''Engine'' you will receive the Engine processing your code. This is the key feature to call ContRap code from C/C++ code.

CONTRAP_FUNCTION SPtr<Integer> add(const String& a, SPtr<Scope>& scope, Engine* engine) {
  DPtr parsed = engine->parse(a+"+"+a);
  DPtr result = engine->evaluate(parsed,scope);
  return engine->hard_cast<Integer>(result,"The result must be an integer",scope);
}

You can read more about how to call ContRap functions from your own code here.

Plugin documentation

Software is only as good as its documentation. This is in particular true for APIs and for plugin libraries, which are black-boxes for the user. The essential requirement to document the plugins is solved in ContRap by ''java-doc''-style meta-comments.

The following is a simple example of a documented plugin. See Appendix D for the list of all comment tags.

/**
 * This plugin adds two strings
 *
 * @export append
 * @param a Left string
 * @param b Right string
 * @default b ""
 * @returns A new string concatenated of the two input strings
 */
CONTRAP_FUNCTION String str_append(const String& a, const String& b) {
  return a+b;
}

Notice that the comments are also used as hints for the plugin generator. It is sometimes required that the exported C/C++ function has a different name in the user-level mode. This is the case when the function name is a constructor of a C++ class or a struct in the user-level mode. The ''export'' tag solves this problem by giving the exported function a different name than the real C/C++ function name.

You can define member functions for all exported C/C++ objects using the ''export'' tag. You simply write down the full function name including the base classes. For example, ''FILE:write'' exports (replaces) the ''write'' function of the exported data type ''FILE''. If there is at least one declaration which refers to a class data structure, ContRap will interpret the name left to the right-most '':'' as a class. This is done even if there is no such data type declared.

Calling ContRap functions from C++

The possibilty to pass the engine and the current scope to C/C++ methods is the key feature to call arbitrary ContRap functions from C/C++ code. What sounds at first glance like a feature gag is the basis for several essential techniques, like the fancy XML-printing engine.

You will need two objects to call the ContRap engine: A pointer to the currently used Engine class and a pointer to the Scope of the current evaluation. Both of them are simply obtained by specifying two corresponding ''phantom'' input parameters of your exported function called ''engine'' and ''scope'' respectively.

CONTRAP_FUNCTION void calling(SPtr<Scope>& scope, Engine* engine) { }

The engine contains a parser and an interpreter and provides a simple interface to use them via the parse() and evaluate() methods. The parser can be used to evaluate commands given as strings. The following code executes the user input command:

CONTRAP_FUNCTION DPtr calling(const String& command, SPtr<Scope>& scope, Engine* engine) {
  return engine->evaluate(engine->parse(command,scope),scope);
}

Remember to use smart pointers, if you want to use the scope to call a ContRap interface function, like the ''evaluate''-command of the Engine class. You can read about the reason for that in the smart pointer section or in the Appendix E.

A more practical application of calling the ContRap API from your own code is calling a user level function from C/C++. The function can be submitted as a Function object or simply as a DPtr.

CONTRAP_FUNCTION DPtr call(const DPtr& function, const DPtr& argument, SPtr<Scope>& scope, Engine* engine) {
  return engine->call(function,new List(argument), scope);
}

Within the user level of ContRap you then can write

f := function(x) x+1
call(f,1) // Results in 2

Reference counting

ContRap implements a non-intrusive reference counting garbage collection. In other words, the number of referrers to an object is counted in a special object, called the ''smart pointer''. Smart pointers are implemented in the weak typed class DPtr and its template-typed subclass SPtr. Both classes behave essentially like a regular data pointer. In addition the smart pointers count the number of objects which refer to the native data pointer. Internally ContRap consequently uses smart pointers instead of regular pointers.

Smart pointers require a little amount of care when programming C/C++ wrappers for your libraries. In short words,

what you should not do, is to create two different smart pointers, which refer to the same data!

Whenever the reference counter of one of the two pointers reaches zero, the data is deleted and the second pointer points to an illegal memory location causing a segmentation fault.

ContRap provides a means of debugging the smart pointer behaviour. Read more about how to enable smart pointer debugging in Appendix E.

Internally, each function, which can be called inside the user-level is wrapped by a plugin wrapper function. This function has smart pointers in its interface as well for all the inputs as for the output. Compare this in the export section. Your own ContRap procedures of course do not need to use smart pointers. You always have the choice between getting a smart pointer or a regular pointer in your input. Consider the following function, which uses smart pointers:

CONTRAP_FUNCTION SPtr<int> plus(const SPtr<int>& left, const SPtr<int> right) {
  return new int(*left + *right);
}

You can simply replace the code by the following procedure without smart pointers:

CONTRAP_FUNCTION int plus(int left, int right) {
  return left + right;
}

The conversion is automatically done by the meta compiler inside the wrapper function.

If you do not use smart pointers but regular pointers in your procedures, you have to follow some restrictive rules to write correct code.

  • Do not return input data or parts of it from your procedure.
  • Do not store or reuse input data in subsequent calls.
  • Do not reuse output data in subsequent calls.

If you write programs which always take the input data, process or modify it and return a completely new data as output, you will never need to think about the topics in the remainder of this section. One example of such a ''pointer-safe'' function is the following integer addition function:

CONTRAP_FUNCTION int* plus(int* left, int* right) {
  return new int(*left + *right);
}

You only need the methods below if you want to store or return inputs or their parts in your procedure.

If you ignore the above rules you easily write code which creates different smart pointers, which point to the same data or parts of it. Suppose, for example, you write the procedure

CONTRAP_FUNCTION int* identity(int* value) {
  return value;
}

There is nothing wrong about this procedure from the C/C++ point of view. ContRap, however, will likely produce a segmentation fault shortly after you use this function from the user-level. The reason for this is that the input parameter ''value'' is internally stored in a smart pointer and de-referenced (i.e. the native C/C++ pointer is extracted from it) to call your wrapper function ''identity''. When you return it as output, a new smart pointer is created by the wrapper procedure, since ContRap meta compiler does not know that the two pointers point to the same data at compile time. If you do not assign the output to a ContRap variable, the output smart pointer is deleted immediately after the function call, since it is unused. As the result of this action the reference counter of the output value goes to zero and the data of ''value'' is deleted. A subsequent reference to the contents of ''value'', which might be stored somewhere else, causes a segmentation fault.

If you want to return the inputs or their parts in the output, you have to use smart pointers. A correct ContRap code for the above critical example goes as follows.

CONTRAP_FUNCTION SPtr<int> identity(SPtr<int> value) {
  return value;
}

You can also use C++ references to the SPtr instances for efficiency reasons, since the wrapper procedure holds a copy of the smart pointer anyway due to inherent casting.

Whenever you need to return parts of the input objects data, you should use the source-dependent constructor SPtr class in return. Then the smart pointer holds a smart pointer to the reference object thus preventing ContRap to delete it.

CONTRAP_FUNCTION SPtr<char*> identity(SPtr<char*> value) {
  return SPtr<char*>(&value[10],value);
}

Controlling efficiency

Besides their function for simple garbage collection smart pointers play also an essential role in writing memory-efficient ContRap wrappers.

Smart pointers provide the possibilty to determine the number of referrers to the current object in constant time. With that, at runtime it is possible to answer the question, whether the current context is the only referrer to an object. If yes, the memory of the object can be reused. This is in particular essential when writing programs working on big amounts of data, like for example on digital images.

We demonstrate this on a simple example of integer addition. Of course, the efficiency benefit of reusing small amount of bytes, like it is in the case of Integers, is small or even negligible. But the principal approach does not change for other data types, where the benefit might be essential for a program to work.

CONTRAP_FUNCTION Integer* plus(Integer* left, Integer* right) {
  return new Integer(*left + *right);
}

This is a naive way to implement integer addition, where the arguments are supplied as pointers and a new Integer instance is created each time the function is called. Notice that by passing pointers in the arguments allow classes derived from the ''Integer'' class to be passed to the above procedure.

The basic idea of efficient memory management with smart pointers is to hold a local static pointer to the output of the procedure and reuse it each time the pointer is not referenced.

CONTRAP_FUNCTION SPtr<Integer>& plus(Integer* left, Integer* right) {
  static SPtr<Integer> result = new Integer(0);
  if (result.get_rc() < 2)
    *result = *left + *right;
  else
    result = new Integer(*left + *right);
  return result;
}

Notice first of all that the static pointer prevents the object behind the ''result'' from being deleted by the garbage collector. If the number of referrers is not more than one the only reference is our static variable ''result'' and we can reuse the data of the smart pointer. If not, we have to create a new integer instance.

Notice that for efficiency reasons we return a reference to the smart pointer. You can also return a pure smart pointer, which results in one call to the copy constructor and one additional destruction of the pointer. Incorrect handling with references to smart pointers should likely be noticed by the compiler.

Also notice that the above implementation of the addition is not thread-safe. You will have to add synchronization techniques when implementing thread safe wrappers.

If you are not sure whether to use or not a reference to a smart pointer, return a pure smart pointer.