Before writing the plugin system, I had to think of the scope of functionality I wanted from the plugins and, as such, the limitations I want to set.
- Plugins should be detectable via set, required plugin API's they expose as public. I call this the plugin signature.
- Plugin API return values should be consistent for simplicity. For this system, all calls to plugin methods should get a boolean return value.
- Plugins should implement the entire supported plugin API due to reasons 1 and 2, even if they offer only stub implementations.
/* Simple Audio Plugin Layer. Proof-of-concept library using basic plugin architecture.
* Copyright (C) 2011 Justin Soulia <rockinup1231@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.IO;
using System.Reflection;
namespace libSAPL.Plugins
{
/// <summary>
/// Used to load and interact with SAPL plugins.
/// </summary>
public class plugin
{
private Type pluginAssemblyType;
private object pluginInstance;
private string[] SignatureComponents = {"loadFile", "playFile", "stopFile"};
/// <summary>
/// Creates a new plugin object.
/// </summary>
/// <param name="pluginFilePath">
/// The path to the plugin file in question.
/// </param>
public plugin (string pluginFilePath)
{
// Determine if the file path provided is valid.
if (File.Exists(pluginFilePath) == false)
{
throw new ArgumentException("The plugin path provided is not valid.");
}
// If good, load the plugin assembly to memory.
// An exception will be thrown if the file is not a valid assembly.
Assembly pluginAssembly = Assembly.LoadFrom(pluginFilePath);
// Use internal routines to determine if assembly is a valid plugin.
string pluginTypePath = getPluginTypeInstance(pluginAssembly);
if (pluginTypePath == null || pluginTypePath == "")
{
throw new ArgumentException("Assembly provided does not contain a valid plugin signature.");
}
}
/// <summary>
/// Provides the path to the signature methods of a libSAPL plugin.
/// </summary>
/// <returns>
/// A <see cref="System.String"/>
/// </returns>
private string getPluginTypeInstance(Assembly pAssembly)
{
string sig = "";
// Cycle through every type in the assembly testing for signature compliance.
// This could probably use some refactoring. Recursive loops :S
foreach (Type pluginType in pAssembly.GetTypes()) {
int match = 0;
// Cycle through every method of every type testing for signature compliance.
foreach (MethodInfo typeMethodInfo in pluginType.GetMethods())
{
// Determine if the method name matches one of the required components.
foreach (string component in SignatureComponents)
{
if (component == typeMethodInfo.Name)
{
// Increment the match count.
match++;
break;
}
}
}
// If we picked up any signature methods, but less than what is expected, throw
// an exception as the signature is partially there, but broken.
//
// Otherwise, if none were detected, continue searching.
if (match > 0 && match < SignatureComponents.Length)
{
throw new ApplicationException("Bad signature detected.");
}
else if (match == SignatureComponents.Length)
{
// Get the full name as the signature and break the loop.
sig = pluginType.FullName;
pluginAssemblyType = pluginType;
pluginInstance = Activator.CreateInstance(pluginAssemblyType);
break;
}
}
// Return the path.
return sig;
}
The detection code simply loops through the methods of each type in the assembly in an attempt to find a matching plugin signature. It does this by counting the number of matches it found in a type, and if the number of matches found equals the number of plugin API methods supported, then it gets the full name of the type and returns it as a means to confirm the location of the signature. It will not return anything if the signature is not found. This could certainly use some refactoring, since its kinda slow, but it works.
At this point the plugin class itself is initialized and an instance of the type with the plugin API methods would have been created. Another routine that finishes off the functionality of the class is used to invoke methods within the plugin assemblies themselves and pass arguments.
/// <summary> /// Sends a command to the plugin.
/// </summary>
/// <param name="command">
/// A plugin command to use on the plugin.
/// </param>
/// <param name="args">
/// An array of arguments to send with the command. Can be null.
/// </param>
/// <returns>
/// Returns a boolean value representing success or failure in processing.
/// </returns>
public bool sendPluginCommand(pluginCommands command, object[] args)
{
object result = null;
// Get our method.
MethodInfo mtd = pluginAssemblyType.GetMethod(Enum.GetName(typeof(pluginCommands), command));
// Get the result of the method.
try {
result = mtd.Invoke(pluginInstance, args);
} catch {
// Throw our own exception, stating an inconsistency between our interface and the plugin interface.
throw new ApplicationException("The interface requested from the plugin is not consistent with ours " +
"and has caused an exception.");
}
// Return value should always be boolean.
try {
return Convert.ToBoolean(result);
} catch {
throw new ApplicationException("Return value from the plugin was not boolean. All return values should be " +
"boolean.");
}
}
public enum pluginCommands
{
loadFile = 0,
playFile = 1,
stopFile = 2,
}
}
}
At this point interacting with the plugins is pretty straightforward. Any methods utilizing the plugin class can pick from an enum of supported plugin methods, and pass arguments as is appropriate. The method is obtained from the earlier selected type and invoked within the instance also created earlier. The method will either return a boolean value, or an exception will be thrown, citing an inconsistency not detected earlier.
Now, what does one of these plugin assemblies look like as source code?
/* SAPL Null Audio Plugin. Dummy audio plugin for SAPL.
* Copyright (C) 2011 Justin Soulia <rockinup1231@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
namespace SAPL_NullAudioPlugin
{
public class NullAudioPlugin_Main
{
public static bool loadFile(string filePath)
{
return true;
}
public static bool playFile(bool repeat)
{
return true;
}
public static bool stopFile()
{
return true;
}
}
}
Here you see one of the pitfalls of this implementation. It requires the plugin API methods to be static, which limits what the plugin methods can do at a global level. Being far more accustomed to VB.NET and modules this was my first instinct to follow.
The next step, although not necessarily required, was to create a friendlier interface for applications to use instead of passing arguments to methods directly through the plugin interface. I made a layer with Play() and Stop() methods. Play() uses loadFile() and playFile() to start up a sound or song. Stop() uses stopFile() to stop it.
/* Simple Audio Plugin Layer. Proof-of-concept library using basic plugin architecture.
* Copyright (C) 2011 Justin Soulia <rockinup1231@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using libSAPL.Plugins;
namespace libSAPL
{
/// <summary>
/// Provides a simple interface for applications to play synchronous audio via supported plugins.
/// </summary>
public class layer
{
private plugin audioPlugin;
/// <summary>
/// Creates a new layer object.
/// </summary>
/// <param name="_audioPlugin">
/// A <see cref="plugin"/>
/// </param>
public layer (plugin _audioPlugin)
{
if (_audioPlugin == null)
{
throw new ArgumentNullException("Plugin cannot be null.");
}
else
{
audioPlugin = _audioPlugin;
}
}
/// <summary>
/// Loads and plays an audio file.
/// </summary>
/// <param name="audioFilePath">
/// The file path to the audio file.
/// </param>
/// <param name="repeat">
/// If set to true, the audio will repeat. If false, the audio will not repeat.
/// </param>
/// <returns>
/// A boolean value representing the success or failure of the command.
/// </returns>
public bool Play(string audioFilePath, bool repeat)
{
bool result;
// Load the file. We need only send the audio file path for this.
result = audioPlugin.sendPluginCommand(plugin.pluginCommands.loadFile, new object[] {audioFilePath});
// Only continue if the file successfully loaded.
if (result != true)
{
return false;
}
// Play the file. We need only send the repeat value for this.
result = audioPlugin.sendPluginCommand(plugin.pluginCommands.playFile, new object[] {repeat});
// Return the result.
return result;
}
/// <summary>
/// Stops any running audio.
/// </summary>
/// <returns>
/// Returns a boolean value representing whether the audio was successfully stopped.
/// </returns>
public bool Stop()
{
// Simply stop any audio.
return audioPlugin.sendPluginCommand(plugin.pluginCommands.stopFile, null);
}
}
}
All this requires is an initialized plugin.
Conclusion
I would not recommend this as a drop in architecture for plugins. Its really not all that robust, and C# probably allows for more efficient means of accomplishing the same things than what I have written. However, it does show that it is not that hard to set such an interface up, even for a beginner.
I've attached an archive with the entire project I wrote. I did everything in Debian using MonoDevelop. I also wrote a plugin that (redundancy incoming) uses SDL.NET as an audio back-end for a more functional demonstration, and a simple GTK# application that can be used to test plugins. (I can't exactly claim it to be robustly coded, however, since I'm pretty new to GTK and its differences from Windows.Forms :D). This means you'll need the libraries to cover all these dependencies if you want to see everything work.
Any feedback is welcome.
Enjoy.
Attached Files
Edited by Grue, 19 August 2011 - 06:04 AM.
Cleaning up the code a bit. Updated attachment.


Sign In
Create Account



Back to top









