XPCF is a lightweight cross platform component framework (it is similar to the well known COM model).
It is designed to provide dependency injection, with clarity, simplicity and light code base in mind.
It implements the abstractfactory and the factory design patterns (through the ComponentManager and ComponentFactory classes, and through IFactory interface).
It also provides a safe toolkit to avoid common c++ design issues such as :
- ensure use of one unique shared_ptr type (even on platforms with several STLs : the SRef class is guaranteed to be from one source only through boost binary)
- ensure correct memory handling (avoid memory leaks)
- provide parallel helpers such as tasks, fifo, circular buffers, delays..
- a common configuration design : configuring a component doesn't require to write serialization code
XPCF also provides remoting automation by generating grpc remoting code from XPCF interfaces, using a dedicated DSL (Domain Specific Language) - see XPCF remoting architecture chapter.
Learn about the latest improvements.
A module consists in a shared library hosting a set of factories to create components/services
Provides:
-
introspection to figure out which components are available in the module
-
separate implementation from interface : creates the concrete implementation and binds it to the IComponentIntrospect interface
-
component creation
-
service instanciation
A component/service interface in the component framework context is :
-
based on C++ virtual class
-
uniquely identified by an UUID (128-bit number guaranteed to be unique based on timestamps, HW addresses, random seeds)
-
mandatory derives from IComponentIntrospect (the base interface of all XPCF interfaces)
-
is a special interface
-
does not derive from any other interface
Provides:
-
component introspection to query interfaces implemented by the component
-
functional reference counting
Special Notes about queryInterface :
-
queryInterface from interface A to interface B, must be able to queryInterface back from interface B to interface A
-
queryInterface from interface A to the IComponentIntrospect interface, must equal the queryInterface from interface B to the IComponentIntrospect interface
An implementation of one or more interfaces identified by a Component UUID
NOTE: The introspection interface is the same for both components and services
-
a class that can have several instances over the application lifetime
-
each instance is unloaded (destroyed) when no more reference exists upon one of its interfaces
-
a component singleton that only has one instance at any given time
-
is never unloaded
___________________________________________________
| Module #1 |
| entry point => XPCF_getComponent |
| ______________________________ |
| | Component #UUIDC1 | |
| | ___________________ | |
| | I0 -> | I0 implementation | | |
| | ------------------- | |
| | | |
| | ___________________ | |
| | I1 -> | I1 implementation | | |
| | ------------------- | |
| ------------------------------ |
| |
| ______________________________ |
| | Component #UUIDC2 | |
| | ___________________ | |
| | I0 -> | I0 implementation | | |
| | ------------------- | |
| | | |
| | ___________________ | |
| | I2 -> | I2 implementation | | |
| | ------------------- | |
| ------------------------------ |
| |
| entry point => XPCF_getService |
| ______________________________ |
| | Service #UUIDS1 | |
| | ___________________ | |
| | I3 -> | I3 implementation | | |
| | ------------------- | |
| ------------------------------ |
---------------------------------------------------
This framework provides the following concepts :
-
an "in-code" dependency injection factory to create components and bind them to a specific interface
-
a component abstract factory to load components at runtime from modules (named ModuleManager)
-
increase modularity with better separation of concerns through interface discovery (using component introspection)
-
a centralized description of modules, components and interfaces (with a dedicated configuration file)
-
module introspection to figure out the components contained and the interfaces they implement
-
secure memory management using smart pointers for components (prevents from memory leaks, dangling null pointer)
-
component and service configuration at creation through abstract variant parameters
-
support for both components and services
-
support for Domain Specific Language through the "IFeatures" interface (for instance, a component can declare "opencv" and "cuda" features as it is an opencv/cuda accelerated implementation of a keypointdetector)
Note: Variant parameters for configuration are a set of parameters. Each parameter is a pair of type, value. Supported types include long, unsigned long, double, string or another subset of parameters. Each parameter has an access definition : in, out or inout.
- an
\<aliases\>
section can be declared in anyxpcf-registry
orxpcf-configuration
node to declare named alias to uuids for an interface, a component or a module. - aliases can be used in
<factory>...</factory>
or<properties>...</properties>
sections to refer indifferently to components, interfaces either through their uuids or alias. Note: an alias name must be unique in aComponentManager
context. - autoalias mode: makes an alias between an interface/component name and its uuid. Provides the ability to use the alias in place of the uuid in factory bindings and in properties configuration (only supported with new
<properties>
semantic). autoalias creates an alias when the first occurence of a name is met (particularly important for interfaces). This mode is enabled whenautoAlias
xml attribute is set to "true" in<xpcf-registry autoAlias="...">
or<xpcf-configuration autoAlias="...">
Note: - when a name is encountered twice, the second occurence is ignored (it should always be the same alias which is true for interfaces)
- any ambiguity must be resolved with explicit aliasing through an xml
<aliases>
node - a potential evolution will be to "namespace" the component name with the module name to reduce naming conflicts
- compile-time injection is provided through
IInjectable
interface - local components can be used during component injection once registered to XPCF using
bindLocal
orbind
methods (components defined in the library's own code or in libraries linked, but not from dynamically loaded modules). The bind method accepts a factory method to create the concrete component instance ->ComponentBase
implementsIInjectable
and provides injection capabilities to every xpcf components - recursive dependency injection and configuration is supported
- injection errors are handled with
InjectionException
,InjectableNotFoundException
andInjectableDeclarationException
exceptions - declarative injection (configuration time injection) is provided in registry files through
<factory>...</factory>
declarations (refer to ````doc/xpcf-registry-sample.xml``` for the semantic) - autobind mode is always enabled. While parsing the
<module>
nodes, it automatically "wires" (binds) components to their declared interfaces. autobind also works upon module introspection made at compile time usingIComponentManager::loadModules
orIComponentManager::loadModuleMetadata
. Note: as autobind is a default behavior, only redundant/specific binds must be declared in<factory ... />
node. Otherwise, default binds are created from components definitions. Note: Autobinding ignoresIComponentIntrospect
,IConfigurable
andIInjectable
XPCF interfaces. Autobinding handles wiring from user's interfaces to components.
IComponentManager
provides:
- global "inject" methods helper
- dependency injection through default binding while parsing the
<module>
nodes, or through<factory><bindings>...
declarations. - structured injection. See the semantic in xpcf-registry-sample.xml: node
<injects>
. - named binding injection ability either from configuration (using
<factory>...</factory>
node) or at compile time - injection from bindings upon
createComponent
from component type trait or uuid - concrete component instance resolution from its interface ( or service ) type traits or uuid (and optional name) Upon creation of the concrete instance injection of the component injectables is made when needed through "resolve" methods, and each injectable is configured. Finally the concrete instance is configured and returned to the caller.
From 2.5.0, XPCF now supports several creation contexts through the "IFactory" concept. The IComponentManager interface provides access to its inner Factory. From this factory, it is possible to create new factories. New factories are created with a context mode within :
- Empty: creates a new and empty factory
- Cloned: creates a factory with a copy of bindings existing in the original factory. Later bindings, configuration ... will only be valid within this cloned factory.
- Shared: creates a factory that shares the bindings, configuration definition. Only component instances are local to the factory. Any later bind or configuration update will be share between the new and the original factory.
A new feature in xpcf 2.5.0 version is the ability to get remote versions of components almost "out of the box".
The remoting functionality works for any xpcf interface that :
- uses datastructures that declare one of the serialization methods exposed through xpcf specifications (one of the methods provided in xpcf/remoting/ISerializable.h) - in 2.5.0 only boost serialization is handled, upcoming minor releases of xpcf will provide support for the other methods
- uses base c++ types only
Note : pointers are not handled as they can handle arrays ... hence a pointer will need a size, but no presumptions to detect such cases can be done. Anyway, interfaces represent contracts between a user and a service, hence interfaces should be self explanatory. In this context, relying on pointersd doesn't explicit the expected behavior, hence interfaces should rely on structured types either std ones (string, vectors ...) or user defined ones (struct, classes).
xpcf provides a Domain specific language through the use of c++ user defined attributes to ease the generation, those attributes can be used on interfaces or on methods of an interface, depending on the attribute.
Attribute | Argument type | Apply to | Semantic |
---|---|---|---|
[[xpcf::ignore]] | none | class or method level | specify that the corresponding class/method must be ignored while generating remoting code |
[[xpcf::clientUUID("uuid_string")]] | string | class level | specify the xpcf remoting client UUID |
[[xpcf::serverUUID("uuid_string")]] | string | class level | specify the xpcf remoting server UUID |
[[grpc::noCompression)]] | none | class or method level | disable grpc compression for a whole interface or for a method (compression code generation is not made for the targeted item) |
[[grpc::server_streaming]], [[grpc::client_streaming]] or [[grpc::streaming]] | none | method level | |
[[grpc::request("requestMessageName")]] | string | method level | optionally set grpc request message name |
[[grpc::response("responseMessageName")]] | string | method level | optionally set grpc response message name |
[[grpc::rpcName("rpcname")]] | string | method level | optionally set grpc method rpc name |
xpcf provides several tools to ease the remoting of components :
-
xpcf_grpc_gen : parses the interfaces and generates the needed remoting code for each inteface in a project.
- From an interface, the tool creates :
- grpc request and response messages with each input/output type translated to grpc types
- a grpc service containing rpc : one for each method of the interface
- a proxy component that inherits from the interface. This component transforms the interface input/output types to grpc request/response and calls the grpc service and rpc method corresponding with the method interface. This component is configured with a channel url (address and port of the server hosting the concrete code) and a grpc credential.
- a server component inheriting from IGrpcService. It also implements the grpc::service class, and the concrete xpcf component is injected to this component. Each rpc method of the grpc::service class is implemented and translates grpc request/response to the xpcf component input/output data types, and calls the xpcf component corresponding method
- From the project the tool creates :
- a Qt creator development project to compile all the generated code in an xpcf module
- a client and a server xpcf xml configuration file
- From an interface, the tool creates :
-
xpcf_grpc_server : it is a xpcf application that hosts any IGrpcService enabled component.
To come: xpcf_grpc_linter : this tool will be used at compilation time to validate the xpcf remoting DSL.
To help developers create efficiently XPCF based applications, several Qt Creator wizards are provided. Wizards are located in the XPCF release in xpcf/version/wizards, or in the xpcf github repository in the wizards folder. To install the wizards, first install Qt Creator, then launch install.bat (windows platform) or install.sh (linux or macosX platforms).
This wizard creates a complete Qt Project to build a shared library containing a xpcf module. It contains a skeleton module_main.cpp file with the main XPCF entry point. It depends on qmake rules (from the builddefs-qmake github repository, installed with the remaken package manager tool from remaken github repository ).
This wizard creates a xpcf interface header file. This file is the public API. Any component interested to provide this interface functionality will use this header file. The developer only need to add its methods to the interface.
This wizard creates a xpcf component header and source file. The developer only need to override and implement the methods defined in the interfaces provided by the component. The component header file doesn't need to be exposed. Only the interfaces implemented are part of the API.
This wizard creates a complete Qt Project to build an application that use xpcf module and components.
XPCF components can be found, configured and injected with a xml configuration file.
###Node tree The supported xml structure is (in each xml node, "..." symbolizes attributes):
<xpcf-registry>
<module ... > <!-- module declaration -->
<component ... > <!-- component declaration -->
<interface ... /> <!-- interface declaration -->
</component>
<!-- ... other component declarations -->
</module>
<aliases>
<alias ... />
<!-- ... other alias declarations -->
</aliases>
<factory>
<!-- default bindings -->
<bindings>
<bind ... />
<!-- ... other binding declarations -->
</bindings>
<!-- custom bindings : explicit injection for a particular component class -->
<injects>
<inject ... >
<bind ... />
<!-- ... other binding declarations -->
</inject>
<!-- ... other inject declarations -->
</injects>
</factory>
<properties> <!-- properties declaration -->
<configure ... > <!-- component configuration declaration -->
<property ... /> <!-- single value property declaration -->
<property ...> <!-- multi value property (vector) declaration -->
<value>...</value>
<!-- ... other value declarations -->
</property>
<!-- ... other property declarations -->
</configure>
<!-- ... other component configuration declarations -->
</properties>
</xpcf-registry>
Section (parent node) | Node | Attributes | Semantic |
---|---|---|---|
document root | xpcf-registry | autoAlias = [true, false] -> set whether XPCF must automatically create aliases while parsing the file between : component UUID <-> component name interface UUID <-> interface name |
This node declares an xpcf registry file, containing both components and modules structure and their properties |
document root | xpcf-configuration | This node declares an xpcf configuration file only, containing components properties | |
xpcf-registry | module | uuid = module is referenced with an uuid. It must be declared here. name = name of the module used path = path to the module used (can contain environment variables) description = the module function description |
declares an xpcf module |
module | component | uuid = component is referenced with an uuid. It must be declared here. name = name of the component used description = the component description |
declares an xpcf component inside a module |
component | interface | uuid = interface is referenced with an uuid. It must be declared here. name = name of the interface used description = the interface description |
declares an xpcf interface implemented by a component |
xpcf-registry / xpcf-configuration | aliases | declares an aliases section. When autoAlias = true in <xpcf-registry>, an alias is created for each component/interface in <module> node when the first occurence of a name is met. Hence the <aliases> section must be used to resolve any name ambiguity, or to simplify a non intuitive name. | |
aliases | alias | name = name of the alias type=[component, interface] uuid = the uuid of the component/interface targeted by the alias |
declares an alias. Alias can be ... |
xpcf-registry / xpcf-configuration | factory | declares the factory section | |
factory | bindings | declares the bindings section - this section is needed only to overload autobinds made while parsing the <module> node | |
bindings | bind | interface = interface alias or uuid to = component alias or uuid [optional] name = name of the binding [optional] range = [all, default, named, withparents, explicit] the binding range [optional] scope = [transient, singleton] the binding scope: singleton ensure there will be only one instance of the binded component in this factory context. Transient scope ensure a component is created for each resolution made within the factory context. [optional] properties = name - the properties name to use to initialize the binded component |
declares a default or named bind between an interface and a component |
factory | injects | declares the injects section - used for structured (also called planned) injection for specific component class | |
injects | inject | to = component alias or uuid | declares an injection pattern for a specific component class |
inject | bind | interface = interface alias or uuid to = component alias or uuid [optional] name = name of the binding [optional] range = [all, default, named, withparents, explicit] the binding range [optional] scope = [transient, singleton] the binding scope: singleton ensure there will be only one instance of the binded component in this factory context. Transient scope ensure a component is created for each resolution made within the factory context. [optional] properties = name - the properties name to use to initialize the binded component |
declares a specific bind (component context bind) between an interface and a component for the specific component class declared in <inject> |
xpcf-registry / xpcf-configuration | properties | declares the components properties section. This section defines component parameters values for configurable components. | |
properties | configure | [optional] uuid = the component uuid or component = a component alias name |
declares the configuration for a specific component |
configure | property | name = property name type = [int, uint, long, ulong, string, wstring, float, double, structure] value = property value (must be a valid value for the type chosen) value can also be declared as sub-nodes for vector properties |
declares a component property |
property | value | declares a component property value |
Note :
Aliasing order : an alias declared in the section is added to the list of existing aliases, or replace any existing alias with the same name. section overload autoAlias already declared.
Binding order :
- autobind : during the parsing of the <module> nodes, a bind is added the first time an interface is met. If the same interface is also declared for another component, the first bind is maintained. where a [interface, component] pair is met.
- factory bindings : when an explicit bind is defined, it replaces any previous autobind made while parsing the <module> nodes.
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<xpcf-registry autoAlias="true">
<module uuid="{3b899ff0-e098-4218-bdaa-82abdec22960}" name="xpcfModule" description="an sample module with some components" path="$REMAKEN_RULES_ROOT/xpcfModule/2.3.1/lib/x86_64/shared">
<component uuid="ae9d10bb-4bd1-462b-8c01-43f1815c6ae0" name="feature1Component - not configurable" description="feature1Component is not Configurable, it implements I0 and I1 interfaces">
<interface uuid="125f2007-1bf9-421d-9367-fbdc1210d006" name="XPCF::IComponentIntrospect" description="Component introspection interface."/>
<interface uuid="46333fd5-9eeb-4c9a-9912-b7df96ccf5fc" name="I0" description="provides I0 features"/>
<interface uuid="3bc8f5ea-ee24-473e-8afd-4f5b1b21c018" name="I1" description="provides I1 features"/>
</component>
<component uuid="63ff193d-93e6-4ede-9947-22f864ac843f" name="feature2Component - configurable" description="feature2Component implements IConfigurable, I2 and I1 interfaces">
<interface uuid="125f2007-1bf9-421d-9367-fbdc1210d006" name="XPCF::IComponentIntrospect" description="Component introspection interface."/>
<interface uuid="98dba14f-6ef9-462e-a387-34756b4cba80" name="XPCF::IConfigurable" description="Provides generic configuration ability to any component through metadata parameters"/>
<interface uuid="3bc8f5ea-ee24-473e-8afd-4f5b1b21c018" name="I1" description="provides I1 features"/>
<interface uuid="41348765-1017-47a7-ab9f-6b59d39e4b95" name="I2" description="provides I2 features"/>
</component>
<component uuid="{7cc64f89-9a81-4fc8-9695-fde123fae225}" name="technicalComponent - not configurable" description="component provides technical feature to functional components">
<interface uuid="125f2007-1bf9-421d-9367-fbdc1210d006" name="XPCF::IComponentIntrospect" description="Component introspection interface."/>
<interface uuid="41348765-1017-47a7-ab9f-6b59d39e4b95" name="I2" description="provides I2 features"/>
<interface uuid="6279bb1d-69b5-42bd-9da1-743c8922bcb9" name="I3" description="provides I3 features"/>
</component>
</module>
<aliases>
<alias name="feature1Component" type="component" uuid="ae9d10bb-4bd1-462b-8c01-43f1815c6ae0"/>
<alias name="feature2Component" type="component" uuid="63ff193d-93e6-4ede-9947-22f864ac843f"/>
<alias name="technicalComponent" type="component" uuid="7cc64f89-9a81-4fc8-9695-fde123fae225"/>
</aliases>
<factory>
<!-- default bindings -->
<bindings>
<bind interface="I1" to="feature1Component" />
<bind interface="3bc8f5ea-ee24-473e-8afd-4f5b1b21c018" to="ae9d10bb-4bd1-462b-8c01-43f1815c6ae0" />
<bind name="feature2Component_I1" interface="I1" to="feature2Component"/>
</bindings>
<!-- custom bindings : explicit injection for a particular component class -->
<injects>
<inject to="feature1Component">
<bind interface="I2" to="technicalComponent" />
</inject>
</injects>
</factory>
<properties>
<configure component="feature2Component">
<property name="parameter1" type="double" value="2.0"/>
<property name="vector_parameter1" type="double" access="ro">
<value>0.2</value>
<value>0.5</value>
</property>
<property name="structured_parameter" type = "structure">
<property name="cols" type="long" value="2"/>
<property name="rows" type="long" value="2"/><!-- rowmajormode only-->
<property name="values" type="double">
<value>1.1</value>
<value>0.7</value>
<value>0.3</value>
<value>2.7</value>
</property>
</property>
</configure>
</properties>
</xpcf-registry>
In order to build XPCF:
- Install conan
- Install QT to get QT Creator
- Clone xpcf repository with submodules (
git clone --recursive https://github.com/b-com-software-basis/xpcf.git
). If you have already cloned the repository but not the submodules, rungit submodule update --init --recursive
within your xpcf repository folder. - If you want a specific version of xpcf, you need to generate files from template running in the folder of xpcf.pro the script update_version.sh for Linux or update_version for Windows with the parameter
-v
and a version numberx.y.z
- Open xpcf.pro file in QT Creator and configure the project with the compilation kit of your OS (gcc on Linux, clang on mac os X, msvc 2017 for windows)
- Run qmake - this can take a while as it should compile boost from conan.
- if there isn't any file generated from template, qmake will do it with a default version number
0.0.0
- if there isn't any file generated from template, qmake will do it with a default version number
- Add an install target for make in "compilation steps" from the "projects" tab in Qt Creator
You can now build xpcf using Build inside Qt Creator.
The compilation result will be located in $HOME/.remaken/packages/[platform-compiler-version]/xpcf/XPCF\_VERSION
(%USERPROFILE%\\.remaken\packages\\[platform-compiler-version]\xpcf\XPCF\_VERSION
on windows)
Note: To automatically include and link with xpcf headers and library, use the pkgconfig file bcom-xpcf.pc located at the root of the above folder.
Components are hosted in modules (i.e. shared libraries with special hook methods). Hence prior to the creation of components, start by creating a module.
To create an XPCF module, interface or component it is recommended to use the QT
Creator wizards located in wizards/qtcreator
folder in xpcf repository.
To install the wizards, please refer to INSTALL.txt.