The Rhino DSL project is a set of components that
turned out to be useful across many DSL implementations. It contains
classes to aid in building a DSL engine, implicit base classes,
multifile DSLs, and so on. |
Compilation
is expensive, and once we load an assembly in the CLR, we have no way
of freeing the occupied memory short of unloading the entire AppDomain.
To deal with these two problems, we need to do at least some caching up
front. Doing this on a DSL-by-DSL basis is annoying, and it would be
nice to get the cost of creating a DSL down as much as possible.
For all of those reasons, Rhino DSL provides the DslFactory class, which takes care of all of that. It works closely with the DslEngine, which is the class we derive from to specify how we want the compilation of the DSL to behave.
Again, none of this is strictly necessary. You can do
it yourself easily, if you choose to, but using Rhino DSL makes it
easier and allows us to focus on the DSL implementation instead of the
compiler mechanics.
We’ve already looked at the BaseScheduler class. Now let’s take a peek at the SchedulingDslEngine class. Listing 1 shows the full source code of the class.
Listing 1. The implementation of SchedulingDslEngine
public class SchedulingDslEngine : DslEngine
{
protected override void CustomizeCompiler(
BooCompiler compiler,
CompilerPipeline pipeline,
string[] urls)
{
pipeline.Insert(1,
new ImplicitBaseClassCompilerStep(
typeof (BaseScheduler),
"Prepare",
// default namespace imports
"Rhino.DSL.Tests.SchedulingDSL"));
}
}
|
As you can see, it doesn’t do much, but what it does do is interesting. For now, keep in mind that Boo allows you to move code around during compilation, and the ImplicitBaseClassCompilerStep does that.
The ImplicitBaseClassCompilerStep will create an implicit class that will derive from BaseScheduler. All the code in the file will be placed in the Prepare derived method. We can also specify default namespace imports. In listing 1, you can see that we add the Rhino.DSL.Tests.ShedulingDSL
namespace. This namespace will be imported to all the DSL scripts, so
we don’t have to explicitly import it. VB.NET users are familiar with
this feature, using the project imports.
We’re nearly at the point when we can execute our DSL. The one thing that’s still missing is the DslFactory intervention. Listing 2 shows how we can work with that.
Listing 2. Executing a Scheduling DSL script
//initialization
DslFactory factory = new DslFactory();
factory.Register<BaseScheduler>(new SchedulingDslEngine());
//get the DSL instance
BaseScheduler scheduler = factory.Create<BaseScheduler>(
@"path/to/ValidateWebSiteUp.boo");
//This is where we run the code from the DSL file
scheduler.Prepare();
//Run the prepared scheduler
scheduler.Run();
|
First, we initialize the DslFactory, and then create and register a DslEngine
for the specific base type we want. Note that you should only do this
once, probably during the startup of the application. This usually means
in the Main method in console and Windows applications, and in Application_Startup in web applications.
We then get the DSL instance from the factory. We pass both the base type we want (which is associated with the DslEngine
that we registered and the return value of this method), and the path
to the DSL script. Usually this will be a path in the filesystem, but I
have seen embedded resources, URLs, and even source control links used.
Once we have the DSL instance, we can do whatever we
want with it. Usually, this depends on the type of DSL it is. When using
an imperative DSL, I would tend to call the Run() or Execute() methods. With a declarative DSL, I would usually call a Prepare() or Build() method, which would execute the code that we wrote using the DSL, and then I would call the Run() or Execute()
method, which would take the result of the previous method call and act
upon it. In more complex scenarios, you might ask a separate class to
process the results, instead of having the base class share both
responsibilities.
In the case of the Scheduling DSL, we use a declarative approach, so we call the Prepare() method to get whatever declarations were made in the DSL, and then we run the code. The Run() method in such a DSL will usually perform some sort of registration into a scheduling engine.
And that’s it—all the building blocks that
you need to write a good DSL. We’re going to spend a lot more time
discussing all the things we can do with DSLs, how we can integrate them
into real applications, and version, test, and deploy them, but you
should now have an overall understanding of what’s involved.