DynObj - Dynamic Binary Runtime Plugin Objects Hosted by:
SourceForge.net Logo

DynObj - A Plugin Scenario

This is an example illustrating the usefulness of a generic type based approach to plugins. In the first, initial attempt, we 'get caught' in some places where a more general solution is suitable. Then we solve the same problem using a typed approach.

Example: An Audio Filter Plugin

Suppose we want the ability to load and use 3rd party audio filter files from binary plugins.

Initial Approach

So, we define a first filter interface class:
  • class AudioFilterI {
    • virtual const char* GetFilterName( );
      virtual bool Apply( const float signal_in[], int signal_length,
      • float signal_out[], char err_buf[], int err_buf_len );

      // We also want to be able to enumerate any parameters of this filter
      virtual bool GetParamName( int param_ix, char name[], int name_len );
      virtual bool GetParamValue( int param_ix, char val[], int val_len );
      virtual bool SetParamValue( int param_ix, const char *val );
    };
Although this could work, there are several things to note:
  • The first method is safe and useful, it returns a constant string from the plugin.
  • The second method has some problems:
    1. We don't know exactly how long the signal_out array is. The assumption is maybe that it's just as long as signal_in. It's not very generic, maybe the output signal will be a bit longer than the input one.
    2. We want to be able to communicate if an error happens, using err_buf with a given length. Maybe our error message doesn't fit inside the buffer?
  • For the methods concerned with communicating parameters, we have some more issues:
    1. Length of param names and values would be better as dynamic.
    2. The methods concerned with parameters are really bit separate from the filtering method. It would deserve an interface of its own.
Handling many filter plugins, including those implemented by 3rd parties, this interface is dangerous.

Audio Filter with Generic Types

Suppose we define a generic interface for signals:
  • // %% DYNOBJ class(VObj)
    class SignalI {
    • virtual const char* GetName( );
      virtual float GetLength( );
      virtual float* GetSamples( );
      virtual bool Apply( const SignalI& signal_in, GrowableSignalI& signal_out,
      • DynStr& err_str );
    };
The first comment %% DYNOBJ class(VObj) tells the DynObj preprocessor (pdoh) that there comes a class or interface declaration next. It will then generate necessary glue code to expose our interface.

We have used the existing DynStr interface (a generic interface to a Unicode capable string class) in the variable err_str.

Then we define an interface for paramaters:
  • // %% DYNOBJ class(VObj)
    class ParamI {
    • virtual bool GetName( int ix, DynStr& name );
      virtual bool GetValue( int ix, DynStr& name );
      virtual bool SetValue( int ix, const char* val );
    };
Now, we can implement a filter plugin along these lines:
  • // %% DYNOBJ class(VObj) bases(SignalI,ParamI)
    class OurFilterI : public BasicSignal, public BasicParam {
    • virtual const char* GetName( ){ return "Our filter"; }

      // GetLength and GetSample implemented by base class BasicSignal

      virtual bool Apply( const SignalI& signal_in, GrowableSignalI& signal_out,
      • DynStr& err_str ){ /* Do filtering */ }
    };
The comment %% DYNOBJ class(VObj) bases(SignalI,ParamI) tells the DynObj preprocessor (pdoh) that we're declaring a class with 'unrooted' base classes, and to expose the interfaces SignalI, ParamI to users of the plugin.

Users of our plugin will find the 'side interface' ParamI by performing a run-time type query [do_cast<ParamI*>(U* pu)] on it.

Conlusions

By moving from an initial 'first shot' approach, to a more generic one, we achieve:
  • We get safe string handling at no expense in the plugin. It just uses the stable DynStr interface to strings implemented in the main program.
  • By moving to a general SignalI we eliminate problems around array length and resizing. We use base class implementations and need not worry about this in our class.
  • Parameter handling is distilled into a separate interface class. Filtering and paramater management are different jobs. Also here, DynStr gives us a safe and simple implementation.
In the initail approach, a plugin could not re-use any of the functionality from the main application. It was left to its own. With a type based approach, the plugin gets more choice and can also query the main application for interfaces and objects. Chances are that some of the new interfaces are useful in other situations than audio filtering. The ParamI interface is a candidate to turn into a public interface.

We should also note that once there exists binary plugins using or implementing some of these interfaces, they rely on the interfaces being stable. I.e. at that point, we cannot add or change the methods of an interface (it's should be tagged as stable if it's public).

Finally, the interface classes above are plain, easy to read C++ classes. The 'big' libraries (COM, xpCOM) use extensive C macros syntax to declare classes and methods. Following such code is more difficult, both for developers and a debugger.