Introspection

SAMSON Elements expose their functionality to SAMSON and to other SAMSON Elements thanks to an introspection mechanism. Exposition is achieved thanks to descriptor files that contain the list of exposed classes and functions, and that are compiled along with the SAMSON Element. One of the major benefits of the introspection mechanism is the ability to share and combine functionality without the need for providing any type of source code or header code. It is thanks to this introspection mechanism that SAMSON Elements form more than a collection of independent modules.

Exposing the functionality of a SAMSON Element typically involves two main steps:

  • Writing one descriptor per class that should be exposed
  • Writing one descriptor for the SAMSON Element

The SAMSON Element generator automatically generates descriptor files that expose the default functionality of the classes it generates. These descriptors may be modified as indicated below in order to expose custom functionality added to the generated classes.

Exposing classes

Registering classes for exposition

Exposing a class to SAMSON first involves three steps:

The following App header code, generated by the SAMSON Element generator (in a file called SEElementMyApp.hpp), demonstrates these three steps:

class SEElementMyApp : public SBDApp {
public :
/// \name Constructors and destructors
//@{
SEElementMyApp(); ///< Constructs an app
virtual ~SEElementMyApp(); ///< Destructs the app
//@}
/// \name GUI
//@{
SEElementMyAppGUI* getGUI() const; ///< Returns a pointer to the app GUI
//@}
};
SB_REGISTER_TARGET_TYPE(SEElementMyApp, "SEElementMyApp",
"D5A1BA95-9CD4-3424-9713-06B979F5C0D9");
SB_DECLARE_BASE_TYPE(SEElementMyApp, SBDApp);

The macros should be added in specific positions:

The SB_REGISTER_TARGET_TYPE macro has the following syntax:

  • The first parameter (without quotes) is the type that should be registered
  • The second parameter (in quotes) is the short name of the class. This should be a single word with a syntax typical of class names. If a short name is not needed (e.g. because the original class name is short enough), then the original class name should be used.
  • The third parameter (in quotes) is the class' unique identifier, which could be generated thanks to the provided UUID Generator.

The SB_DECLARE_BASE_TYPE macro has the following syntax:

  • The first parameter is the derived type.
  • The second parameter is the base type. This class must be exposed as well (this is the case for base classes provided in the SAMSON SDK, such as the SBDApp class in the example above).

Note that, at the moment, SAMSON's introspection mechanism may not expose multiple inheritance relationships: while it is perfectly possible to expose a class that inherits multiple classes, the SB_DECLARE_BASE_TYPE macro may be used to expose only one derivation relationship, with only one of the base classes.

Writing class descriptors

Once a class is registered and its potential derivation relationship is declared using the macros described in the previous section, a corresponding class descriptor may be written.

Descriptor structure

A class descriptor implements a proxy for the class, and contains five main descriptions:

  • A type description
  • A class description
  • A class version number
  • A factory description, i.e. a list of constructors descriptions
  • An interface description, i.e. a list of member functions descriptions

Descriptions are written using macros provided by the SAMSON SDK. In particular, a class descriptor is enclosed between two macros, SB_CLASS_BEGIN and SB_CLASS_END, that take as argument the class name (e.g. SB_CLASS_BEGIN(SEElementMyApp) and SB_CLASS_END(SEElementMyApp) for the example above).

The example below shows a descriptor generated by the SAMSON Element generator (in a file called SEElementMyAppDescriptor.hpp) for the App class it has generated:

SB_CLASS_BEGIN(SEElementMyApp);
SB_CLASS_DESCRIPTION("SAMSON Element generator pro tip: modify me");
SB_CONSTRUCTOR_0(SEElementMyApp);
SB_CLASS_END(SEElementMyApp);

Type description

The class type is added to the class descriptor thanks to the SB_CLASS_TYPE macro. The argument of the macro should be one of the possible class types.

Class description

The class description is added to the class descriptor thanks to the SB_CLASS_DESCRIPTION macro. The argument of the macro should be a short string.

Class version number

