Free Electron
Component Writer's Guide

Purpose

This document should describes how to make your own components and package them into a library.

Component

Each component has to be virtually derived from fe::Component, but not necessarily directly. Since components are instantiated through automatically generated factories, all component implementation classes should have a single constructor with no arguments.

Any component can participate in a post-constructor mechanism. This allows all participating classes in the the class hierarchy to have their initialize methods called in the same order as the original construction. Post-construction is useful for constructive operations that cannot be done safely or appropriately during real construction. See fe::Initialized for details.

Smart Pointers

Smart pointers use a reference-counting mechanism in order to destruct object once the count of references drops to zero. The FE smart pointer mechanism is intrusive; the count is held in the object itself. These objects are presumed to be derived from fe::Counted, but all that is technically required is that they provide some of the same method functions as fe::Counted.

There are three variations of smart pointers that are used in FE.

The regular smart pointer fe::sp, once assigned to a valid pointer, will stay valid as long as that reference is held in some persisting scope.

The weak smart handle fe::hp, can become invalid even if it was once valid. The isValid() method can check this status, but it is safer to simply cast an fe::hp to an fe::sp in a narrow scope where it is used, in case it becomes invalid between when it is checked and when it is accessed.

The protectable copy-on-write pointer fe::cp acts much like a regular smart pointer fe::sp except that it can be protected against writing by spontaneous cloning a duplicate. This is not used as often as smart pointer and handle pointer, but it is very important in the few cases where it is needed.

All components can be used in smart pointers (fe::sp), smart handles (fe::hp), copy-on-write pointers (fe::cp), and smart hubs (fe::Component::adjoin).

Interfaces and Implementations

The plugin system does not require an interface/implementation design paradigm. However, it does promote and facilitate its use.

This is how you can use interfaces in the context of this framework.

Interfaces are abstract classes. They contain a set of public virtual functions that state what implementions fulfilling that interface can do. Generally, interfaces are always virtually inherited to prevent redundant base classes. By convention, all interface class names in FE end with a capital 'I', such as fe::SurfaceI.

An implememention defines actual code for any number of interfaces. Under normal conditions, implementations should only be accessed through their interfaces.

Implementations can inherit from other implementions to extend or reuse existing code, even across multiple dynamic libraries. They generally inherit non-virtually since it is more efficient.

Inherited implementations have more intimate access to the base classes than would normally be available through just the interfaces. It is basically the same design responsibility that exists in normal class inheritance.

Dirty interfaces are deviations that contain state and/or functionality in the interface. They are frowned upon, but there is no mechanism to prevent them.

Component Naming

The string names for components are expected to follow a specific dot-based convention of the form "InterfaceI.Implementation.origin.misc".

By convention, the first token exactly states a C++ interface class that the component fulfills. A component can register as multiple interfaces if it would like to make itself explicitly available to fulfill all those interfaces. A component is not required to register for all interfaces that it could potentially fulfill.

The second token exactly states the C++ class that is explicitly instantiated for this name.

The third token states who is providing the interface, presumably a organization or company name.

The fourth token is basically anything that follows, often a further distinction, like a file format. It isn't required and shouldn't be expected.

Requests for component creation are usually abbreviated. If a request string matches a candidate exactly up to where the terminator in the request aligns with a period in the candidate, the pair is considered a match.

For example, a request for "WindowI" can be resolved with "WindowI.NativeWindow.fe".

An asterisk between two periods matches anything in that token. Using the same example, "WindowI.*.fe" matches "WindowI.NativeWindow.fe" or "WindowI.SpecialWindow.fe", but not "WindowI.NativeWindow.substitute".

Complex regular expression matching is not supported in this context.

Module Files

A set of closely related components are grouped into a module. The source for each module is contained in one subdirectory of a module set, such as ext/ or lab/. See Directory Structure under Framework Basics for more information on module sets.

There is a common layout and naming for the files in a module. Capitalization is intentional and specific.

Each class has its own header that matches the name of the class exactly, followed by ".h". If the class has implementation other than in the header, that is stored in a file of the same name, except ending in ".cc" instead of ".h". So, the ".h" and ".cc" of each class should show up adjacent to each other at the base directory of each module.

Each module has a file named bld.py that contains the python on how to build that specific module. The are many examples of how to write a build file for a module, given all the modules included. Most of the build setups are pretty generic, generally just listing out source files and then detailing added include paths, library paths, and external libraries, as needed. But other build files are much more specialized, like creating multiple versions of a module for multiple third party versions, or for spawning additional builds that use other tools. Even if there were a module that, say, only contained headers and didn't need to build anything, every module should still contain the build file to be clear that it is a module. Also, the build system has a mechanism for each moduleto check and report that it is buildable in the current environment. This should be done in each build file, when at all reasonable.

Each module usually contains a public collective header and private master header. If a module only contains hidden implementations and no additional public includes, then a public collective header may not be necessary. If a module only contains public headers, like interfaces, then a private master header may not be necessary. Both of these situations do happen.

The public collective header has the same name as the module directory followed by ".h". It contains all the interface headers and required dependencies that another module or program would need to utilize this module without explicitly seeing the implementations.

The private master header has the same name as the module directory followed by ".pmh". It first includes the public collective header and then adds includes for all those implemention details that were hidden in the public collective header. This file is usually included by all the ".cc" files of the module as well as any other mdules that want to derive from an implementation of the module. Where supported, the private master header is used to create a precompiled header, during the build, which can significantly improve build times. For this to work properly, each ".cc" of a module should usually include just the private master header. If there are additional includes for an individual file, it should follow the private master header or the precompiled header won't match, requiring a longer compile.

Each module that has components available for use will need a file with the same name as the module directory, followed by "DL.cc". This file has the externally exposed C functions that make the module functional as a plugin. The three recognized functions are ListDependencies(), CreateLibrary(), and InitializeLibrary(). ListDependencies() and InitializeLibrary() are optional. ListDependencies() provides a list of other modules that must be loaded prior to the one being described. CreateLibrary() explicitly creates the fe::Library object and can add factories through that object. InitializeLibrary() is sort of a post-constructor for libraries. The resulting dynamic library that can be built will be represented as a shared object (.so) under Linux or a DLL (.dll) under Windows.

So, for a module called "acme", there might be a list of files like so:

Anvil.cc
Anvil.h
CatapultTrap.cc
CatapultTrap.h
PurchaseI.h
Rope.cc
Rope.h
TrapI.h
bld.py
acme.h
acme.pmh
acmeDL.cc

Often, a derived class's name will adopt some part on the name of a class it derives from, in either ascending or descending scope, but this is not part of any requirement. Thoughtful and intuitive class naming is merely good practice.

The Plugin Walkthrough describes an example of putting together a simple module.