Monday, September 14, 2009

The Gallio Plug-in Model

One of the biggest changes in Gallio v3.1 was is a new plug-in model.

The new plug-in model features comprehensive support for late-binding of components, provides a rich declarative metadata model, and provides enough information to support the installation and verification of plug-in assemblies and resources.


Late-Binding and Declarative Metadata

Gallio includes many plug-ins out of the box and more can be installed later but not all of the plug-ins need to be loaded at all times.  To improve startup time, Gallio defers plug-in assembly loading until the last possible moment.  Consequently Gallio's plug-in model relies heavily on late-binding and declarative metadata that is external to the plug-in assemblies.

We don't use .Net custom attributes to provide most of this metadata because then we would have to load assemblies in order to read it out.  (Note: We could read metadata from custom attributes once and cache it like MEF does but it is not practical for some purposes such as plug-in installation.)

So the basic idea is to pack a bunch of useful information in XML.

Drop-In Plug-In Installation and Caching

Gallio strives to support XCopy-style deployment as much as possible.  This feature makes it possible to check-out a copy of Gallio from your source tree and just run it with no strings attached (except for special features that require installation like Visual Studio integration).

When designing the Gallio plug-in model it was important to ensure that Gallio plug-ins could be installed just by dropping files in the right place.  At run-time Gallio locates its plug-ins by searching plug-in directories recursively for .plugin files.  There is one major problem with this process: recursively scanning directories and loading lots of little files can be very time-consuming.  So the new plug-in model incorporates a cache.

Gallio maintains a per-user cache of installed plug-ins in any given list of plug-in directories.  This way when it starts up, it only needs to load the cache file and move on.

However there is a small problem with this approach.  By caching plug-in metadata, it is no longer sufficient to just drop in new plug-in files to install them.  The cache must be cleared somehow.

Gallio implements three strategies for refreshing its cache:

  1. When loading a cache file, it checks the timestamps on the original plug-in files to verify that they have not been changed.  This way Gallio can automatically detect an edit to a .plugin file and refresh the cache.  Unfortunately it cannot detect the presence of new .plugin files.
  2. A user can explicitly clear the plug-in cache using the  "Gallio.Utility.exe ClearUserPluginCache" command.  Unfortunately if there are multiple users of the machine then all users will need to have their plug-in cache cleared to detect the presence of a new plug-in.
  3. A admin user can explicitly reset the Gallio "installation id" of the machine by running "Gallio.Utility.exe ResetInstallationId".  This command effectively invalidates the caches of all users running on the machine at once.

It's not ideal but it works well for now.  Plug-in metadata caching has significantly improved Gallio start-up performance.

Plug-in Metadata

.plugin files

Each Gallio plug-ins has a ".plugin" file that describes the plug-in using XML.  The description includes the id of the plug-in, some traits, dependencies, associated files, referenced assemblies, services and components.

Here is an excerpt of Gallio.plugin.

<?xml version="1.0" encoding="utf-8" ?>
<plugin pluginId="Gallio" recommendedInstallationPath="" xmlns="">
    <description>The heart of Gallio.</description>

    <dependency pluginId="BuiltIn" />

    <file path="Gallio.plugin" />
    <file path="Gallio.dll" />

    <!-- SNIP -->


    <assembly fullName="Gallio, Version=, Culture=neutral, PublicKeyToken=eb9cfa67ee6ab36e" codeBase="Gallio.dll" qualifyPartialName="true" />

    <service serviceId="Gallio.TestFramework"
             serviceType="Gallio.Model.ITestFramework, Gallio" />

    <service serviceId="Gallio.TestFrameworkManager"
             serviceType="Gallio.Model.ITestFrameworkManager, Gallio" />

    <!-- SNIP -->


    <component componentId="Gallio.FallbackTestFramework"
               componentType="Gallio.Model.FallbackTestFramework, Gallio">

    <component componentId="Gallio.TestFrameworkManager"
               componentType="Gallio.Model.DefaultTestFrameworkManager, Gallio">

    <!-- SNIP -->