The class version number is added to the class descriptor thanks to the SB_CLASS_VERSION_NUMBER macro. The argument of the macro should be a string corresponding to a three-part version number: "major.minor.patch", which should be increased in particular when serialization of the class changes.

Factory description

The class factory, i.e. the collection of class constructors, is added to the class descriptor between the SB_FACTORY_BEGIN and SB_FACTORY_END macros:

Each constructor should be added using one of the seventeen provided macros SB_CONSTRUCTOR_0, SB_CONSTRUCTOR_1, ... SB_CONSTRUCTOR_16, where the integer indicates the number of arguments taken by the constructor.

For example, the class constructor

SEElementMyApp(int i, double d, const SBAtom* atom);

may be exposed with the following macro:

SB_CONSTRUCTOR_3(SEElementMyApp, int, double, const SBAtom*);

If multiple constructors are exposed, they are all described in a unique pair of enclosing macros SB_FACTORY_BEGIN and SB_FACTORY_END, e.g.:

SB_CONSTRUCTOR_0(SEElementMyApp);
SB_CONSTRUCTOR_1(SEElementMyApp, const std::string&);
SB_CONSTRUCTOR_1(SEElementMyApp, int);

Note that one constructor that takes no argument should always exist and be exposed.

Interface description

The class interface, i.e. the collection of class functions, is added to the class descriptor between the SB_INTERFACE_BEGIN and SB_INTERFACE_END macros.

For example, the member function

void setName(const std::string&);

may be exposed with the following macro inserted between the SB_INTERFACE_BEGIN and SB_INTERFACE_END macros:

SB_FUNCTION_1(void, SEElementMyApp, setName, const std::string&);

If a function is const:

std::string getName() const;

then it may be exposed with a macro for const functions:

SB_CONST_FUNCTION_0(std::string, SEElementMyApp, getName);

Finally, if a function is static:

static unsigned int getNumberOfInstances();

then it may be exposed with a macro for static functions:

SB_STATIC_FUNCTION_0(unsigned int, SEElementMyApp, getNumberOfInstances);

In general, each function should be added using one of the fifty-one provided macros:

If multiple functions are exposed, they are all described in a unique pair of enclosing macros SB_INTERFACE_BEGIN and SB_INTERFACE_END, e.g.:

SB_CONST_FUNCTION_0(std::string, SEElementMyApp, getName);
SB_FUNCTION_1(void, SEElementMyApp, setName, const std::string&);
SB_CONST_FUNCTION_2(SBQuantity::length, SEElementMyApp, getDistance,
const SBAtom*, const SBAtom*);
SB_STATIC_FUNCTION_0(int, SEElementMyApp, getNumberOfInstances);

Note that even when no functions are exposed, the class descriptor should always contain a class interface:

Finally, note that virtual functions that are part of the API of SAMSON (i.e. are member functions of classes defined in the SAMSON SDK), and that are reimplemented in derived classes in SAMSON Elements, do not have to be exposed: since the function is defined in the SAMSON SDK, developers may write code that calls the function without relying on the introspection mechanism, and the appropriate implementation will be called at runtime.

Exposing a SAMSON Element

Once descriptors have been written for all classes that should be exposed, a descriptor may be written for the SAMSON Element itself.

Descriptor structure

A SAMSON Element descriptor contains five parts:

  • A description
  • A documentation address
  • A version number
  • A list of exposed classes
  • A list of categories

Again, descriptions are written using macros provided by the SAMSON SDK. Typically, this descriptor is generator by the SAMSON Element generator, and needs to be completed when more classes are added to the SAMSON Element:

SB_ELEMENT_DESCRIPTION("This SAMSON Element contains an App that shakes atoms.");
SB_ELEMENT_DOCUMENTATION("Resource/Documentation/doc.html");
SB_ELEMENT_CLASS(SEElementMyApp);
SB_ELEMENT_CATEGORY(SBClass::Category::General);

Description

The description is added to the SAMSON Element descriptor thanks to the SB_ELEMENT_DESCRIPTION macro. The argument of the macro should be a short string.

Documentation address

