Search this site
Plugin architecture with REALbasic and RBScript
A plugin is hereby defined as a program that can be added to the already-built application and executed at the app's discretion.
In the next paragraphs I will suggest ideas. I will base them on the following example:
Design for a search app, similar to how Apple's Spotlight works.
The app shall be a tool to search for text in many files. While the app has hard-coded routines to search for text in a single file, it wants to allow plugins to convert binary files into text files where appropriate. Imagine documents from applications such as Word (.doc), Acrobat (.pdf) and even REALbasic (.rbp), which, when opened in a plain text editor, show a lot of gibberish that you don't want to have searched. Instead, you want it all readable.
Hence, the plugins for this search app will basically function like this:
The app will pass each file, before looking at it, to the plugins, so that the plugins can convert the file to text if necessary. Each plugin gets to look at the file, decide whether it can understand it. If it can, it returns the readable text, which the app then searches for the contents the user wants to find.
Defining the Plugin API
The plugin will have one or more more functions that the app can call. The functions need to be well defined. This should best be written down first, into a separate document, for later reference. And it needs to be kept up-to-date all the time, of course.
Here are a few functions that should never be missed in such an API:
For our example, here are the functions it shall implement:
That's all the functions the plugin needs to provide.
Next, we define the functions the plugin can invoke to help in its task. These functions will be implemented by the app:
With these methods, a plugin can refer to a file, read its contents or pass it to a command line tool for conversion (which may be useful to convert .doc and .pdf files), or even read neighboring files in case it's a set of files that need to be read for the conversion.
Note that RBScript already offers a set of basic functions by default, such as many string functions. But apart from those, any other functions that are available to a RB application need to be explicitly made available to the plugin. This means that the plugin runs in a sandbox where it can't mess with files or other system resources unless the app allows it by providing operations for that explicitly via a custom class assigned to the Context property of a RBScript instance.
Plugin file format
It's up to you to define how you want plugins for your app be formatted so that you can identify and load them.
Basically, always use a format that allows you to have more than just one RBScript loaded. You'll probably want to have separate scripts for different tasks.
My usual solution is to use one XML file per plugin that includes not only the script code but also other attributes a plugin might want to provide, such as version, name, descriptive text, embedded pictures (base64 encoded) and so on. Optionally, you might require the plugin to be a folder, in which the main xml file resides plus additional files the plugin might need (e.g. larger picture files).
I suggest an XML with the following attributes:
<plugin internalName="...a never changing and unique name to identify this plugin..." visibleName="...what the user sees as the name for this plugin..." internalVersion="...an ever-increasing number, preferrably integer..." visibleVersion="...a text, preferrably in the common version scheme..." apiVersion="...the number of the API version used here..." > <method name="...name of a function..." args="parameter names and types, in RB syntax" returns="...a type..."> ... here goes the RBScript code for this function ... (note that you need to convert a few characters, such as "<" and ">" need to appear here as "<" and ">" or it would confuse the XML structure) </method> ... and possibly more functions as <method> tags </plugin>
Note: Declaring the arguments and return types is useful to let the app know which parameters the plugin expects. This would help, for instance if the API the plugin uses is newer than the app it is used with (shouldn't happen if the user keeps all his apps up-to-date, but it's still better to always clarify the expectations as verbose as possible in an API): The app may then supply default values to the unknown arguments.
For our example, I've decided to go a simpler way, though. All methods will be just declared in one large RbScript file, and then code is generated by the plugin loader to access either of those functions.
For simplicity, I simply assume that there is one text file ending in .rbs (for "RBScript", which Yuma uses as well), containing all RBScript code and nothing else.
A simple plugin to handle plain text files would look like this:
function CanHandleExtension (ext as String) as Boolean return ext="txt" or ext="xml" end function function ConvertToText (f as File) as String return f.ReadAll end function
Another plugin to convert Word .doc files could look like this (note: this uses a tool for Mac OS X - I don't know how to do it on Linux and Windows):
function CanHandleExtension (ext as String) as Boolean return ext = "doc" and OSPlatform() = "osx" end function function ConvertToText (f as File) as String dim path as String = f.ShellPath return SystemCall ("textutil -convert txt -stdout "+path) end function
Implementation of the API in the application
At this point, this article doesn't go much deeper for now.
I have implemented a demo application which implements what was outlined above. The app loads plugins, then takes a file and tries to operate each plugin on the file, until one succeeds.
Suspending a script waiting for input or events
A script may have to wait for an RB event to occur before it can continue.
For example, if a script wants to download a file from the internet, a called function in the Context class would start the download and then somehow have to wait until the "DownloadComplete" event or something like that is called. The script, however, wants to wait until this event has occured. The solution is to run the script in a thread, and then the downoad is started, the script is put to sleep using a Semaphore. Once the "completed" event gets called, the script is resumed with the result passed to it.
Catching runtime errors (exceptions) in executed scripts
RBScript has a few long standing bugs that prevent us from catching exceptions in scripts as it was intended (and even documented).
Fact is that when an executed RBScript raises an exception, the RBScript.RuntimeError event that's meant for this is not called. Also, scripts do not even know the class RuntimeException. Lastly, if a Context method gets called by a script, and that method raises an exception, even different rules apply as to how they can be caught.
The overall solution is to both wrap the RBScript.Run method and the script code in a try...catch handler to catch exceptions in the script.
The RBScript Plugin demo project (see above) implements this.
You can also read my presentation I made for REALWorld 2006 on this.