![]() |
| ||
Classes - Annotated - Tree - Functions - Home - Structure |
This documentation is under development and is subject to change.
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 |
---|
InterfaceAn 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,
Interfaces can't change. Changing an interface would break any application and component relying on the binary compatibility of the interface.
Universally Unique IdentifiersA 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.
ComponentsA 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 ServerComponents 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-insPlugins 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; };
Most platforms provide a tool to generate a UUID that is associated with this interface. The Qt distribution includes quuidgen which is a GUI tool that makes it easy to copy the generated ID directly into the source code, e.g.
// {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.
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; if ( *iface ) (*iface)->addRef(); return QS_OK; }
The return type QRESULT can be used to give a more detailed reason why an interface is available or not. Valid 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, but 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 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_INTERFACE
that declares an exported function
QUnknownInterface *ucom_instantiate()
for all platforms. The address to 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_INTERFACE() { 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 )
The ucom_instantiate function must be declared and implemented exactly once for each component server.
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 plugin 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 plugin 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 Q_EXPORT 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 QComponentServerInterface 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 QComponentServerInterface 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 QComponentInterface : 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 QComponentInterface 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(); }
While the plugin concept allows you to dynamically extend and modify an application it has one major drawback: the application has to load a large number of libraries to determine which ones provide suitable components. This might not be a problem for a small number of highly specialized plugins, but when plugins are shared between different applications the number of libraries being loaded and unloaded unnecessarily makes application startup very inefficient.
A Component Trader tells an application which components implement a certain set of interfaces, and the application can then select and use one of those components. If multiple components are suitable the user is able to prioritize a single component.
This makes it necessary for components to register their set of implemented interfaces in the central database, so that the component trader can look up the components matching the request and pick the preferred one for the application to use.
Registering (and unregistering) the component is again done in the implementation of the QComponentServerInterface:
bool ComponentServer::registerComponents( const QString &filepath ) const { return QComponentFactory::registerComponent( TestComponent::cid, filepath, "Test Component" ); }
Copyright © 2001 Trolltech | Trademarks | Qt version 3.0.0-beta3
|