InFact
Interpreter and factory for easily creating C++ objects at run-time
|
InFact makes it easy to construct C++ objects at run-time, using a simple-yet-powerful language that is almost identical to C++.
To build and install, run the following command sequence:
Requirements:
autoconf
2.68 or higher automake
1.11 or higher Additional requirements are checked by the supplied configure
script.
To build the InFact package, you must first run the supplied configure
script. Please run
to see common options. In particular, you can use the –prefix
option to specify the installation directory, which defaults to /usr/local/
.
After running ./configure
with any desired options, you can build the entire package by simply issuing the make command:
Installation of the package is completed by running
Finally, there are a number of additional make targets supplied “for free” with the GNU autoconf build system, the most useful of which is
which cleans the build directory and
which cleans everything, including files auto-generated by the configure
script.
Executables are in the bin
subdirectory, and a library is built in the lib
subdirectory. There are a few unit test executables, one of which is very useful for trying out InFact:
The main class of this library is called Interpreter. This class knows how to read assignment statements from any input stream (string or file) and dynamically construct objects that have been registered using special macros. The Factory class is the class responsible for constructing these objects on the fly; see the Factory::CreateOrDie method for the gory details.
In order to have the data members initialized for Factory-constructible objects, one must implement a method called RegisterInitializers
(see the empty definition in the FactoryConstructible class; for convenience, most concrete Factory-constructible implementations derive from this class). You can find some examples defined in the files example.h and example.cc.
You can try out typing assignment statements to the interpreter using the test executable bin/interpreter-test
, built automatically when you run make
.
There’s a famous quotation of Philip Greenspun known as Greenspun’s Tenth Rule:
This statement is remarkably true in practice, and no less so here. C++ lacks convenient support for dynamic object instantiation, but the InFact Framework uses a Factory class and a C++-style (yet simple) syntax.
To motivate the C++-style syntax used by the InFact Framework’s Factory class, let’s look at a simple example of a C++ class Person
and its constructor:
As you can see, the Person
class has three data members, one of which happens to be an instance of another class called Date
. In this case, all of the initialization of a Person
happens in the initialization phase of the constructor—the part after the colon but before the declaration phase block. By convention, each parameter to the constructor has a name nearly identical to the data member that will be initialized from it. If we wanted to construct a Person
instance for someone named “Fred” who was 180 cm tall and was born January 10th, 1990, we could write the following:
If Person
were a Factory-constructible type in the InFact Framework (and it is: see example.h), we would be able to specify the following as a specification string to tell the Factory how to construct a Person
instance for Fred:
As you can see, the syntax is very similar to that of C++. It’s kind of a combination of the parameter list and the initialization phase of a C++ constructor. Unfortunately, we can’t get this kind of dynamic instantiation in C++ for free; we need some help from the programmer. However, we’ve tried to make the burden on the programmer fairly low, using just a couple of macros to help declare a Factory for an abstract base class, as well as to make it easy to make that Factory aware of the concrete subtypes of that base class that it can construct.
Every Factory-constructible abstract type needs to declare its factory via the IMPLEMENT_FACTORY macro. For example, since the InFact Framework uses a Factory to construct concrete instances of the abstract type Animal , the line
appears in the file example.cc
. (It is unfortunate that we have to resort to using macros, but the point is that the burden on the programmer to create a factory is extremely low, and therefore so is the risk of introducing bugs.)
By convention every Factory-constructible abstract type defines one or two macros in terms of the REGISTER_NAMED macro defined in factory.h to allow concrete subtypes to register themselves with the Factory, so that they may be instantiated. For example, since the Animal class is an abstract base class in the InFact Framework that has a Factory, in example.h you can find the declaration of a macro, REGISTER_ANIMAL. The Cow class is a concrete subclass of Animal, and so it registers itself with Factory<Animal> by having
in cow.cc
. That macro expands to
which tells the Factory<Animal> that there is a class Cow
whose “factory name” (the string that can appear in specification strings—more on these in a moment) is "Cow"
and that the class Cow is a concrete subclass of Animal , i.e., that it can be constructed by Factory<Animal>
, as opposed to some other Factory
for a different abstract base class.
Every Factory-constructible abstract type must also specify two methods, a RegisterInitializers(Initializers&)
method and an Init(const string&)
method. Both methods are guaranteed to be invoked, in order, just after construction of every object by the Factory. To reduce the burden on the programmer, you can derive your abstract class from FactoryConstructible , which implements both methods to do nothing. (All of the abstract base classes that can be constructed via Factory in the InFact Framework already do this.) For most concrete subtypes, most of the work of initialization is done inside the factory to initialize registered data members, handled by the class’s RegisterInitializers(Initializers&)
method. The implementation of this method generally contains a set of invocations to the various Add
methods of the Initializers class, “registering” each variable with a name that will be recognized by the Factory when it parses the specification string. When member initializations are added to an Initializers instance, they are optional by default. By including a third argument that is true
, one may specify a member whose initialization string must appear within the specification. If it does not contain it, a runtime error will be raised.
For completeness, post–member-initialization may be performed by the class’s Init(const string &)
method, which is guaranteed to be invoked with the complete string that was parsed by the Factory. The code executed by a class’ Init(cosnt string &)
method is very much akin to the declaration phase of a C++ constructor, because it is the code that gets executed just after the members have been initialized.
For example, Animal instances are Factory-constructible, and so the Animal class ensures its concrete subclasses have a RegisterInitializers method and an Init method by being a subclass of infact::FactoryConstructible. As we saw above, Cow is a concrete subtype of Animal . That class has two data members that can be initialized by a factory, one required and one optional. To show you how easy it is to “declare” data members that need initialization, here is the exact code from the Cow::RegisterInitializers method:
The above code says that the Cow has a data member name_
, which happens to be an string
, that is required to be initialized when an Cow instance is constructed by a Factory, and that the name of this variable will be "name"
as far as the factory is concerned (i.e., no underscore character, which is the convention). It also says that it has a data member age_
, which happens to be of type int
, whose factory name will be "age"
, and that is not required to be present in a specification string for an Cow.
As of InFact v1.0.6, there are now some macros to make it even easier and more readable when registering parameters for initialization. The above code can now be written as follows:
Please see the documentation for the INFACT_ADD_PARAM, INFACT_ADD_REQUIRED_PARAM and INFACT_ADD_TEMPORARY macros for more information.
As we’ve seen, the language used to instantiate objects is quite simple. An object is constructed via a specification string of the following form:
where RegisteredClassName
is the concrete subtype’s name specified with the REGISTER_NAMED macro (or, more likely, one of the convenience macros that is “implemented” in terms of the REGISTER_NAMED macro, such as REGISTER_ANIMAL or REGISTER_PET_OWNER). The comma-separated list inside the outermost set of parentheses is the set of member initializations, which looks, as we saw above, intentionally similar to the format of a C++ constructor’s initialization phase. The names of class members that can be initialized are specified via repeated invocations of the templated Initializers::Add method. There is essentially one Add
method per primitive C++ type, as well as an Add
method for Factory-constructible types.
If you love Backus-Naur Form specifications, please see the documentation for the Factory::CreateOrDie method for the formal description of the grammar for specification strings.
To continue our example with Cow , the following are all legal specification strings for constructing Cow instances:
As you can see, the order of member initializers is not important (because each has a unique name), and you can optionally put a comma after the last initializer. The following are illegal specification strings for Cow instances:
In the first two cases, the specification strings are missing the required variable name
, and in the final case, the optional age
member is being initialized, but with an string
literal instead of a int
literal.
Here is a template illustrating how one creates a Factory for an abstract base class called “Abby
” and declares a concrete subtype “Concky
” to that Factory.
abby.h
abby.cc
concky.h
concky.cc
Finally, here is an example showing how you can construct an arbitrary set of C++ objects at run-time without having to touch C++, using the toy example classes that are declared in example.h
:
pet-owners.infact
my-pet-application.cc
So what about Greenspun’s Tenth Rule? Well, the idea that initialization strings can themselves contain specification strings suggests that there is a full-blown language being interpreted here, complete with a proper tokenizer and a recursive-descent parser. There is. It is a simple language, and one that is formally specified. To the extent that it mirrors the way C++ does things, it is not quite ad hoc; rather, it is (close to being) an exceedingly small subset of C++ that can be executed dynamically. We hope it is not bug-ridden, but we’ll let you, the user, be the judge of that.