Home | All Classes | Main Classes | Annotated | Grouped Classes | Functions | ![]() |
Qt's Object Model provides very powerful extensions to the C++ Object Model and makes it natural to write components that are highly encapsulated and easily reused.
The Qt Component Model implements the Universal Component Model (UCOM) and extends the aspects of encapsulation and reusability. As the combination of C++ and the Qt Object Model helps to break an application source code into reusable and specialized classes, the Component Model helps to break a monolithic binary into components that can be dynamically loaded and used by different applications. While a single application with fixed functionality needs recompiling and linking for every extension, using components makes it trivial to replace or extend only parts of the application, or to add new functionality at runtime.
Component Concepts
Interface
An interface is a group of well-defined functions that provide access to a component. A client can query a component for an interface, and can use the interface to call functions implemented in the component. In the Qt Component Model,
- All interfaces inherit QUnknownInterface
- All interfaces are pure abstract classes
- Every interface has a unique ID
Interfaces can't change. Changing an interface would break any application and component relying on the binary compatibility of the interface.
Universally Unique Identifiers
A Universally Unique Identifier is a 128 bit value that is unique over both time and space. UUIDs are used by various component models to assign a 128 bit value to an interface or a component, so that interfaces and components can be compared efficiently. Most platforms provide an API that generates UUIDs (or GUIDs) with an algorithm that guarantees that the resulting ID is unique, using the network card address of the computer and a 60-bit timestamp that represents the number of 100-nanosecond intervals since 15 October 1582.
Components
A component is executable code that provides a specified set of publicly available services which are exposed using interfaces. A component can implement any number of interfaces. The client using the component does not have to know anything about the implementation details. If the client can get an interface from a component then it has to be safe for the client to use the interface without any preconditions.
Components can have an ID like interfaces and can use this ID to be registered in a system global database (e.g. the Windows Registry). This makes it fairly efficient for applications to load a specific component.
Component Server
Components are distributed by the means of component servers. In most cases this is a shared library, e.g. a .dll or .so file. A component server can provide any number of components.
Plug-ins and Add-ins
Plugins are component servers with components that implement an application specific interface. The application can make use of any available implementation of its interfaces to extend or modify existing functionality dynamically. Plugins are commonly provided as shared libraries, and placing the library in a special subdirectory makes the application recognize the new functionality at start up. Most applications support only single-component libraries.
Most exisiting component models, e.g. COM, use a similar architecture. This document assumes that you have a basic knowledge of component development. Recommended reading:
In the Qt Component Model, interfaces are declared as pure abstract classes. Since interfaces encapsulate a functionality of a component that is publicly available it does not make sense to have interfaces that have protected or private member functions, the struct keyword is used so that all functions are made public. A common naming convention for interfaces is to use the interface name with the postfix interface like in QUnknownInterface, or
struct IntegerInterface : public QUnknownInterface { virtual int add( int a, int b ) = 0; virtual int sub( int a, int b ) = 0; virtual int mul( int a, int b ) = 0; virtual int mod( int a, int b ) = 0; };
Many platforms provide a tool to generate a UUID that is associated with this interface. The Qt distribution includes quuidgen, a GUI tool that makes it easy to copy the generated ID directly into the source code:
// {2FFC826E-AA74-4528-8FC9-2FDE08F77CD4} #ifndef IID_Integer #define IID_Integer QUuid( 0x2ffc826e, 0xaa74, 0x4528, 0x8f, 0xc9, 0x2f, 0xde, 0x08, 0xf7, 0x7c, 0xd4 ) #endif
It is common to use the name of the interface together with the prefix IID_ as the descriptor for an interface ID.
quuidgen is located in the tools/quuidgen subdirectory of your Qt distribution and is not build by default, as the required header files or libraries are not present on all platforms. If you have difficulties building the quuidgen tool, go to e.g. http://www.google.com and search for libuuid.
A component implementing an interface simply uses a class that inherits from the interface and implements all the functions the interface declares. Since all interfaces inherit from QUnknownInterface, whose functions are all pure virtual, every QUnknownInterface function must be implemented.
class NumberComponent : public IntegerInterface { public: NumberComponent(); // from QUnknownInterface QRESULT queryInterface( const QUuid&, QUnknownInterface** ); ulong addRef(); ulong release(); // from IntegerInterface int add( int a, int b ); int sub( int a, int b ); int mul( int a, int b ); int mod( int a, int b ); private: ulong ref; };
The implementation of IntegerInterface is trivial.
The implementation of the QUnknownInterface defines the set of interfaces the component provides and controls the lifetime of the component using reference counting.
The implementation of queryInterface has to follow several rules to make the use of the component reliable:
See the QUnknownInterface API reference for a more detailed explanation of those rules.
An implementation of queryInterface usually looks like this:
QRESULT NumberComponent::queryInterface( const QUuid &uuid, QUnknownInterface **iface ) { *iface = 0; if ( uuid == IID_QUnknown ) *iface = this; else if ( uuid == IID_Integer ) *iface = this; else return QS_NOINTERFACE; (*iface)->addRef(); return QS_OK; }
Naturally, the queryInterface implementation should return a valid interface pointer for all interfaces implemented by the component, e.g. all the interfaces that the component inherits from, which includes the whole inheritance tree of those interfaces themselves.
The return type QRESULT is used to give a more detailed reason why an interface is available or not. Common return values for queryInterface are QS_OK if the call was successful, or QE_NOINTERFACE if the queried interface is not provided by this component.
Since a single component can be used by multiple clients at a time the client itself cannot simply delete the interface pointer when it is no longer used - it would invalidate all references a different client has to the same interface. Only the component itself knows when it is safe to delete itself, and the client has to tell the component when it is no longer used. This is done using a reference count. Whenever a pointer to an interface is copied, the reference count is increased using addRef:
ulong NumberComponent::addRef() { return ref++; }
addRef should also be called by any function that returns an interface directly or in an out parameter, e.g. in queryInterface. Whenever a pointer to an interface is no longer used, the counter is decreased using release. As soon as the count reaches zero the interface knows that there are no more references and it can delete itself:
ulong NumberComponent::release() { if ( !--ref ) { delete this; return 0; } return ref; }
If the reference count runs out of sync it is usually the reason for hard to find bugs - crashes when the interface get's released to often, or memory leaks when there are to many addRef calls.
The above standard implementations of addRef and release are provided by the C macro Q_REFCOUNT that can be used in the declaration of every class implementing QUnknownInterface, e.g.
class NumberComponent : IntegerInterface { public: NumberComponent(); // from QUnknownInterface QRESULT queryInterface( const QUuid&, QUnknownInterface** ); Q_REFCOUNT ... };
Since interfaces consist entirely of pure virtual functions, multiple inheritance makes it easy to implement multiple interfaces within a single class, without having to implement QUnknownInterface many times in a single component. If the NumberComponent implemented a second interface, e.g.
struct DoubleInterface : public QUnknownInterface { virtual double add( double a, double b ) = 0; virtual double sub( double a, double b ) = 0; virtual double mul( double a, double b ) = 0; virtual double mod( double a, double b ) = 0; };
class NumberComponent : public IntegerInterface { public: NumberComponent(); ... int mod( int a, int b ); // from DoubleInterface double add( double a, double b ); double sub( double a, double b ); double mul( double a, double b ); double mod( double a, double b ); };
the relevant part of the queryInterface implementation would become:
... if ( uuid == IID_QUnknown ) *iface = (QUnknownInterface*)(IntegerInterface*)this; else if ( uuid == IID_Integer ) *iface = (IntegerInterface*)this; else if ( uuid == IID_Double ) *iface = (DoubleInterface*)this; ...
The casting is necessary because both IntegerInterface and DoubleInterface inherit QUnknownInterface, and the compiler is not able to cast the this pointer to an QUnknownInterface* so the explizit cast becomes necessary. Which inheritance path is used for the casting is not important, but it has to be consistent throughout the component.
The classes declared and implemented above are supposed to be used by or added to a client application without having to relink the executable against the component's binary code created by the compiler.
In order to be able to distribute a component in binary form it must be wrapped in a component server. The most common kind of server is a shared library that gets dynamically loaded by the application. In order for the application to be able to access the component there must be a function exported by this shared library. This function will return a pointer to a QUnknownInterface implementation that can be used as an entry point to query for the interfaces needed. Qt provides a macro
Q_EXPORT_COMPONENT
that declares the exported function
QUnknownInterface *ucom_instantiate()
for all platforms. The address of this function can be resolved using the standard procedures for dynamically loaded libraries, or using the QLibrary class. An implementation of this function might look like this:
Q_EXPORT_COMPONENT() { NumberComponent *comp = new NumberComponent; QUnknownInterface *iface; comp->queryInterface( IID_QUnknown, &iface ); return iface; }
This default implementation is provided by another macro:
Q_CREATE_INSTANCE( NumberComponent )
so that the standard line for exporting the component becomes
Q_EXPORT_COMPONENT() { Q_CREATE_INSTANCE( NumberComponent ) }
The ucom_instantiate() function must be declared and implemented exactly once for each component server.
Sidenote: The Q_EXPORT_COMPONENT macro expands to a declaration and implementation of a second exported function:
int ucm_initialize( QApplication* theApp, bool *mt, bool *debug )
This function is called by the QLibrary class to ensure that the plugin does not use any incompatible Qt library versions, e.g. a multi threaded library when the application is running single threaded.
To build a component server, some settings have to be changed in the compiler and linker options. If you use qmake to generate your makefiles, the dll configuration does that for you. A simple component server with a single C++ implementation file main.cpp could look like that:
TEMPLATE += lib CONFIG += qt dll TARGET += myplugin VERSION = 1.0.0 DESTDIR = {...} SOURCES = main.cpp
Running
qmake myplugin.pro
will generate a makefile to build a shared library myplugin100 into the directory specified with DESTDIR.
The function exported by the component server returns a pointer to an implementation of QUnknownInterface. The client can then use queryInterface on that interface to query for the component's interface implementations. All interfaces returned will always be implemented by the same component - it is not possible to use queryInterface to query one component for an interface and get the interface implementation of another component. This makes it impossible for a client to access any other component than the one returned by the exported function.
The solution is to make use of another component that has the sole purpose of creating other components. This component has to be the one returned by the exported function, and it has to implement the interface
struct QComponentFactoryInterface : public QUnknownInterface { virtual QRESULT createInstance( const QUuid &cid, const QUuid &iid, QUnknownInterface** instance, QUnknownInterface *outer ) = 0; };
The cid parameter is a unique identifier for the component and tells the component factory which component to create. Each component supported by this component factory must have such an ID defined, for example using a static class member like this:
class NumberComponent ... { public: ... static QUuid CID; ... }; QUuid NumberComponent::CID = QUuid( 0xDD19964B, 0xA2C8, 0x42AE, 0xAA, 0xF9, 0x8A, 0xDC, 0x50, 0x9B, 0xCA, 0x03 );
The iid and iface parameters are then passed to the queryInterface function of the created component:
QRESULT ComponentFactory::createInstance( const QUuid &cid, const QUuid &iid, QUnknownInterface** iface, QUnknownInterface *outer ) { if ( cid == NumberComponent::CID ) { NumberComponent *comp = new NumberComponent; return comp->queryInterface( iid, iface ); } else if ( cid == SecondComponent::CID ) { SecondComponent *comp = new SecondComponent; return comp->queryInterface( iid, iface ); } return QE_NOCOMPONENT; }
The outer parameter can be used for component aggregation. Note that it is not possible to use the Q_CREATE_INTERFACE macro, since this would not propagate the iid parameter.
The client application can now use this component by explicitely loading the shared library, resolving and calling the ucm_instantiate function, and using the interfaces returned by the queryInterface implementation.
If the application knows which component server provides the component it wants to use it can load the component server using the QLibrary class like this:
int sum; QLibrary lib( "filename" ); IntegerInterface *iface; if ( lib->queryInterface( IID_Integer, (QUnknownInterface**)&iface ) == QS_OK ) { sum = iface->add( 5, 8 ); iface->release; }
The loading and unloading of the shared library and resolving of the ucm_instantiate function is done by the the QLibrary object and explained in the QLibrary API reference.
But loading a specific library file is not very generic - it makes the usage of the component depend on the installation of the shared library into a specific directory.
Most applications know only which component they want to use. In order to find the component server that provides this specific component the application has to look up the filename of the component server in some kind of central database, and the component will have to register itself in this central database.
The identification of components is again done using the UUID assigned to the component, and using this UUID a component - or the installation process for the component - can add an entry to the central component database. The application can then look up the UUID it requires in the database and use the filename to create a QLibrary object.
Registering the components in a server is done by implementing the QComponentRegistrationInterface using the QComponentFactory class like this:
bool ComponentServer::registerComponents( const QString &filepath ) const { return QComponentFactory::registerComponent( NumberComponent::cid, filepath, "Component for numbers" ); } bool ComponentServer::unregisterComponents() const { return QComponentFactory::unregisterComponent( NumberComponent::cid ); }
On Windows, this will use the system registry and register the component along with all other COM components. On UNIX, this will create a settings file .CLSID that lists all installed components. To register all components in a server, it is sufficient to load the library, query for the QComponentRegistrationInterface and call the registerComponent function during the installation process of the component.
Any client application that wants to use this specific component can then use the static functions of the QComponentFactory class to get an interface to the requested component:
int sum; IntegerInterface *iface; if ( QComponentFactory::createInstance( QUuid("{DD19964B-A2C8-42AE-AAF9-8ADC509BCA03}"), IID_Integer, (QUnknownInterface**)&iface ) == QS_OK ) { sum = iface->add( 5, 3 ); iface->release(); }
Both approaches described above make it necessary for the application to know details of the component it wants to use. But many applications only know that they want to use any component that implements a certain interface, e.g. to provide plugin support. The application would then have to load multiple component servers, query for the required interface and add the returned component to an internal list which it can look-up to use the component when necessary.
If an application wants to enable plugin support it has to provide one or more interface definitions that the plugins can implement to add functionality to the application. A plugin for an image processing application might implement the following interface to add a color-manipulation filter:
struct ImageColorInterface : public QUnknownInterface { QImage changeColor( const QImage &image ) = 0; };
The application will be able to use all components that implement this interface as plugins. All the application needs to know now is which interface implementation to call for which image filter. But this is not possible with the above interface definition - there is no way for the application to "know" which color filter is provided by which component.
A plugable component implements an interface providing a certain set of "features", and it must inform the client application about the list of features it provides. To achieve this the component must implement the QFeatureListInterface
struct QFeatureListInterface : public QUnknownInterface { QStringList featureList() const = 0; };
To make the ImageColorInterface "plugable" it has to inherit QFeatureListInterface, and the changeColor function would have to change like this:
struct ImageColorInterface : public QFeatureListInterface { QImage changeColor( const QString &feature, const QImage &image ) = 0; };
All functions of this interface must be passed the name of the requested feature, and have to be implemented like this:
QImage ImageColorComponent::changeColor( const QString &feature, const QImage &image ) { QImage newImage; if ( feature == "Sepia" ) { ... } else if ( feature == "Contrast" ) { ... } else if ( feature == "Brightness" ) { ... } else { newImage = image; } return newImage; }
Sometimes this is not necessary or wanted, so there is another interface that can be used to inform the application about what the component does:
struct QComponentInformationInterface : public QUnknownInterface { QString name() const = 0; QString description() const = 0; QString version() const = 0; QString author() const = 0; };
Any component that implements the ImageColorInterface and either the QFeatureListInterface or the QComponentInformationInterface can now be used as a plugin for an image processing (or any other) application. The QPluginManager template class provides a generic solution for this.
QImage image( "picture.png" ); QPluginManager<ImageColorInterface> colorPlugins( IID_ImageColor, "./plugins" ); ImageColorInterface *iface; colorPlugins.queryInterface( "Sepia", &iface ); if ( iface ) { image = iface->changeColor( "Sepia", image ); iface->release(); }
The plugin manager will load all shared libraries in the path specified using QLibrary, calls queryInterface() to test the library for the implementation of the interface of type IID_ImageColor, and uses any implementation of the QFeatureListInterface or the QComponentInformationInterface to get information about what the plugin actually does. Calling
colorPlugins.queryInterface( "Sepia", &iface );
will have the plugin manager look up the library that is responsible for the feature "Sepia", gets a pointer to the ImageColorInterface implementation in that component, and returns this pointer in the iface variable that can then be used to call the implementation of the "Sepia" color filter.
With the QPluginManager's destruction, all plugins are unloaded again.
Even though an application and shared libraries loaded by that application live inside the same address space, all heap operations executed within the code operate on separate parts of the memory. This raises a number of issues on all platforms
If you allocate memory in component code using operator new, make sure that there are no more references to that memory before unloading the library. See the documentation of the QLibraryInterface for more information.
On certain platforms, additional problems result from the implementation of the library loader, or the memory management
The qmake tool will apply settings to generate code for the shared C runtime to make sure that all binaries use the same instance of the runtime library.
QLibrary tries to catch those mismatches and will prevent loading of libraries, or issue a warning.
Copyright © 2001 Trolltech | Trademarks | Qt version 3.0.0-beta5
|