The address of the SAMSON Element documentation is added to the SAMSON Element descriptor thanks to the SB_ELEMENT_DOCUMENTATION macro. The argument of the macro should be a short string containing the address relative to the installation folder of the SAMSON Element (typically in the generated Resource/Documentation/ folder).

Version number

The version number of the SAMSON Element is added to the SAMSON Element descriptor thanks to the SB_ELEMENT_VERSION_NUMBER macro. The argument of the macro should be a short string containing a well-formated version number. Note that the version number of the SAMSON Element is completely independent of the version number of the SAMSON SDK used to build the SAMSON Element. Please refer to the chapter about versioning for more information.

List of exposed classes

The list of exposed classes is added to the SAMSON Element descriptor between the SB_ELEMENT_CLASSES_BEGIN and SB_ELEMENT_CLASSES_END macros:

Each exposed class (i.e. each class which has a descriptor) should be added using the SB_ELEMENT_CLASS macro.

If multiple classes are exposed, they are all described in a unique pair of enclosing macros SB_ELEMENT_CLASSES_BEGIN and SB_ELEMENT_CLASSES_END, e.g.:

SB_ELEMENT_CLASS(SEElementMyApp);
SB_ELEMENT_CLASS(SEElementMyVisualModel);
SB_ELEMENT_CLASS(SEElementMyFirstInteractionModel);
SB_ELEMENT_CLASS(SEElementMySecondInteractionModel);

List of categories

The list of SAMSON Element categories is added to the SAMSON Element descriptor between the SB_ELEMENT_CATEGORIES_BEGIN and SB_ELEMENT_CATEGORIES_END macros:

Each category should be added using the SB_ELEMENT_CATEGORY macro, which takes as argument a category.

If the SAMSON Element should belong to multiple categories, they are all listed in a unique pair of enclosing macros SB_ELEMENT_CATEGORIES_BEGIN and SB_ELEMENT_CATEGORIES_END, e.g.:

SB_ELEMENT_CATEGORY(SBClass::Category::General);
SB_ELEMENT_CATEGORY(SBClass::Category::Medicine);
SB_ELEMENT_CATEGORY(SBClass::Category::Biology);

Using exposed classes

Once a class is exposed, SAMSON and other SAMSON Elements may create instances of it and call its member functions. This section explains how.

Class proxies

The functionality of an exposed class is accessible through its class proxy, which gathers the class factory (its collection of constructors) and the class interface (its collection of member functions).

The class proxy of an exposed class is retrieved using SAMSON:

SBProxy* classProxy = SAMSON::getProxy("SEElementMyApp");

In case there is a risk that several SAMSON Elements might provide a class called SEElementMyApp, it is possible to specify the name and the UUID of the SAMSON Element:

SBProxy* classProxy = SAMSON::getProxy("SEElementMyApp", SBUUID("C8EC88EA-38CE-70F4-2A74-C71F2C86A692"));

The SAMSON Element UUID is guaranteed to be unique if the SAMSON Element has been installed from SAMSON Connect.

Constructing class instances

The creation of a class instance depends on the type of the exposed class.

Special classes

For exposed classes that derive from some classes from the SDK, SAMSON provides convenience factory functions to create instances.

Assume for example a SAMSON Element provides a particle interaction model:

class InteractionModel : public class SBMInteractionModelParticleSystem {
InteractionModel(SBMDyamicalModelParticleSystem* dynamicalModel);
~InteractionModel();
...
virtual void updateInteractions();
...
};

Then, an instance of this class can be created as follows:

SAMSON::makeInteractionModel(dynamicalModel, "InteractionModel");

where dynamicalModel is the argument that would be passed to the constructor of the interaction model.

If only the reimplemented virtual functions of the interaction model should be used (e.g. updateInteractions), then the interactionModel pointer is sufficient, even though interactionModel is a pointer to a SBMInteractionModelParticleSystem and not a pointer to an InteractionModel.

General case

In case the exposed class does not derive from a type for which SAMSON offers convenience functions, such as the makeInteractionModel function mentioned in the previous section, a class instance may be directly created from a class proxy.

