Virtual Developer Workshop: Containerized Development with Docker
So you want to create a plugin framework for your application?
When do you begin?
To understand how plugins work is to understand a technique called late binding, and of defining a certain set of rules plugin developers must follow to work with your application framework.
To start from the beginning, late binding is the term used to define the act of dynamically creating objects at runtime and then using their methods. When you use late bound calls, all of your objects will be stored in references of the generic type "Object". If you are working in VB, you will not have any IntelliSense available to help you when calling the methods or properties, so you will have to get use to typing somewhat blind.
The following lines of Visual Basic code will dynamically create an object.
dim fso as Object set fso = CreateObject("Scripting.FileSystemObject")
Scripting.FileSystemObject is the progId.className of a common system component found on most machines. If you were to add a reference to it under the project menu, you would find it as "Microsoft Scripting Runtime."
Then you could
dim fso as FileSystemObject set fso = CreateObject("Scripting.FileSystemObject")
and you would have full IntelliSense for all of its methods.
If you are familiar with Visual Basic, you know that adding a reference is the way that you make use of external DLLs. This brings us to understanding just what we need to have a plugin work. Without going into too much detail to confuse you, I will just mention that CreateObject will Create an instance of an ActiveX Component and return its instance to you. For our plugin framework, we will use ActiveX DLLs to house our plugin Clients.
ActiveX DLLs are much like standard projects, except that they can be called and reused by other code and even from other languages. Functions in ActiveX DLLs can only be accessed through its public classes. If you have never created an ActiveX DLL, don't worry; it's easy and there are samples included.
So, let's get back to our late binding, early binding example. Early binding is fine for widely used components that you know you want to use beforehand, but it does not fit in with our need to load things dynamically at runtime.
Looking at our argument to the CreateObject function, we see that it takes one argument. This argument was further defined as being in the format of progId.ClassName, but what exactly does this mean?
In a Visual Basic DLL's project properties, the progId used is the name entered in the "Program Name" field.
This is not necessarily the same as the DLL name! When you make plugins, you have to be aware of that. The className can be any valid class that you have publicly exposed in your plugin client.
This brings us to our first general rule for our plugin framework. We are going to have to have a method of knowing the progId.Classname for any plugin we encounter without any extra configuration or input from the user so that we can call CreateObject to load it into memory.
How do we do that?
- Make sure your plugin client's "Program Name" (progId) is the same as the DLL name. By enumerating which plugin DLLs are in the /plugins directory, we can tell their progIds if they are the same as their filenames.
- For the classname, let's use something standard such as a class named, well, plugin. Although the class name you use is completely arbitrary, it has to be standardized across all of your plugins so that you can call them up without errors and interface with their functions.
Okay, we now know how to dynamically create as many plugins as we can find at runtime, but how do we integrate with them?
This brings us to the next question, "How do these plugins get called?"
A typical layout for a plugin framework is to provide a couple of menus that the plugins can add entries to. In our framework example, this is exactly what we have done.
When a plugin is first found, we create the object. Now, we have a live instance of the plugin class. For us to make use of this plugin class, we have to know what functions to call on it to have it register itself with our main program and to call to have it perform the services it has promised us. This brings us to rule
- The plugin class must have a consistent interface that we can blindly call. It has to be the same for each plugin. Furthermore, it has to let the plugin register its services with the main application, as well as provide a way for the main application to start any of the functions it promises to provide in that registration.
In our example framework, this is done through a simple plugin class composed of two Subs.
Sub SetHost(newRef as Object) Sub StartUp(myArg as integer)
After the plugin is created, the main application will call the SetHost function and pass in a reference to its own main form (or whatever object you want the plugins to have access to).
The plugin will then call a special function we are sure to implement on our main form to register the services it wants to provide to the host.
In our framework, this function in the main application is defined as
Sub RegisterPlugin(intMenu as integer,strMenuName as string, _ intStartupArg as integer)
Through the object reference passed into the setHost function, the plugin now has a way to call functions on that object. The first one that it calls is the RegisterPlugin sub. In this way, each plugin can supply as many features to the host application as they want. For each one they provide, they supply a different startup argument that will passed back to them when the user selects that entry's function.
An example of this would look like the following:
The arguments of the RegsiterPlugin Function are the minimal amount of info needed for the plugin to register itself. The first argument, intMenu, allows the plugin to specify which menu it would like this entry to show on. This is provided in case the host allows plugins to add to multiple menus.
The second argument, strMenuNames, is of course the menu text that the plugin want to appear in the new menu item.
The last argument is the startUp parameter the plugin expects to receive when this menu item is selected by the user. The reason it passes in a startup argument is so that each plugin can register multiple functions with its host.
When the plugin's functionality is selected, the host will now call the StartUp function in the plugin class and pass back this argument so the plugin can tell which of its functions was chosen.
So, now we have a working plugin framework, we have a host that can dynamically load whatever plugins it encounters, and we have a method where the plugin can register as many functions as it wants with the host, and receive its events to start them up at the proper time.
But how do plugins do anything meaningful?
Well, in the SetHost function, remember how we were able to call the RegsiterPlugin function back on the main form? With that reference to frmMain, you can access any of the public variables, functions, and objects that are on that form!
The host developer will commonly provide you with a listing of the public members' names and types for your plugin development.
Can I receive events for the controls on the host?
Sure. Let's say you wanted to access the onChange event of a textbox on the main form. In your class you could
Dim WithEvents mTextBox as TextBox
then in SetHost
set mTextBox = frmMain.txtMain
(assuming frmMain had a text box named txtMain, of course)
If the control you wanted to hook the events for was a type not natively supported in Visual Basic, you would have to add that component to your own library first.
For the last stage of the article, I want to discuss some of the samples I have included in the package. One of the beauties of plugins is that they use COM. COM, short for Component Object Model, is basically a big plugin framework of its own that lets components written in multiple languages all interoperate seamlessly.
In Visual Basic, COM is an everyday part of life; just about everything we see is a COM object. In other languages, such as C++, COM is a very specialized area and not to be taken for granted.
Because COM can work across multiple languages, and because VB supports COM, you will be quite pleased to find that plugins can be created in just about any language you want!
In this package, I have included plugin samples written in: Visual Basic, C++, and J++. You can even create plugins from scripting languages such as VBScript!
Feel free to use this generic plugin framework in your own applications. If you do, you are free to distribute these samples with your product to demonstrate how to make plugins for your application in your language of choice.
Now, you should have enough background to create your own plugin framework, and understand just how to make it work for you. Enjoy!