Outputting Code - Exploring Details of Brute-Force Generation
(Page 10 of 19 )
Brute-force code generation is the simplest way to output code. It just spits out code to an open stream or writer. Because there are many variations on streams, there are different approaches to doing brute-force code generation. The approach I’ll demonstrate uses the same metadata input file used for XSLT code generation. Instead of transforming it with XSLT, the metadata is loaded into the XML Document Object Model (DOM) where you can manipulate it for code generation.
TIP: Brute-force generation doesn’t impose a high degree of order on your code generation—it’s easy to be sloppy. Moving from manual development to the more abstract approach of code generation can be a big step, so don’t make it more difficult with obscure or inconsistent techniques. |
NOTE: I’ll outline one approach in detail here. If you’re using a variation of brute force, use this section for comparison. |
All three of the code generation mechanisms rely on streams when run through the code generation harness. This allows hashing tools to access the output stream and insert a hash marker so you can later tell if the file has been edited manually. Other than hashing, XSLT and CodeDOM code generation don’t use readers and writers. They use other .NET tools for their processing. Brute-force code generation outputs code directly to a stream, so you’ll have to be familiar with writers. Readers aren’t important in this context because you’re writing to a stream, not reading from one. See the “Understanding File Streams” sidebar for more information on working with streams.
Understanding File Streams For an understanding of streams, maybe a good place to start is Help for the Stream class. The description of the class is that it “provides a generic view of a sequence of bytes.” (see footnote 3)
That reminds me of a joke: A pilot lost in a plane low on fuel with limited visibility shouts to a guy on the roof of a building to ask, “Where am I?” The guy on the building says, “You’re in a plane.” The pilot flies around and lands at the airport. His terrified passenger asks how he found the airport. “That answer was absolutely correct and not terribly helpful, so I figured that was Redmond.” (see footnote 4)
Footnote 3. .NET 2003 Help: Description of the Stream class.
Footnote 4. I like the joke, but the Microsoft documentation actually does a decent job of covering many parts of the complex .NET Framework.
Anyway, further exploration of streams shows this description of streams to be absolutely correct, even if it doesn’t seem terribly helpful at first glance. Data— all the data you work with—is ultimately a bunch of bytes. These bytes might be in a file, an explicit memory buffer, a string, your console, and so on. The actual storage of the bytes is called the backing store, and regardless of which backing store you use, it’s still just a bunch of bytes.
Streams are pipes into the backing store. Streams are named in terms of the backing store they lead to, such as FileStream or MemoryStream. Streams are pipes, so they don’t themselves hold anything—although in some cases, such as the MemoryStream, the backing store is somewhat hidden behind the stream. Exactly what you stick into a stream depends on the encoding it supports. Like real-world pipes, a stream can generally transfer information in either direction.
.NET also provides fittings for the end of the pipes called readers and writers. These fittings are specific to either reading or writing, like a spigot with a backflow fitting. Readers and writers don’t depend on where the data is going—in other words, they don’t care what’s on the other end of the pipe. Readers and writers provide special features you can use to get data pushed into or pulled out of the stream. They provide specialized syntax for the type of data you’re converting to or from bytes. This can be as simple as Unicode encoding or as complex as an interface that understands HTML (such as the System.Web.UI.HTMLTextWriter). For XSLT and CodeDOM generation, the stream is just returned from the process and passed on to the hash tools.
NOTE: There’s more on using hashing code output in Chapter 5. |
The hash tools create a reader and a writer to work with the stream. Because it’s inserting something into the stream, it reads from one stream and outputs to another. This is analogous to adding dye to a stream. You’d run water from the tap into some sort of a mixing bucket, and then you’d pour it into another pipe leading to what you were doing with the dyed liquid. You wouldn’t try to stuff the dyed liquid back into the pipe from which the clear liquid came. The full ApplyHash routine is as follows:
Public Shared Function ApplyHash( _
ByVal inStream As IO.Stream, _
ByVal commentText As String, _
ByVal commentStart As String, _
ByVal commentEnd As String) _
As IO.Stream
Dim s As String
Dim reader As New IO.StreamReader(inStream)
Dim writer As New IO.StreamWriter(New IO.MemoryStream)
Dim hashstring As String
Dim fullHeaderMarker As String = commentStart & HeaderMarker & commentEnd
inStream.Seek(0, IO.SeekOrigin.Begin)
s = StripHeader(reader.ReadToEnd, fullHeaderMarker)
hashstring = CreateHash(s)
writer.WriteLine(fullHeaderMarker)
writer.WriteLine(commentStart & commentEnd)
writer.WriteLine(commentStart & commentText & commentEnd)
writer.WriteLine(commentStart & commentEnd)
writer.WriteLine(commentStart & HashMarker & hashstring & HashMarker & _
commentEnd)
writer.WriteLine(fullHeaderMarker)
writer.Write(s)
writer.Flush()
writer.BaseStream.Seek(0, IO.SeekOrigin.Begin)
Return writer.BaseStream
End Function
This function also shows how you can access the underlying stream for a reader or writer and how you can use the Seek method to reset the position within the file.
Initially streams might seem nonintuitive, but they provide a granular approach to byte processing. In the ApplyHash routine, it makes no difference what type of stream the data comes from. Another version of this routine could take the output stream as a parameter, making the task of applying the hash independent of the backing store for the data coming in or passed back out.
Using the IndentTextWriter
Although there are numerous writers within .NET that you could use for brute-force code generation, the best one is snuggled inside the CodeDOM.Compiler namespace. The IndentTextWriter manages indenting spaces for you. You can easily increase and decrease the indent of the output code using the Indent property:
Dim stream As New IO.MemoryStream
Dim inwriter As New CodeDom.Compiler.IndentedTextWriter( _
New IO.StreamWriter(stream))
inwriter.Indent += 1
You can also skip indenting with the WriteLineNoTabs method:
inWriter.WriteLineNoTabs("#Region " & DQ & regionName & DQ)
TIP: You’ll be outputting a lot of double quotes; creating a constant for them with a short name such as DQ will save some typing. |
NOTE: When you concatenate to strings as part of your code generation, consider using a StringBuilder rather than just concatenating pieces onto a string. .NET strings have a lot of cool qualities, but they require copying on every assignment. If you’re doing more than about six assignments that concatenate (multiple concatenations within a single assignment have no performance hit), you’ll gain performance if you append using a string builder. This performance advantage becomes significant with more than a few dozen assignments. |
This chapter is from Code Generation in Microsoft .NET by Kathleen Dollard (Apress, 2004, ISBN: 1590591372). Check it out at your favorite bookstore today.
Buy this book now. |
Next: Creating a Class >>
More .NET Articles
More By Apress Publishing