Assume for example a SAMSON Element contains the following class:

class A {
public:
A() {}
virtual ~A() {}
};
SB_REGISTER_TYPE(A, "A", "BF99103E-06FE-C4C1-D929-4C6E833B101C");

as well as the following class descriptor:

Then SAMSON may be used to retrieve a proxy to the class A:

SBProxy* classProxy = SAMSON::getProxy("A");

which may then be used to create an instance of class A:

SBValue* objectHolder = classProxy->createInstance();

It must be noted that, in this case, the pointer to the object of class A return by the proxy is held in a value holder.

Assume a SAMSON Element called SEElement defines a class A, and another SAMSON Element called SEOtherElement wants to use it. If the SAMSON Element called SEOtherElement does not have access to the declaration of class A (typically provided in a header file included in SEElement), it cannot use the symbol A in its source code. For example, if the source code of SEOtherElement contained the line:

A* a = new A();

the compiler would complain that the symbol A is not defined.

In order to provide access to exposed classes, SAMSON uses a mechanism to hold values (for example, pointers to objects) in a uniform way. In the example above, the constructor exposed in the SAMSON Element SEElement returns a value holder that stores a pointer to the new instance of class A:

return new SBValueHolder<A*>(new A());

and the SAMSON Element SEOtherElement sees this value holder as a pointer to a SBValue (the base class of SBValueHolder<A*>):

SBValue* objectHolder = classProxy->createInstance();

Calling functions

Assume a SAMSON Element contains the following class:

class A {
public:
A() {}
virtual ~A() {}
int multiplyByTwo(int i) {
return 2 * i;
}
};
SB_REGISTER_TYPE(A, "A", "BF99103E-06FE-C4C1-D929-4C6E833B101C");

as well as the following class descriptor:

In this example, the function multiplyByTwo may be called by other SAMSON Elements. Since the function is not static, we must first construct an instance of class A:

SBProxy* classProxy = SAMSON::getProxy("A");
SBValue* objectHolder = classProxy->createInstance();

We may then call the function:

SBValue* argumentHolder = new SBValueHolder<int>(17);
SBValue* resultHolder = classProxy->call(objectHolder, "multiplyByTwo", argument);
int result = static_cast<SBValueHolder<int>*>(resultHolder)->getValue();
delete argumentHolder;
delete resultHolder;

Here, also, we use value holders in order to wrap arguments provided to the called function. In this example, we are able to unwrap the int held in the value holder with:

int result = static_cast<SBValueHolder<int>*>(resultHolder)->getValue();

because int is a standard C++ type that all SAMSON Elements may use. In case the return value of the exposed function was not of a type known by the SAMSON Element that calls the function, unwrapping would not be possible, but the functionality of the return type may still be accessible through its proxy, and the returned value holder may still be passed as argument to other exposed functions.

Remark: if an exposed function is static, no class instance is required, and the pointer passed to the call function can be set to 0.

Optimizations

When multiple instances of the same class are created, it is more efficient to store a pointer to a class constructor:

SBProxy* classProxy = SAMSON::getProxy("A");
SBFactory* classFactory = classProxy->getFactory();
SBConstructor* classConstructor = classFactory->getConstructor();

which we may then use to construct an instance of class A:

SBValue* objectHolder = classConstructor->createInstance();

Similarly, when multiple calls to the same function are performed, it is more efficient to store a pointer to a class function and reuse it:

SBProxy* classProxy = SAMSON::getProxy("A");
SBInterface* classInterface = classProxy->getInterface();
SBFunction* classFunction = classInterface->getFunction("multiplyByTwo", "int");

We may then use the exposed function by wrapping arguments and unwrapping results:

SBValue* objectHolder = classProxy->createInstance();
SBValue* argumentHolder = new SBValueHolder<int>(17);
SBValue* resultHolder = classFunction->call(objectHolder, argumentHolder);
int result = static_cast<SBValueHolder<int>*>(resultHolder)->getValue();
delete argumentHolder;
delete resultHolder;