Gnofract 4D Internals
This section explains how Gnofract 4D is structured. You don't need to know any of this to use the program, but it may come in handy if you want to change it or contribute to its development (which you're heartily encouraged to do).
Gnofract 4D is implemented primarily in Python, with some C++
extensions. Extensive use
is made of Python unittest framework to keep everything working - each
Python file foo.py
is accompanied by
test_foo.py
, which contains unit tests for that
file's features. 'test.py' for each folder runs all of the tests.
Source Code Layout
The important directories in the source are:
Directory | Contents |
---|---|
| This contains all the non-GUI-related, relatively
platform-independent parts of the code. This is in case it ever needs
to be ported to another environment (eg run on a server without a GUI
as part of a cluster). Most of the files here are parts of the
compiler (see below). The main class which represents a fractal is in
|
| This contains the C++ extension code which is compiled
to produce |
| This contains the python code which implements the
GUI. It uses PyGTK as the GUI toolkit. The earliest PyGTK we support
is 1.99, which has some annoying incompatibilities with newer PyGTK's
like 2.4. I need to work out whether to ditch the older library
altogether or try to come up with some wrappers to hide the
differences. Basically there's one class per dialog or custom control,
and a few other for utility purposes. The central class is
|
| This contains the C code which implements the fract4dguic.so extension. This only has one minimal function, to obtain gconf settings. |
Compiler
The most complicated part of Gnofract 4D is the compiler. This takes as input an UltraFractal or Fractint formula file, and produces C code. We then invoke a C compiler (eg gcc) to produce a shared library containing code to generate the fractal which we dynamically load.
The UltraFractal manual is the best current description of the formula file format, though there are some UltraFractal features which are not yet supported. You can download it from here.
The implementation is based on the outline in Modern Compiler Implementation in ML: basic techniques (Appel 1997, Cambridge). It doesn't do any optimization at this point, leaving that to the C compiler used as a back-end. It would be worthwhile to do some simple optimization (eg constant-folding, removing multiplication by 1.0) because the C compiler refuses to do this to floating point numbers.
Overall structure: The PLY package
is used to do lexing and SLR parsing - it's in
lex.py
and
yacc.py
. fractlexer.py
and
fractparser.py
are the lexer and parser
definitions, respectively. They produce as output an abstract syntax
tree (defined in the Absyn
module). The
Translate
module type-checks the code,
maintains the symbol table (symbol.py
) and
converts it into an intermediate form (ir.py
).
Canon
performs several simplifying passes on
the IR to make it easier to deal with, then
codegen
converts it into a linear sequence of
simple C instructions. stdlib.py
contains the
'standard library' of mathematical functions, like cosh(z). It's at
this point that complex and hypercomplex variables are expanded out
into pairs of floating point numbers - the C code is oblivious to the
complex numbers. Finally we invoke the C compiler to convert to a
native code shared library.
At runtime the different phases happen at different times. First, the entire .frm file is lexed and parsed. Then when a particular formula is selected, it's translated and syntax-checked. The actual code is only generated just before the fractal is drawn. This phase is repeated whenever the function parameters are changed (eg @fn1 is set to 'cosh').
Probably the ugliest part of the code is the handling of parameters. Numeric parameters like floats are passed in as an array, and the C++ code and Python code need to collaborate to work out which indices into this array correspond to which params- this is done by sorting them into alphabetic order. In general this area is a bit of a mess.
Threading
One of the weirder parts of the code is how we deal with threading. Basically we want the calculation of the fractal to happen on a different thread (or multiple threads for SMP) from the main UI, so you can interrupt at any point. This is complicated by the fact that Python only allows a single thread in the Global Interpreter Lock, and that PyGTK is often compiled by Linux distribution vendors without thread support, meaning this lock is not released when running the GTK main loop.
The way out of this is that the additional threads live only in the
C++ code, where they are invisible to the Python code and GTK. When
pycalc
is called with async=True, it spawns a
thread to do the calculation, which may in turn spawn more workers if
we want multiple threads. These all write to the image buffer and
report back what they're doing by writing messages into a pipe. This
pipe is added to the list of things the GTK main loop monitors, so
whenever a new message appears we get a callback into the gtkfractal
code, interleaved with the normal GTK events. We can interrupt a
calculation in progress by setting a var which the calculation threads
check frequently - they then abandon their work and quit.
Warning
Multiple threads and C++ exceptions do not coexist well, at least on some of the libstdc++'s that Gnofract 4D runs with. So the C++ code can't throw exceptions or very odd things including crashes will happen.