Services and Components

Following the nomenclature used by several .Net Inversion of Control Containers, Gallio refers to its extension points as services and its extensions as components.  Services nominally represent interfaces.  Components provide implementations of those interfaces.

Internally Gallio refers to all service and component types by name using a TypeName object until it needs to instantiate it.  This is different from most .Net IoCs which immediately resolve most type names to runtime Type objects during initialization which causes most assemblies to be loaded up front.

Dependency Injection and Configuration

Gallio's plug-in model supports constructor and property dependency injection and configuration for components.

In the .plugin XML, the component parameters appear in a <parameters> element which contains an element for each constructor parameter or property that is to be bound.  If no value is specified for a given constructor parameter, then Gallio will attempt to resolve a service of the appropriate type to bind.  Otherwise if a value is specified, then Gallio will attempt to convert the provided string to an object of the required type using a DefaultObjectDependencyResolver.

The default object dependency resolver knows how to resolve all sorts of object types including strings, arrays, components, ComponentHandles, IComponentDescriptors, Versions, Guids, Icons, Images, Conditions, FileInfos, DirectoryInfos, Assemblies, AssemblySignatures, AssemblyNames, Types, TypeNames, and explicit references to other components (using the ${} syntax seen in the excerpt above.)

Gallio resolves Icons, FileInfos, and other disk-based plug-in resources using a special Uri scheme called "plugin".   For example, a component might require a reference to a file that belongs to the plug-in so it declares a constructor parameter of type FileInfo.  In the XML, it provides the path of the file relative to the plug-in base directory by specifing a parameter with a Uri like: "plugin://MyPlugin/Path/To/File.txt".  When the component is instantiated, Gallio will resolve the Uri and provide a FileInfo with the appropriate full path to the file.

Plug-in and Component Traits

Traits are collections of typed properties that Gallio uses to describe plug-ins and components.

At runtime, plug-in traits are represented by instances of the PluginTraits class which looks a bit like this:

public class PluginTraits : Traits
    public PluginTraits(string name) { ... }
    public string Name { get; }
    public string Version { get; set; }
    public Icon Icon { get; set; }
    // SNIP...

Likewise, component traits are represented by instances of the service's associated Traits type.  To establish that association, a service type may have an optional TraitsAttribute applied.

public interface MyService
    void DoSomething();

public class MyServiceTraits : Traits
    public string SomeInformation { get; set; }
    public string[] MoreInformation { get; set; }
    public Icon MyIcon { get; set; }

Traits parameters are bound just like component parameters except that they appear in the <traits> element of the ,plugin XML.

Plug-in Dependencies

In the .plugin XML, the <dependencies> element specifies the ids of other plug-ins that are required by this plug-in.

The dependency information allows Gallio to do a few useful things:

  • If a plug-in dependency is missing or disabled then all dependent plug-ins are automatically disabled.
  • Before activating a plug-in, all of its dependencies are activated.  (Note: Not implemented in v3.1 but may appear in a later version.)
  • When verifying the plug-in installation, Gallio can check that all of the services required by a plug-in are provided by that plug-in or by one of its dependencies.  This check helps developers find errors in component registrations.
Assembly References and Probing Paths

Each plug-in describes the list of assemblies that it required in the <assemblies> element of its plug-in XML.  Before attempting to use any components defined by a plug-in, Gallio first ensures that the plug-in's assemblies can be resolved.  Assembly references can also be qualified with binding redirects and other loader options.

A plug-in can also declare custom probing paths in a <probingPaths> element.

Here are a few possibilities:


    <assembly fullName="MyAssembly, Version=, Culture=neutral, PublicKeyToken=eb9cfa67ee6ab36e" codeBase="MyAssembly.dll" qualifyPartialName="true"  applyPublisherPolicy="true">
            <bindingRedirect oldVersions="" newVersion="" />

    <assembly fullName="MyAssemblyInGAC, Version=, Culture=neutral, PublicKeyToken=eb9cfa67ee6ab36e" />

