tl;dr
Using D's getopt standard library and Object.factory, I've made it easy to choose between multiple programs from a single executable.
Moving on to the next demo
The next OpenGL topic will most likely be about perspective, which a spinning multi-colour tetrahedron is not terribly helpful with. But I'm pretty happy with that (and the tearing will remind me to get back to that later), so I want to start a new demo. (It will also help to see multiple programs to decide how to create a small framework and set of library functions to simplify building new ones.)
Up to now, the source/app.d (the default entry-point used by dub
) was really
simple - just five lines of code:
module app; import abandonedtemple.demo1; void main() { Demo1 d = new Demo1(640, 480, "Hello"); d.run(); }
What I want to do instead is allow the demo to be chosen on the command line,
and to instantiate that demo and run it. I remembered reading about
Object.factory
in The D Programming Language, so at least I knew it would be
possible.
Object.factory
First step was just instantiating the existing demo:
auto o = Object.factory("abandonedtemple.demo1.Demo1");
That surprisingly (at the time) did not work. It didn't take long to realize why - the only constructor expected a bunch of parameters. So I shoved in a default constructor that called the original one with default values, and that worked fine.
Command line arguments
Next step was getting the demo to run from the command line. Instead of no
arguments, main
now needs to take an array of strings:
void main(string[] args) { }
I ran into the dub
"pass on the rest of the arguments to the executable"
argument separator earlier, so I used it:
dub -- --demo=demo1
Printing that out, the arguments were:
["./abandonedtemple", "--demo=demo1"]
std.getopt
I also recalled std.getopt
existed but not much about it, and not having the
book on me, I visited dlang.org and found
std.getopt.
Besides a bit of sadness about the way single-letter arguments work (-i5
is
the only way short options work with values - -i 5
does not), it has a nice
user experience.
Here's the example code from the module documentation:
import std.getopt; string data = "file.dat"; int length = 24; bool verbose; enum Color { no, yes }; Color color; void main(string[] args) { getopt( args, "length", &length, // numeric "file", &data, // string "verbose", &verbose, // flag "color", &color); // enum ... }
As you can see, there's no messing with type descriptions in the call to getopt - the referenced variable tells getopt what type is expected, including enums and arrays and even maps and custom functions. Another potential missed opportunity here is auto-generating help.
The new entry-point
Anyway, with that help, here's the new entry-point source/app.d
:
module app; import std.stdio : writefln; import std.getopt : getopt; import abandonedtemple.demos.base; void main(string[] args) { auto demo = "demo1"; getopt(args, "demo", &demo); auto demo_classname = "abandonedtemple.demos." ~ demo ~ ".Demo"; auto o = Object.factory(demo_classname); if (o) { (cast(DemoBase)o).run(); return; } writefln("Could not find specified demo: %s", demo); }
I take the demo name from the command line (or default to "demo1" for now), and
construct the class name from that. By convention all demos will live in
abandonedtemple.demos
and have a Demo
class. I created a base interface
(just void run()
) that they all implement so that I can cast to that and run
the demo from here.
Putting it together
Here is the state of my code repo at this point. A later commit added a bit of documentation.