If you’re not in the Java world, you may not have run into Eclipse (http://www.eclipse.org). I highly recommend checking it out – it’s a great project full of interesting ideas. One of the main features of Eclipse is its plug-in architecture. In fact, apart from a base runtime, everything in Eclipse is a plug-in. One set of plug-ins turn Eclipse into a Java IDE. Another set of plug-ins turn it into a C/C++ IDE.
How does this relate to Suneido? I’m not suggesting we turn Suneido into a universal IDE like Eclipse. But I do think that Suneido could benefit greatly from a plug-in architecture like Eclipse. As some of you have discovered, unlike conventional IDE’s like Visual C++, it’s quite possible to modify or extend Suneido’s IDE (and other components). This is normally done by deriving from, or overriding portions of the standard library. However, these changes are not always easy. Often you have to copy large sections of code to your custom library, or modify the standard library to add “hooks” for your code. Often, there are compatibility issues with new releases of Suneido. These issues certainly don’t encourage experimentation and contributions the way we’d like.
The basic idea behind a plug-in architecture like Eclipse is that you have a way to define “plug-ins”. Each plug-in can define “extension points” which other plug-ins can contribute to, and “contributions” which the plug-in is adding to other plug-in’s extension points. An interesting twist is that plug-in’s can contribute to their own extension points. A plug-in registry lets you get the set of contributions for a given extension point of a plug-in.
In Eclipse, plug-in’s are defined in XML in a file called plugin.xml in the plug-in’s directory. In Suneido, it makes more sense to define plug-in’s as objects in library records. For example, I converted Suneido’s import/export facility so formats are contributed to extension points. Here is the plug-in definition:
Plugin_ImportExport
#( ExtensionPoints: ( ("import_formats"), ("export_formats") ) Contributions: ( (ImportExport, export_formats, name: "Comma separated", impl: ExportCSV) (ImportExport, export_formats, name: "Tab delimited", impl: ExportTab) (ImportExport, export_formats, name: "XML", impl: ExportXml) (ImportExport, import_formats, name: "Comma separated", impl: ImportCSV) (ImportExport, import_formats, name: "Tab delimited", impl: ImportTab) (ImportExport, import_formats, name: "XML", impl: ImportXml) ) )
The “Plugin_” prefix on the name is what identifies this as a plug-in definition. During initialization, the plug-in registry scans the libraries for names starting with this prefix. When referenced in plug-in definitions the “name” of the plug-in does not include this prefix. Two extension points are defined: “import_formats” and “export_formats”. Then, contributions to the same plug-in (“ImportExport”) are listed, three export formats and three import formats. The import/export code could simply “build-in” these standard formats, but it’s easier and simpler if they are contributed the same way additional formats would be. (It also ensures that you’ve corrected implemented the contribution mechanism.)
In the contributions, the first value is the name of the plug-in you want to contribute to (in this case ImportExport) and the second value specifies the extension point. (The plug-in registry verifies that the plug-in exists and has that extension point.) Any additional values in the contribution are specific to the extension point.
How is this information used? There no magic to it. It doesn’t get used “automatically” – code has to be written to handle the contributions. However, it’s not that hard. Let’s look at GlobalExportControl. The first step is to replace the hard coded list of formats with a list obtained from the plug-in definitions. To do this, we change:
(ChooseList ('Comma separated', 'Tab delimited', 'XML'), selectFirst:, name: format)
to:
(ChooseList listField: export_formats, selectFirst:, name: format)
Then we can define a rule to return the formats:
Rule_export_formats
function () { return Plugins().Contributions("ImportExport", "export_formats").Map!({|x| x.name }) }
Plugins() gets us the plug-in registry. Contributions(“ImportExport”, “export_formats”) returns a list of the contributions to that extension point (gathered from all the plug-in definitions from all the libraries in use). We then use Map! to convert from a list of objects to a list of names.
Next, we have to use the class specified in the contributions (impl:) to perform the export. Here is a simplified version.
On_OK() { data = .Data.Get() for c in Plugins().Contributions("ImportExport", "export_formats") if c.name is data.format fn = c.impl.Eval() fn(.query, data.file, header: data.header) .Window.Result(true) }
We get the list of contributions, find the one selected, and Eval the specified class (to convert from a string to the actual class), and call it. And the code is actually smaller than before since we don’t have to have a chain of if-else’s to handle each format.
The big benefit of this, is that now anyone can add additional formats without touching the Global Export code at all. To do this, you’d simply write your export class and then create a plug-in definition to contribute it. For example:
Plugin_MyExport
#( Contributions: ( (ImportExport, export_formats, name: "My Format", impl: MyExport) ) )
The name isn’t that important since we’re not defining any extension points for others to add to. However, plug-in names do have to be unique so it’s a good idea to add some kind of prefix to your plugin names (such as your initials). (“MyExport” is probably not a good name!)
After adding or modifying plug-in definitions we need to “reset” the registry so it sees the changes:
Plugins().Reset()
That’s all there is to it. Global Export will now include your format. Very cool!
The only part we haven’t covered is the implementation of the plug-in registry itself. Here’s the entire code:
class { CallClass() { if not Suneido.Member?('Plugins') Suneido.Plugins = new Plugins return Suneido.Plugins } New() { .Reset() } Reset() { // build the plugin registry .plugins = Object() .extenpts = Object() .contribs = Object().Set_default(Object()) .get() .check() } get() { .ForeachPlugin() { |x| name = x.name.Replace("Plugin_", "") .plugins.Add(name) plugin = x.text.Compile() if plugin.Member?('ExtensionPoints') .extenpts[name] = plugin.ExtensionPoints if plugin.Member?('Contributions') for c in plugin.Contributions .contribs[c[0]].Add(c.Copy().Add(name, at: 'from')) } } ForeachPlugin(block) { query = '(' $ Libraries().Join(' union ') $ ')' $ ' where name > "Plugin_" and name < "Plugin_~"' QueryApply(query, block) } check() { for plugin in .contribs for contrib in plugin .check1(contrib) } check1(contrib) { id = contrib[1] plugin = .extenpts[contrib[0]] Assert(plugin.HasIf?({|e| e[0] is id})) } Plugins() { return .plugins } Contributions(plugin, extenpt = false) { return .contribs[plugin].Filter( {|c| extenpt is false or c[1] is extenpt }) } }
I won’t explain the details of this code – it’s fairly straightforward. (But questions are welcome in the User Group.) The point is what a powerful tool this simple idea provides. And even better, it can be applied a little at a time. Look for more use of plug-ins in future releases of Suneido. (And other ideas borrowed from Eclipse.)
Note: This code above is still in its early stages. It has changed a lot as I’ve developed it, and it will likely continue to evolve.
References
Eclipse Platform Technical Overview – http://www.eclipse.org articles
Notes on the Eclipse Plug-in Architecture – http://www.eclipse.org articles
The Java Developer’s Guide to Eclipse, Shavor, D’Anjou, Fairbrother, Kehn, Kellerman, McCarthy, Addison-Wesley, 2003
Contributing to Eclipse, Kent Beck & Erich Gamma, upcoming book from Addison-Wesley, draft available at http://groups.yahoo.com/group/contributingtoeclipse