1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
|
=============================================
Cog: A Code Generation Tool Written in Python
=============================================
:Category: Business
:Keywords: cpython, code generation, utility, scripting, companion language
:Title: Cog: A Code Generation Tool Written in Python
:Author: Ned Batchelder
:Date: $Date: 2004/05/25 21:12:37 $
:Websites: http://www.nedbatchelder.com/
:Website: http://www.kubisoftware.com/
:Summary: Cog, a general-purpose Python-based code generation tool, is used to speed development of a collaboration system written in C++.
:Logo: images/batchelder-logo.gif
Introduction
------------
`Cog`__ is a simple code generation tool written in Python. We use it or its
results every day in the production of Kubi.
__ http://www.nedbatchelder.com/code/cog
`Kubi`__ is a collaboration system embodied in a handful of different products.
We have a schema that describes the representation of customers'
collaboration data: discussion topics, documents, calendar events, and so on.
This data has to be handled in many ways: stored in a number of different
data stores, shipped over the wire in an XML representation, manipulated in
memory using traditional C++ objects, presented for debugging, and reasoned
about to assess data validity, to name a few.
__ http://www.kubisoftware.com/
We needed a way to describe this schema once and then reliably produce
executable code from it.
The Hard Way with C++
---------------------
Our first implementation of this schema involved a fractured collection of
representations. The XML protocol module had tables describing the
serialization and deserialization of XML streams. The storage modules had
other tables describing the mapping from disk to memory structures. The
validation module had its own tables containing rules about which properties
had to be present on which items. The in-memory objects had getters and
setters for each property.
It worked, after a fashion, but was becoming unmanageable. Adding a new
property to the schema required editing ten tables in different formats in
as many source files, as well as adding getters and setters for the new
property. There was no single authority in the code for the schema as a
whole. Different aspects of the schema were represented in different
ways in different files.
We tried to simplify the mess using C++ macros. This worked to a degree, but
was still difficult to manage. The schema representation was hampered by the
simplistic nature of C++ macros, and the possibilities for expansion were
extremely limited.
The schema tables that could not be created with these primitive macros were
still composed and edited by hand. Changing a property in the schema still
meant touching a dozen files. This was tedious and error prone. Missing one
place might introduce a bug that would go unnoticed for days.
Searching for a Better Way
--------------------------
It was becoming clear that we needed a better way to manage the property
schema. Not only were the existing modifications difficult, but new areas of
development were going to require new uses of the schema, and new kinds of
modification that would be even more onerous.
We'd been using C++ macros to try to turn a declarative description of the
schema into executable code. The better way to do it is with code
generation: a program that writes programs. We could use a tool to read the
schema and generate the C++ code, then compile that generated code into the
product.
We needed a way to read the schema description file and output pieces of code
that could be integrated into our C++ sources to be compiled with the rest of
the product.
Rather than write a program specific to our problem, I chose instead to write
a general-purpose, although simple, code generator tool. It would solve the
problem of managing small chunks of generator code sprinkled throughout a
large collection of files. We could then use this general purpose tool to
solve our specific generation problem.
The tool I wrote is called Cog. Its requirements were:
* We needed to be able to perform interesting computation on the schema to
create the code we needed. Cog would have to provide a powerful language
to write the code generators in. An existing language would make it easier
for developers to begin using Cog.
* I wanted developers to be able to change the schema, and then run the tool
without having to understand the complexities of the code generation. Cog
would have to make it simple to combine the generated chunks of code with
the rest of the C++ source, and it should be simple to run Cog to generate
the final code.
* The tool shouldn't care about the language of the host file. We originally
wanted to generate C++ files, but we were branching out into other
languages. The generation process should be a pure text process, without
regard to the eventual interpretation of that text.
* Because the schema would change infrequently, the generation of code should
be an edit-time activity, rather than a build-time activity. This avoided
having to run the code generator as part of the build, and meant that the
generated code would be available to our IDE and debugger.
Code Generation with Python
---------------------------
The language I chose for the code generators was, of course, Python. Its
simplicity and power are perfect for the job of reading data files and
producing code. To simplify the integration with the C++ code, the Python
generators are inserted directly into the C++ file as comments.
Cog reads a text file (C++ in our case), looking for specially-marked
sections of text, that it will use as generators. It executes those sections
as Python code, capturing the output. The output is then spliced into the
file following the generator code.
Because the generator code and its output are both kept in the file, there is
no distinction between the input file and output file. Cog reads and writes
the same file, and can be run over and over again without losing information.
.. figure:: images/cog-web.png
:alt: Cog's Processing Model
*Cog processes text files, converting specially marked sections of the file
into new content without disturbing the rest of the file or the sections
that it executes to produce the generated content.* `Zoom in`__
__ images/cog.png
In addition to executing Python generators, Cog itself is written in Python.
Python's dynamic nature made it simple to execute the Python code Cog found,
and its flexibility made it possible to execute it in a properly-constructed
environment to get the desired semantics. Much of Cog's code is concerned
with getting indentation correct: I wanted the author to be able to organize
his generator code to look good in the host file, and produce generated code
that looked good as well, without worrying about fiddly whitespace issues.
Python's OS-level integration let me execute shell commands where needed. We
use Perforce for source control, which keeps files read-only until they need
to be edited. When running Cog, it may need to change files that the
developer has not edited yet. It can execute a shell command to check out
files that are read-only.
Lastly, we used XML for our new property schema description, and Python's
wide variety of XML processing libraries made parsing the XML a snap.
An Example
----------
Here's a concrete but slightly contrived example. The properties are
described in an XML file::
<!-- Properties.xml -->
<props>
<property name='Id' type='String' />
<property name='RevNum' type='Integer' />
<property name='Subject' type='String' />
<property name='ModDate' type='Date' />
</props>
We can write a C++ file with inlined Python code::
// SchemaPropEnum.h
enum SchemaPropEnum {
/* [[[cog
import cog, handyxml
for p in handyxml.xpath('Properties.xml', '//property'):
cog.outl("Property%s," % p.name)
]]] */
// [[[end]]]
};
After running this file through Cog, it looks like this::
// SchemaPropEnum.h
enum SchemaPropEnum {
/* [[[cog
import cog, handyxml
for p in handyxml.xpath('Properties.xml', '//property'):
cog.outl("Property%s," % p.name)
]]] */
PropertyId,
PropertyRevNum,
PropertySubject,
PropertyModDate,
// [[[end]]]
};
The lines with triple-brackets are marker lines that delimit the sections Cog
cares about. The text between the **[[[cog and ]]]** lines is generator Python
code. The text between **]]]** and **[[[end]]]** is the output from the last run of
Cog (if any). For each chunk of generator code it finds, Cog will:
1. discard the output from the last run,
2. execute the generator code,
3. capture the output, from the cog.outl calls, and
4. insert the output back into the output section.
How It Worked Out
-----------------
In a word, great. We now have a powerful tool that lets us maintain a single
XML file that describes our data schema. Developers changing the schema have
a simple tool to run that generates code from the schema, producing output
code in four different languages across 50 files.
Where we once used a repetitive and aggravating process that was inadequate
to our needs, we now have an automated process that lets developers express
themselves and have Cog do the hard work.
Python's flexibility and power were put to work in two ways: to develop Cog
itself, and sprinkled throughout our C++ source code to give our developers a
powerful tool to turn static data into running code.
Although our product is built in C++, we've used Python to increase our
productivity and expressive power, ease maintenance work, and automate
error-prone tasks. Our shipping software is built every day with Python
hard at work behind the scenes.
More information, and Cog itself, is available at
http://www.nedbatchelder.com/code/cog
About the Author
----------------
*Ned Batchelder is a professional software developer who struggles along with
C++, using Python to ease the friction every chance he gets. A previous
project of his,* `Natsworld`__, *was the subject of an earlier Python Success Story.*
__ /success&story=natsworld
|