Using XSLT to Generate Code
The last time I talked about XSLT was quite a while ago. I was using XSLT to generate wiki markup for both my personal projects and for SlimDX. It’s worth noting that we have abandoned the wiki markup documentation in SlimDX, however — it didn’t look or operate as nice as we would have liked, and it wasn’t available offline. Anyway, this weekend I felt like exploring the technology a little more, so I decided to see if I could craft an XSL transformation that would produce (well formatted!) C# code for an object model described by XML.
I already have a custom tool called transformadf.exe to do this. It reads .adf files (which describe the physical structure of in-game assets used by my code) and produces some C# source files containing the asset class and some supporting machinery. The .NET XPath query library is used to build an in-memory description of the structure from the .adf file, and then produces the code files based that structure and heavy use of StringBuilder. There are two things I don’t like about this approach:
- Modifying the resulting code is difficult. The boilerplate portions of the code, the “template” if you will, are all contained in string literals scattered around the file. Some characters need escaping, and a particular set of simple rules need to be obeyed with respect to line endings (not Unix / DOS / Mac, but rather whether or not you end the line via
StringBuilder.AppendLine, or just use theStringBuilder.Appendmethod, which will not include the newline). Making changes to the template is more cumbersome than it should be. transformadf.exeis part of the build process for the framework code itself (because the framework includes standard asset types), and the build process for the framework’s client games. The other tools in the framework are used only by the client games. This makestransformadf.exeunique within the framework’s build system, as it can’t use the standard set of prebuild/postbuild actions (since it’s part of those actions).
Back Into the Breach
I had not really done anything with XSLT since the last time I wrote about it, so I suffered through a few false starts before I ended up with something that wasn’t entirely embarrassing. The two things I took away from this study session with the language were the ancestor selection axis and the mode attribute on templates.
The ancestor Axis
XSLT uses XPath to query for (”select”) nodes in the XML hierarchy to operate on. Part of that selection process is the notion of the axis along which you are performing the selection — you can think of this axis as a direction, but I found it easier (although less intuitive, initially) to think of the axis as specifying the set from which you will select. The default selection axis, if you don’t specify one, is the child axis. To specify an axis other than the default, you use the syntax axis::expr, where axis is the axis specifier and expr is an XPath expression.
The ancestor axis is the set of nodes between the document root and the current context node, but not the context node itself (use ancestor-or-self for that). It’s important to note that the resulting set of nodes selected from this axis is organized in document order: matching nodes closer to the document root appear first. This is why I suggested thinking of the axis as a set rather than a direction, since “direction” and “ancestor” put me in the mind of traversing the node tree backwards, “towards” the ancestors, and thus returning the nodes closes to the context node first. Until I shifted gears, I kept making stupid mistakes in my xsl:for-each operations.
The mode Attribute
For every asset description, I generate three C# class: the asset class itself, a marshaller class that handles lifetime and serialization tasks, and a reference class that allows the asset to be pointed to by other assets (and allows the asset wrangling subsystem to ensure all dependent assets are automatically loaded, if desired). This requires three passes through the source XML. My original strategy was to copy the source tree into three variables, renaming the root of each tree. Then I applied templates to each tree in turn, and the templates themselves matched nodes that were descendants of specific named roots (using the XPath selection named-root//node-name).
This let me have a template for matching marshaller-class//field that was different from asset-class//field; desirable, since I needed to do entirely different things to process fields in each of the three classes. It worked, but it felt clunky and ugly — and it was. While researching some other aspect of XSLT that had slipped my mind, I stumbled across the mode attribute that can be applied to templates and the apply-template element that allows for exactly this kind of filtering without the bloated triple copying of the source tree.
It’s quite simple: an XSL template can be given a named mode, and will only match when templates are being applied in that mode. Now I can simply write the appropriate templates and mark them with an appropriate mode, and apply-templates three times — it is much cleaner to look at.
Results
In the end, deciding whether or not to use XSLT or my hand-rolled tool was very much a “six of one, half-dozen of the other,” kind of decision. The XSLT worked, and worked well. The time I spent struggling with new parts of the language (or those I’d forgotten) was about equal to the time I spent writing the generator initially. Maintaining the generator has been expensive, but I suspect maintaining the XSLT will be equally so. I didn’t gain a lot in terms of template code expressibility in XSL, either — that is to say, it’s nearly as difficult to write or maintain the non-generated boilerplate portions of the code in XSL as it was with transformadf.exe. The difficult is simply elsewhere (in particular, in coping with whitespace so that the generated code is at least passably clean).
While I haven’t made up my mind completely, I’m leaning towards continuing to use the XSLT. For the most part things stay roughly the same, but it does provide a solution to my second problem, that of transformadf.exe being a special-case project. We’ll see what happens.
