|
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:
- 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.
- 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:
- Length of param names and values would be better as dynamic.
- 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,
};
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:
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.
|