Plug-in Files and Recommended Installation Path

In order to facilitate plug-in installation, each plug-in includes two pieces of information:

  • A list of files in a <files> element.
  • A recommended installation path (relative to the Gallio bin directory) in the recommendedInstallationPath attribute of the <plugin> element.

Gallio uses this information in two ways:

  • During plug-in installation it is able to determine which files belong to which plug-ins so that it can copy or delete just the necessary files.  (Note: Automatic plug-in installation is not implemented in Gallio v3.1 and will appear in a later release.)
  • On request, it checks the information to verify that all plug-ins are installed as expected and that no files are missing.  This is part of what happens when you run the "Gallio.Utility.exe VerifyInstallation" command.
Conditional Plugin Enablement

Plug-ins can include a conditional expression that specifies when the plug-in should be enabled based on characteristics of the environment.  For example, some plug-ins related to Visual Studio integration are only enabled when Gallio is hosted by the appropriate version of Visual Studio.

The conditional expression appears in the enableCondition attribute of the <plugin> element of the .plugin XML files.

Conditions are represented in the code by Condition objects.  Each Condition consists of a simple boolean expression of terms drawn from a ConditionContext and combined using logical "or".  For plug-in enablement the runtime uses the RuntimeConditionContext object to query properties in the environment.

The following condition properties are currently supported.

  • "${env:ENVIRONMENTVARIABLE}": Satisfied when the environment contains a variable called "ENVIRONMENTVARIABLE".
  • "${minFramework:NET20}", "${minFramework:NET30}", "${minFramework:NET35}", "${minFramework:NET40}": Satisfied when the currently running .Net runtime version is at least the specified version.
  • "${process:PROC.EXE}", "${process:PROC.EXE_V1}", "${process:PROC.EXE_V1.2}", "${process:PROC.EXE_V1.2.3}", "${process:PROC.EXE_V1.2.3.4}": Satisfied when the currently running process main module is "PROC.EXE" and exactly matches the specified file version components (if any).

For example, here's part of the .plugin XML file for the Gallio35 plug-in which provides .Net 3.5 specific extensions.

<plugin pluginId="Gallio35"

Runtime API

Gallio clients mainly interact with the plug-in model by way of the following types:

  • RuntimeAccessor: Accessor for the core runtime objects.
  • IRuntime: Configures and provides access to the plug-in registry, service locator, runtime logger, and other runtime parameters.
  • IRegistry: A registry of all installed plug-ins, services and components.
  • IServiceLocator: A service locator for resolving services and components provided by plug-ins.

The initialization process looks a bit like this:

  • Client code calls RuntimeBootstrap.Initialize() to initialize the runtime.
  • The runtime creates a CachingPluginLoader.
  • The caching plugin loader loads plug-in metadata into a PluginCatalog.  The metadata is represented in the catalog as PluginRegistration, ServiceRegistration, and ComponentRegistration objects.
  • The runtime augments the catalog with a few built-in services of its own.
  • The runtime creates a new Registry and populates it from the catalog.  Gallio performs many checks during this stage to ensure that plug-ins are well-formed and that all of their assemblies are accessible.  It automatically disables plug-ins that have problems.

Once the registry has been initialized, client code can use it in different ways.

  • The registry provides methods for getting IPluginDescriptor, IServiceDescriptor, and IComponentDescriptor objects.  Descriptors are used to get information about plug-ins, services and components as well as to obtain instances of components and traits objects.
  • The service locator provides methods for resolving services.  These are the same kinds of familiar operations exposed by most .Net IoCs such as Resolve, and ResolveByComponentId along with a couple novel ones like ResolveHandle and ResolveHandleByComponentId which obtain late-bound ComponentHandles.  Gallio does not use the service locator explicitly very often.  Most service location is performed implicitly by the runtime as part of dependency injection.

The ComponentHandle type is interesting in that it allows clients to obtain a typed reference to a component descriptor without resolving the component instance itself until needed.  Here is an example usage of a component handle to enumerate installed components and choose to instantiate one or more of them based on some criterion.

