1. Concepts
1.1 Object
The basic components of the system are the Objects. They are implemented as Qt plugins (shared libraries) that can be dynamically loaded during runtime. Qt provides the ability of getting the root interface of a plugin and performing dynamic casts on the object. There can be only one instance of any plugin object in the system because of the way Qt handles it. If multiple instances are need, a factory plugin object must be defined. The factory object can provide a public slot to create and return an object. The returned object is managed by the Application Context and delete when the application exits.
1.2 Application Context
Application Context manages the life cycle all the objects in the system. It loads the objects, sets the properties of the objects, calls init method on the objects if they have one and wire the objects together. When the application exits, it calls destroy method if there is one and the init method was called, then deletes the factory created objects and unload the plugins.
1.3 Configuration File
Configuration files are XML files that specify the objects and their relationships. There can be multiple configuration files for each application.
2. Programming Interface
2.1 Environment Variables
QIC_DIR must be defined to point to the root directory of the framework. For example, if the package is extracted in home directory, QIC_DIR must be set to $(HOME)/qtioccontainer.
2.2 Using Application Context
ApplicationContext is created by compiling a list of configuration files and pass them to the constructor. After the ApplicationContext object is successfully constructed, the objects are ready to use. They can be retrieved using getObject method.
2.3 Threading
XmlApplicationContext is not thread safe. So different threads cannot share same instance without locking.
3. Configuration File
Right now, no DTD validation or XML schema is used because Qt XML module does not support them.
Each configuration files starts with <objects> as the root tag. All the definitions come inside the objects tag.
3.1 Plugin Object
Objects in the system are specified as "object" tags. Every object must have an ID. The ID is used by the ApplicationContext to distinguish different objects and to retrieve objects. For plugin objects, the path to its library file must be provided. Note that each plugin library only provide a single instance in the system.
<objects>
<object id="rectangle"
plugin="${QIC_DIR}/test/test_plugin/rectangle_plugin/librectangle_plugin.so"/>
</objects>
3.2 Factory Created Object
Factory is used when more than one instances of the same object are needed. In this case, the factory itself has only one instance. But it can create and return multiple objects. Notice that in the following configuration file, "star1" and "star2" are also singletons.
<objects>
<object id="star1" factory-object="shapeFactory" factory-method="createStar">
...
</object>
<object id="star2" factory-object="shapeFactory" factory-method="createStar">
...
</object>
</objects>
To define factory created objects, "factory-object" attribute specifies the factory object id, "factory-method" attribute specifies the method for creating objects. This method must be defined as Qt slot and looks like
QVariant createStar ()
{
QObject * obj = new Star();
return QVariant::fromValue<QObject *>(obj);
}
Notice that the QObject * should be returned as QVariant. When used in client, the return value needs to be convert back to QObject * using
QVariant::value<QObject *>()
3.3 Value Properties
Object can define properties using Qt's property system. If a property is defined for an object, its value can be specified in the configuration file using property tag. The name of the property is specified as an attribute. It's value is specified in value tag. When supported by QVariant, Qt will automatically convert the string to proper type.
<object ...>
<property name="size">
<value>100</value>
</property>
</object>
When no string is provided in the value tag, it is assume to be empty string. If NULL is desired, use "null" tag. In this case, the QVariant value contains a null string.
<object ...>
<property name="address">
<value></value>
</property>
</object>
<object ...>
<property name="address">
<value><null/></value>
</property>
</object>
In addition to "value" and "null" tag, the system also supports "list" and "map" tag.
<object ...>
<property name="email_list">
<list>
<value>a@qic.org</value>
<value>b@qic.org</value>
</list>
</property>
<property name="accounts">
<map>
<entry>
<key>one</key>
<value>1.0</value>
</entry>
<entry>
<key>two</key>
<value>2.0</value>
</entry>
</map>
</property>
</object>
The sub tag of a map entry, or a list, can also again be any of the following tags:
value | list | map | null | ref
3.4 Reference Properties
Properties can refer to other objects in the system using "ref" attribute of the property tag.
<object id="rectangle" ...>
...
</object>
<object id="canvas" ...>
<property name="rectangleObj" ref="rectangle">
</property>
</object>
In this case, the QObject * has to be wrapped in QVariant. The following shows how the property is defined in canvas object.
...
Q_PROPERTY(QVariant rectangleObj READ rectangleObj WRITE setRectangleObj)
...
QVariant rectangleObj ()
{
return QVariant::fromValue(_rectangleObj);
}
void setRectangleObj (QVariant obj)
{
_rectangleObj = obj.value<QObject *>();
}
...
QObject * _rectangleObj;
When an object is referenced this way, it will be created and initialized before the object refering it.
3.5 Weak Reference
Sometimes objects need to refer to each other. However, this leads to circular dependencies. For example, when loading the following configuration file, CyclicDependencyException will be thrown. Because each object depends on the other to finish its construction. None could be successfully constructed.
<objects>
<object id="rectangle"
plugin="${QIC_DIR}/test/test_plugin/rectangle_plugin/librectangle_plugin.so">
<property name="canvasObj" ref="canvas">
</property>
</object>
<object id="canvas"
plugin="${QIC_DIR}/test/test_plugin/canvas_plugin/libcanvas_plugin.so">
<property name="rectangleObj" ref="rectangle">
</property>
</object>
</objects>
If one object does not require the other to be fully constructed to construct itself, the objects could somehow be loaded successfully in theory. For instance, if rectangle only uses its reference to canvas object to get runtime information and does not use it during its own construction, we could specify this fact by indicating that this reference is "weak", meaning that when the reference is being resolved, it points to the canvas object, but the canvas object may not be in ready state. As shown in the following code, when this extra piece of information is provided, the configuration file could be loaded successfully.
<objects>
<object id="rectangle"
plugin="${QIC_DIR}/test/test_plugin/rectangle_plugin/librectangle_plugin.so">
<property name="canvasObj" >
<ref object="canvas" weak="yes"/>
</property>
</object>
<object id="canvas"
plugin="${QIC_DIR}/test/test_plugin/canvas_plugin/libcanvas_plugin.so">
<property name="rectangleObj" ref="rectangle">
</property>
</object>
</objects>
When weak attribute is not present or when the reference is specified using the format in section 3.4, the reference is assume to be "strong".
3.6 References in List and Map Tag
In addition to standalone reference properties, references could also be embedded in other properties as long as the object knows how to inteprete them.
<object id="canvas"
plugin="${QIC_DIR}/test/test_plugin/canvas_plugin/libcanvas_plugin.so">
<property name="listObj">
<list>
<value>Item 1</value>
<value><null/></value>
<ref object="rectangle" weak="false"></ref>
<map>
<entry>
<key>Rectangle</key>
<ref object="rectangle"></ref>
</entry>
</map>
</list>
</property>
</object>
3.7 Init and Destroy Methods
Objects can define init-method or destroy-method that the system calls after loading or before unloading it respectively. The init-method and destroy-method have to be slots to be callable.
<object id="canvas" plugin="..." init-method="init" destroy-method="destroy" />
3.8 Wiring Objects using Qt's Signal and Slot Mechanism
The wiring of objects does not have to hard coded. They can be placed in configuration files so that they can be changed without recompiling. Thus, new components can be plugged in, existing behavior can be changed dynamically. And the only thing that needs to be changed is the configuration file.
The wiring components are specified using wire tag. The type of the wire can be auto, direct or queued. See Qt documentation for their explanation. The sender tag specifies the sender object id and the Qt signal signature. The receiver specifies that receiver object id and the receiving method. The receiving method can be either signal or slot. See Qt documentation for how to provide the method and signal signatures.
<objects>
<object id="rectangle" plugin="...">
...
</object>
<object id="canvas" plugin="...">
...
</object>
<wire type="auto">
<sender id="canvas" signal="sizeChanged(int)"> </sender>
<receiver id="rectangle" method="setCanvasSize(int)"> </receiver>
</wire>
<wire>
...
</wire>
</objects>
3.9 Variable Substitution
Environment variables or QSetting variables can be used for plugin path or property values in configuration files. For example, ${QIC_DIR} will be replaced by the actual value of the environment variable QIC_DIR.
3.10 Object Life Cycle
The following is the life cycle of the objects in the system.
-
1. The plugin for the object is loaded. If the object is a factory created object, the factory is loaded and initialized first.
-
2. Properties are set. If the object refer to other objects, those objects are loaded and initialized first.
-
3. Init method is called if there is one.
-
Here, the object is used.
-
4. Destroy method is called if there is one.
-
5. The plugin is unloaded.
If cyclic dependcies are detected, CyclicDependencyException will be thrown.
4. Writing Plugins
For detailed description, see Qt documents. Please also check out the plugins in test/test_plugin directory.
Here's some hints.
-
Remember to include QtPlugin in the cpp file. Or there will be strange error messages.
-
Remember to implement all functions. Once, I forgot to implement the virtual destructor. The plugin compiled OK. But it failed to load.
-
Plugins do not have to implement Interfaces. When they do, the interface they implement must be declared using Q_INTERFACES macro.
5. Extending QtIocContainer
5.1 Custom Parsers
Parsers are used in QtIOCContainer to parse XML fragments to property values. By default, parsers handling value, null, list, map, ref tag are included. To create additional custom parsers, plugins implementing PropertyParser interface must be defined. The parser should recognize certain XML tag and return the QVariant value repesenting the information specified in the tag. After the plugin is ready, it can be used by changing your configuration files. Multiple custom parsers can be defined and used at the same time.
<objects>
<parser tag="cumstom_tag_name" plugin="/absolute/path/to/your/parser" />
<parser ... />
<object>
...
</object>
<wire>
...
</wire>
...
</objects>
The buildin parsers are shown below.
<objects>
<parser tag="value" plugin="${QIC_DIR}/lib/libvalue_parser.so"/>
<parser tag="list" plugin="${QIC_DIR}/lib/liblist_parser.so"/>
<parser tag="map" plugin="${QIC_DIR}/lib/libmap_parser.so"/>
<parser tag="null" plugin="${QIC_DIR}/lib/libnull_parser.so"/>
<parser tag="ref" plugin="${QIC_DIR}/lib/libref_parser.so"/>
</objects>
5.2 Custom Injectors
QtIocContainer uses custom injectors to inject object references in property values. They replaces ReferenceDefinition structures by the actual Object * pointers wrapped in QVariant. The buildin injectors are listed below.
<objects>
<injector type="qic::ReferenceDefinition" plugin="${QIC_DIR}/lib/libref_injector.so"/>
<injector type="QVariantList" plugin="${QIC_DIR}/lib/liblist_injector.so"/>
<injector type="QVariantMap" plugin="${QIC_DIR}/lib/libmap_injector.so"/>
</objects>
Zhihong "John" Wang
(zhihong_w@yahoo.com).