Simple Plugin System
Sizable applications are often in need of externalizing code from the main application. Often times this is done via a library or framework and it is well understood in the Flash community how to use these in our applications. What is less-understood is how we can go about delegating some parts of our applications to plugins. Read on for a simple technique to get plugins into your Flash application.
When you write a simple AS3 application, you’ll usually write it as a single SWF. It may load external assets like images, other SWFs, or videos, but your application will likely be a single SWF. I call this the one SWF solution. As your application gets more advanced and you feel the need to split out parts of it as they are more generally useful as libraries, you’ll start to compile those libraries to a SWC (which, remember, is a ZIP file containing a SWF) and then compile the library into your application SWF. This is a two SWF solution. The present article is about a plugin system which, as you might have guessed, constitutes a three SWF solution. Here is a breakdown of the three SWFs:
- App SWF – Full implementation of all of your application’s classes and interfaces
- API SWF – Dummy implementations of all the classes and interfaces plugins are allowed to use
- Plugin SWFs – Plugin-specific classes and interfaces.
The App SWF is cleanly split from the Plugin SWFs and the API SWF since it doesn’t compile against either. Instead, it simply loads Plugin SWFs and uses what it finds it has loaded.
The API SWF doesn’t depend on the App SWF or the Plugin SWFs since it too doesn’t compile against either. It is full of dummy classes whose function, variable, and constant definitions need not be specified beyond what is required to compile. For example, a function returning void
need not have any body and a function returning int
may simply have the body return 0;
. These dummy classes need not even be complete as they do not need to specify any function, variable, or constant that you don’t want plugins to know about at compile time. The point of the API SWF is simply to provide a SWC (which, again, contains a SWF) containing an interface for the plugins to compile against.
The Plugin SWFs are the only SWFs that have any dependency: they depend on the API SWF. Still, since the API SWF is a subset of the classes and interfaces in the App SWF, the plugin is largely cut off from full access to the application. It can, however, get at unspecified functions, variables, and constants at runtime runtime by using dynamic access techniques like someAPIClassObject["hiddenFunc"]();
or ApplicationDomain.currentDomain.getDefinition("SomeAppClass")
.
Let’s see how this all works with some concrete examples starting with a simple app:
Vector2.as
package { public class Vector2 { public var x:Number; public var y:Number; public function magnitude(): Number { return Math.sqrt(x*x + y*y); } } }
IHasVector.as
package { public interface IHasVector { function get vector(): Vector2; } }
App.as
package { import flash.display.*; import flash.events.*; import flash.utils.*; import flash.text.*; import flash.net.*; public class App extends Sprite { private var __logger:TextField = new TextField(); private function log(msg:*): void { __logger.appendText(msg + "\n"); } public function App() { __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onPlugin1Loaded); loader.load(new URLRequest("../plugins/Plugin1.swf")); } private function onPlugin1Loaded(ev:Event): void { var vec:Vector2 = IHasVector(ev.target.content).vector; log("Plugin 1 x=" + vec.x + ", y=" + vec.y); var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onPlugin2Loaded); loader.load(new URLRequest("../plugins/Plugin2.swf")); } private function onPlugin2Loaded(ev:Event): void { var vec:Vector2 = IHasVector(ev.target.content).vector; log("Plugin 2 x=" + vec.x + ", y=" + vec.y); } } }
As you can see, the App SWF simply loads two Plugin SWFs (Plugin1.swf and Plugin2.swf), treats them as an IHasVector
, and prints their Vector2
values. Here’s how to compile the app:
mxmlc app/App.as
Easy! Now let’s look at the classes and interfaces in the API SWF:
Vector2.as
package { public class Vector2 { public var x:Number; public var y:Number; public function magnitude(): Number { return 0; } } }
IHasVector.as
package { public interface IHasVector { function get vector(): Vector2; } }
There isn’t much code to strip out here, but you can see that the example magnitude
function in Vector2
was replaced by simply returning 0. In a real app there would be a lot more code to strip out, but this is just an example. Here’s how you compile the API SWF:
compc -sp api -is api -output api/API.swc
Here we have a couple of unusual arguments to compc
. The first, -sp api
specifies the source path of the API class and interface files. In my example environment, I named this api
. The second is the input source, which is the same as the source path. Lastly, I specify the name of the output SWC to produce. This SWC is what you give to plugin developers to compile against, as we’ll see in the next section. Let’s get right to that and take a look at a couple of trivial plugins:
Plugin1.as
package { import flash.display.*; public class Plugin1 extends Sprite implements IHasVector { private var __vector:Vector2; public function Plugin1() { __vector = new Vector2(); __vector.x = 1; __vector.y = 2; } public function get vector(): Vector2 { return __vector; } } }
Plugin2.as
package { import flash.display.*; public class Plugin2 extends Sprite implements IHasVector { private var __vector:Vector2; public function Plugin2() { __vector = new Vector2(); __vector.x = 3; __vector.y = 4; } public function get vector(): Vector2 { return __vector; } } }
The only difference here is the values stored in the vector. The plugins are, however, free to do any work they need to as their constructors will be called once loaded and they then have free reign to do whatever they’d like, including usage of Vector2.magnitude
or any other functionality exposed through the API SWC. Here’s how you compile the plugins:
mxmlc plugins/Plugin1.as -external-library-path=api/API.swc mxmlc plugins/Plugin2.as -external-library-path=api/API.swc
Specifying the external library path tells the compiler to not add any used classes from the API SWC into the Plugin SWFs. It’s saying “trust me, these will already be around when you load me”. Since your App SWF runs first and in so doing defines the actual Vector2
class and IHasVector
interface, you’re holding up your end of the deal with the compc
compiler. If you try to run the plugin outside of the App SWF, you’ll get a runtime error telling you that the IHasVector
or Vector2
class isn’t defined and your plugin will fail to run. If you run it the proper way though, you’ll get the following output:
Plugin 1 x=1, y=2 Plugin 2 x=3, y=4
In conclusion, this is a simple-to-understand and simple-to-implement plugin system for AS3 and Flash. It carries no overhead in API classes compiled into the plugins, no overhead in speed to implement a scripting language such as Lua or JavaScript, and allows you full, native AS3 speed and power in your plugins. The main drawback is that you need to define your API and that may be tedious and present a maintenance problem. However, a script could be developed to help keep these in sync similar to those scripts that generated intrinsic classes for AS2. Any volunteers?
Download the above demonstration app, API, and plugins, and build script.
#1 by Paul on October 25th, 2010 ·
What happens if these classes are part of a package structure?
#2 by jackson on October 25th, 2010 ·
It works just fine. I omitted packages for simplicity in the article. In practice I use packages without issue.
#3 by pnico on October 30th, 2010 ·
Just a few nitpicks about your post:
I think this could confuse people who are just learning how Flex/Flash projects are built. For example, you describe a project with a main app SWF that links to a pre-compiled SWC library, and call that a “two SWF solution”. I would call that a one-SWF solution, because only one SWF is loaded at runtime.
You say “The App SWF is cleanly split from the Plugin SWFs and the API SWF since it doesn’t compile against either.” This also seems confusing, because a SWF and a SWC are really two different things, despite the existence of “library.swf” inside the API SWC- I don’t know why they did that instead of just including an .abc file, but whatever. Anyway, the App SWF does compile against the API SWC, as you demonstrate in your compc commandline, and in the code since it references IHasVector.
I think it’s important to be clear about the distinction between SWCs and SWFs because I’ve seen people new to Flash get confused about that, since it’s not always explained that well that SWCs are really like static-linked libraries more than anything else, at least in practice. Maybe if you referred to the app and the API as separate “projects”, rather than “SWFs”.
Does any of that make sense? Anyway, I just found your site today and am impressed by all the performance profiling stuff you have up here. thanks-P
#4 by pnico on October 30th, 2010 ·
er, sorry – should be “as you demonstrate in your mxmlc commandline”
#5 by jackson on October 30th, 2010 ·
Glad to hear you’re enjoying the site!
I still think of compiling an app against a SWC as a “two SWF solution”. To me, the SWC is a SWF (library.swf) bundled with metadata (catalog.xml) describing it in a ZIP file. Then again, it’s perfectly reasonable to see a SWC as metadata bundled with a SWF. The point is that there are two SWFs in the project setup, but I suppose newcomers could find this confusing.
The App SWF does not compile against the API SWC as shown by the MXMLC command:
mxmlc app/App.as
. TheIHasVector
referenced is theIHasVector.as
in theapp
directory. This is perhaps the biggest downside to the approach: it requires making identically-named class/interface AS files in theapp
andapi
directories/projects. The upside is that the versions in theapi
directory can be stripped down so as to expose (at least at compile-time) less of the functionality to plugins.Thanks for your thoughts on this system. This site frequently targets the advanced ActionScript/Flash user, so it’s good to have someone around to advocate for the new user. :)
#6 by pnico on November 2nd, 2010 ·
Ah, OK – then I would offer an additional nitpick: It’s usually a bad idea to keep duplicate class files like this, IMO. Once a project reaches a certain size, it’s really asking for trouble. I would instead allow the main app and the plugins to link against the same API interfaces, either using a shared SWC or by directly linking to the same source files themselves. This way you guard against versioning problems and ensure compatibility between differing implementations of the same API. You clearly trust your own ability to not make dumb mistakes more than I trust mine!
If you really prefer including all the API source files inside each project that uses them, one compromise might be to use a package structure that lets you at least link to the same directory in SVN or whatever source control you use.
Since you’re interested in targeting the advanced user, I’ll mention I’ve had good results using flex-mojos & maven for building & testing complex projects with multiple runtime SWFs and API SWCs. It’s a bit of a pain to set up, but it forces you to version everything, and works great with CI servers like Hudson.
#7 by jackson on November 2nd, 2010 ·
“Duplicate” classes and interfaces are definitely an issue with this architecture, as I concluded the article:
There are two big mitigating factors:
magnitude
function just returns 0 in the API, but has an implementation in the app.And one big feature: you can strip down the classes and interfaces to limit plugins’ access to your application. If the plugin simply linked against your whole app or even shared real, non-stub versions of the interface classes, this wouldn’t be possible. Still, it may eliminate some confusion. I suppose this plugin system is more aimed at someone who wants to carefully control the plugin API.
Thanks for the tip about flex-mojos and maven. I’ll make a note to look into their applicability towards a plugin system.
#8 by pnico on November 2nd, 2010 ·
One last question, then: Why not have an interface IVector2 then, instead of a “stub” class which is essentially acting as an interface anyway?
#9 by jackson on November 2nd, 2010 ·
The example in the article is pretty contrived to keep it simple. I was just hoping to show how a class—not an interface—could be used by a plugin. For example,
Vector2
hasx
andy
public variables and it would be a shame to have to create aIVector2
and implement itsget x
andget y
interface functions when direct field access 1) is so much faster, 2) doesn’t require typing out those methods, 3) keeps SWF size small.There is also the case where the plugin might want to extend a class to gain all of its base functionality. For example, consider a system where you use plugins to create custom dialogs. The app would define a
Dialog
, the API would define a stub ofDialog
, and the plugins would define custom dialogs likeUserInfoDialog
andPrivacySettingsDialog
that each extendDialog
, inheriting and using its base functionality likeset modal
,set titleText
, andonClose
.#10 by pnico on November 5th, 2010 ·
Hm – I see your point about performance. I still worry about things getting hairy in certain situations with application domains, since AS3 will ignore identically-named classes loaded in a child domain, etc.
It’s also sad to see from your performance tests that 10.1 introduced a performance penalty for calls to interface functions (I guess AVM is becoming more like JVM?). I remember a long time ago doing a test on that and being pleasantly surprised that calls to interface functions used to have almost the same performance as direct calls – I still feel like interfaces are the “right” way to go in terms of code quality, but performance is performance, I guess.
Re. “2) doesn’t require typing out those methods” – interfaces require you to type out even less. Re. base classes – that’s the exact same whether you use interfaces or your “stubs-that-act-like-interfaces”, which granted you’ve found perform better nowadays, so point taken, at least when you need optimal performance.
Re. your argument about SWF size though – are you kidding me? Are you on dialup and using no graphical assets or something? :) I’ll take an extra 10k for cleaner code any day..
#11 by jackson on November 5th, 2010 ·
Since the plugins are compiled against the plugin API with
-external-library-path=API.swc
, the classes and interfaces of the plugin API are not compiled into each plugin. This keeps the plugin SWFs small and means that when the App SWF loads them there is no conflict between classes as they simply don’t exist in the plugin SWFs.By “typing out those methods”, I meant having to type out:
Rather than simply having a
public var x:Number
. That extra typing and the extra SWF size are valid concerns for some programmers, but quite minor compared to the first point: public field access is radically faster than getters and setters. See my article “Beware of Getters and Setters” where I show a 6-8x performance advantage to public fields.I still contend that this system is a very good way to get two main advantages:
And its major drawbacks:
#12 by pnico on November 5th, 2010 ·
OK just let me know when I get annoying, I could go on all day :) anyway..
Re. linking externally, I also do that with API SWCs – it works with interfaces just fine, obviously
Re control to limit API, just as true using interfaces – in fact it is arguably their primary use
Re public vars vs getters, YES. I have used classes instead of interfaces in an API solely because I needed the performance of a public var (and in other cases like Events, enum classes and classes that just hold a bunch of static consts, but that’s another topic). I generally end up including subclasses of the API class in the implementation (ie the plugin SWFs), rather than using duplicate classes. AFAIK there isn’t a performance penalty accessing public vars on a subclass, but maybe you know differently, I haven’t tested.
It would be nice if getters in an interface could be implemented as public vars in an implementation and still get the performance benefit, but maybe this breaks all kinds of rules of language/VM design; then again we’re talking about Flash here, so that boat sailed long ago and I wish they’d just allow it. Ah well..
#13 by jackson on November 5th, 2010 ·
I think I see what you mean about using interfaces: write the app however you normally would and then add interfaces to the parts of it you want to expose to plugins. For example, the app would normally have:
And then when you want to expose it to the plugins you would change it to:
And the plugins would then only have access to the parts you choose to expose via the interfaces. Do I have this right? If so, I think it’s a good way to avoid creating stub classes in the API SWF/SWC so long as:
It’s arguably a “cleaner” solution—though that term is notoriously subjective—but definitely comes with its own set of drawbacks. I suppose it’s up to the individual developer to decide, based on the individual project, which solution is preferable.
Thanks for all the comments on this article. It’s been an interesting discussion so far. :)
#14 by pnico on November 5th, 2010 ·
Just to clarify (I promise I’ll stop after this):
To do what you’re doing, my approach would be for any simple classes that will be used a lot and need fast property access, like Vector2, I’d include the whole implementation in the API (ie just type out the magnitude() function, I know it’s tedious :) ). If I needed a specialized form of Vector2 in a plugin, I’d subclass it, but if I didn’t I’d just use Vector2 from the plugin as the implementation would be available in the app SWF. Anything major where I want to control the API and isn’t performance-critical, I’d use an interface.