var frameworks = new List<ITestFramework>()
ComponentHandle<ITestFramework, TestFrameworkTraits>[]  frameworkHandles = // ...

foreach (var frameworkHandle in frameworkHandles)
    TestFrameworkTraits traits = frameworkHandle.GetTraits();
    if (IsSupportedFileType(traits.FileTypes, testFile))

Brief Comparison with Other Plug-In Models

Gallio's plug-in model is similar to the Eclipse plug-in model, Mono.Addins and MEF.

Eclipse and Mono.Addins both use XML to register plug-ins and to declare extensions points (services) and extensions (components).  Extensions can provide ample metadata in the form of XML extension configuration data.  Both of these frameworks also have provisions for packaging plug-ins as archives that can be installed incrementally.  (Of course, Mono.Addins was inspired by Eclipse so this is no surprise.)

MEF supports late-binding Exports (component handles) and makes available ExportMetadata (traits) for clients to use.  MEF is mainly attribute-based but can be configured to use external metadata files by providing an appropriate Catalog implementation.  It is quite powerful and will be included as a standard system component in the .Net Framework 4.0 release.

Aside: The original Gallio plug-in model used the Castle Windsor inversion of control container as its foundation.  Unfortunately Windsor does not support late-binding (it resolves all type names to Types on initialization) and it has limited support for component metadata.  It is a great tool (and I highly recommend it for many projects) but just not well suited to Gallio's needs.

Why Not Use Mono.Addins or MEF for Gallio?

This was a tough call.

I took a good long look at Mono.Addins and MEF beforehand.  Both are good options but I had a couple of problems with each of them.

I rejected Mono.Addins because I was not confident that I would be able to morph it into the kind of shape I needed for Gallio.  I found the code to be tightly coupled in several places that I knew I would have to change dramatically to get the kind of dependency injection and late-binding features that I wanted.

And MEF unfortunately required .Net 3.5 as a minimum whereas I had selected a target framework of .Net 2.0 for Gallio.  I briefly considered porting MEF to .Net 2.0 but gave up once I saw how much of System.Core it used.  Also it was clear that the attribute model would not work well for Gallio's late-binding needs so I would not benefit from those features.  Oh well.  I look forward to trying MEF out another day with a .Net 4.0 project perhaps.

So it just seemed more straightforward to start over which would give me a maximum of flexibility in the implementation.  As it happens, I have taken advantage of that flexibility many times over to make things better and I can say I am very happy I did not try to shoehorn Gallio into Mono.Addins or MEF.  For example, some of the plug-in metadata Gallio uses would be very awkward to implement and consume with either of these libraries.


Future Directions

Here are some things to expect from future versions of the Gallio plug-in model:

  1. A plug-in manager tool for downloading, installing, and maintaining plug-ins.
  2. A plug-in activation process to enable plug-ins to have more control over their early initialization.
  3. Extensible object dependency resolvers.
  4. More optimizations for start-up performance.
  5. Component scopes so that test frameworks can install special extensions while they run.  eg. custom object formatters for MbUnit.


Technorati Tags:


Unknown said...

Have you considered providing this IoC & plugins framework as a separate library (or at least a separate assembly), so that it could be used outside of Gallio unit-testing arena?

BTW it looks very cool

Jeff Brown said...

@Igor Brejc,
I thought about splitting up the Gallio runtime but I'm not sure how much reuse potential it really has and it would be a bit awkward to move it out to a different assembly.

However I would support anyone who decided to rip the runtime out and use it for other purposes. :-)

RC said...

We are having problem, our tests are not being recognized. We upgraded to VS2010 and using .net framework 3.5 and Gallio v3.1 (along with mbUnit). Looks like Gallio is not supported with .net 3.5, I said so as if I upgrade our projects to .net 4.0 then i could see the tests visible/active, but if use .net 3.5, it is not recognizing our existing test.

Is there any config/settings you can suggest?