MMXX Overview
MMXX (pronounced "2020" as per roman numerals, or "mix", depending
on personal preference) stands for Modules and Metaclasses ++. Accordingly, MMXX is two things:
- An antidote for C++'s Fragile Base Class Problem. It allows C++
applications to be arbitrarily divided into binary code fragments
(such as a main executable, shared libraries and/or plug-in code
modules) with single-class granularity. Each fragmented component
can then be substantially revised while maintaining backward compatibility
with the components that were built prior to the revision, within
limits determined by the particular design and revision strategies
employed.
This is not possible with C++ using C-style dynamic linking alone
- a revision in a base class generally requires the recompilation
of those code fragments that refer to that class. For this reason,
MMXX's runtime linker takes over much of the work of the native
dynamic linker.
On some platforms, a major side-effect benefit of using MMXX is
enhanced compatibility between code fragments built with different
compilers.
- A "metadata/metacode gateway" (a.k.a. an "Interface Repository",
a.k.a. "Super RTTI") that makes many C++ compile-time constructs
visible at runtime. In particular, it makes class properties queryable
and methods programmatically invocable. This is especially useful
for gateway-type functions, whereby a portion of the program's
internal interfaces are to be exposed to the outside. This makes
a variety of functions easily implementable, with little or no
per-class/per-method writing costs:
- Fully generalized RPC-style gateways (e.g. Java native methods,
AppleScript, CORBA bridges, etc.)
- Initialization gateways (e.g. init script parsing tied indirectly
to accessor functions, scripted UI looks, etc.)
- "Very Intelligent Templates," or code that operates on objects
selectively depending on various class properties.
- "Reconciliation Gateways." If two APIs A and B have the same purpose,
were both designed to rely on MMXX plumbing, but have minor differences
between them, an automatic gateway can be written to reconcile
those differences programmatically.
Using MMXX: a Snapshot
While MMXX is fairly easy to learn to use and understand, it's
most often the case that legacy code can't be ported to it without
major revision. This is because MMXX only allows a subset of C++
to be used in MMXX-enabled classes. Here are some of the limitations:
- Only private member variables allowed (use accessors for everything)
- No inline functions that access variables (they would fail upon
revision)
- No template base classes for MMXX classes (the derived class would
fail upon revision)
- No virtual base classes (as in class foo : public virtual bar { };) for MMXX classes (incompatible with MMXX's cross-module inheritance
scheme, they would be shared in some cases and not shared in others
in an apparently erratic fashion.)
Note that not all classes in an MMXX application are MMXX classes - regular C++ classes are also allowed and can derive from MMXX
classes, without restrictions. MMXX classes can also have private
non-MMXX bases. In essence, only the portion of the design that
will be exposed across module boundaries and/or queried through
the metaclass interface needs to be MMXX-enabled.
In addition, MMXX requires the full integration of certain constructs
into the application's design:
- Headers that contain class declarations must be written using
the MMXX grammar. Things like:
class C : public A, public B {
/* ... */
};
become:
MMXX_CLASS_2_0(C,public,A,public,B)
/* ... */
MMXX_CLASS_END(C)
This is necessary to relay the appropriate information to MMXX
for it to generate runtime support code and data structures. Thanks
to the C preprocessor, a header file written with the MMXX grammar
can be directly #include'd as if it was a regular header.
Alternatively, the twentifier translation tool may be used to generate a MMXX header automatically
from a C++-like input header file. Using twentifier is strictly optional, since it's a simple translator rather than
an IDL compiler. This arrangement promotes maximum compatibility
with those circumstances where twentifier may not be available or desirable, such as with IDEs (integrated
development environments) that are unable to deal with special
make rules, with platforms to which twentifier has not yet been ported, or with users who would simply rather
not duplicate the number of header files involved.
- In function signatures and cross-module data structures, object
pointers and references of the form:
MyClass * |
const MyClass * |
MyClass & |
const MyClass &
|
must be replaced with cross-module object "handles" of the form:
x_ptr<MyClass> |
x_cptr<MyClass> |
x_ref<MyClass> |
x_cref<MyClass> |
respectively.
Cross-module object handles are automatically type-converted back
and forth to pointers and references, without extra syntax. Note
that once a handle has been obtained, it's usually dereferenced
into a regular pointer or reference to maximize performance (which
can then be stored in private member variables, passed to STL
methods, etc., as long as it will not cross a module boundary
without being converted back into a handle.)
- Use
object->VirtualDelete();
instead of
delete object;
This is because delete is not "virtual enough" for cross-module inheritance. Note that
objects can still be created on the stack or as member variables,
without the need for special treatment - such objects cannot possibly
be instances of subclasses declared in other modules (it's also
OK to use delete if one's certain that the object is not a subclass instance being
accessed through a base class pointer, but then again using VirtualDelete() everywhere is more consistent and less error-prone.)
- Use
bool MMXX_DynCast(x_ptr<ClassTO> &destination, const x_ptr<ClassFROM>
&source);
instead of
ClassTO *dynamic_cast<ClassTO*>(ClassFROM *source);
This is somewhat optional; dynamic_cast<> will also succeed in most circumstances. However MMXX implicitly
"extends" C++ polymorphism by making it possible to have an object
inherit from two base classes A and B without being an instance of any compiler-visible class C that derives from both. This is because the derived class C may have been defined in a module Bar that sits atop the module Foo where only A and B are defined. Yet MMXX_Dyncast(b, a) will succeed even if it's invoked from module Foo.
- To throw user-defined exceptions across module boundaries, derive
your exception classes from MMXXException and throw pointers instead of instances. In other words, use
class MyException : public MMXXException { }; // assuming twentifier
is being used
throw new MyException;
instead of
class MyException : public exception { };
throw MyException;
This way exception objects themselves are unaffected by the fragile
base class problem and can be thrown across module boundaries.
Since pointers are thrown, exception handlers should invoke the
VirtualDelete() method on MMXXException objects once they're done inspecting them, or otherwise rely
on the garbage-collection feature of the MMXXException class to do so automatically at some later time. Notice that
you may still throw instances of the standard exception classes,
such as exception, bad_alloc, runtime_error, etc., without special treatment. If you throw some other user-defined
exception, it is converted to its closest base type in the standard
library (or if it doesn't derive from exception altogether, to a bad_exception) if and when it crosses a module boundary.
Metaclass Interface Overview
The facilities in MMXX's metaclass interface are optional; your
application can rely on MMXX to address the fragile base class
problem without using them. However, frameworks, gateway-type
software and other advanced applications will potentially find
them very useful.
MMXX maintains these runtime objects to match their compile-time
equivalents:
- MMXXUnit - A container of MMXXClass'es. There is an instance of MMXXUnit for each module, including the main executable.
- MMXXClass - A MMXX-enabled class. There are MMXXClass methods to query various class properties, such as super- and
sub-classes and member MMXXSymbol's.
- MMXXSymbol - A member of a MMXX-enabled class. There are MMXXSymbol instances for methods, constructors, destructors, static functions,
member enumerations, member structures, etc. MMXXSymbol methods allow querying of symbol properties such as signatures
and enumeration tuples.
MMXXUnit, MMXXClass and MMXXSymbol are themselves implemented as MMXX-enabled classes so that MMXX
can itself be revised without suffering from the fragile base
class problem (indeed, there are MMXXClass instances for these classes also!)
While the above are high-level representations, MMXX also uses
some low-level types to refer to units, classes and objects:
- MMXXMetaunit - An opaque generic reference to a unit.
- MMXXMetaclass - An opaque generic reference to a class.
- MMXXMetaobject - An opaque generic reference to an object.
While MMXXMetaunit and MMXXMetaclass offer little additional functionality (indeed, an application
does not often use them explicitly, and when it does it will usually
ask MMXX to retrieve the pointers to their matching MMXXUnit and MMXXClass instances,) MMXXMetaobject is very significant in that it can be used to uniquely identify
any instance of a MMXX-enabled class.
Like an object pointer (and like an x_ptr<>), a MMXXMetaobject variable identifies a class along with an instance (so that two
different values for the variable can indeed refer to the same
object.) Unlike a pointer, an MMXXMetaobject is not bound to a particular base class. Yet it's possible to convert from
a MMXXMetaobject variable to an x_ptr<C> and vice versa, as long as C is a base class for the object in question, as follows:
x_ptr<C> p, q;
p = new C; // as usual, implicitly convert from C* to x_ptr<C>
MMXXMetaobject mp = p._GetMO(); // mp now refers to *p
MMXX_ProtoDynCast(q, mp); // now q == p
Now assume that D is not one of C's base classes, then:
x_ptr<D> r;
MMXX_ProtoDynCast(r, mp); // does not succeed, r is set to null (and MMXX_ProtoDynCast returns
false)
In other words, MMXXMetaobject is somewhat like a void * in that it can refer to any object. Unlike a void *, no type information is actually lost.
Functor interface
Like an MMXXMetaobject refers to a generic object, a functor is an MMXX type that refers
to a generic class method of a given signature. Functors are very
similar to method pointers, except that:
- Functors are safe for cross-module operation (and also deal with
cross-module exceptions correctly.)
- Functors are not restricted to non-static methods: they can be
used to point to constructors, destructors and static methods
also.
- While method pointers are compile-time constructs whose types
are tied to a (base) class, functors are runtime objects whose
types are not tied to any specific class or base class (in this
respect, functors are rather more like function pointers than
method pointers.)
Here's another comparison between functors, method pointers and
function pointers:
- Method pointers: Fully generic in objects and methods, partially generic in classes
(through inheritance only,) specific in signatures. Fragile base
class unsafe. Can only be used for methods.
- Function pointers: Fully generic in classes and methods, specific in signatures.
Fragile base class safe. Can only be used for static methods and
global functions.
- Functors: Fully generic in classes, methods and objects, specific in signatures.
Fragile base class safe. Can be used for methods, constructors,
destructors and static methods.
Functors are implemented with class templates. Here's an example
of their use, assuming there exists a class named foo with a non-static method named bar with signature int bar(double) :
xf_method_1R<int,double> myFunctor; // declare a method functor with signature int (double)
MMXXClass *fooClass = ...; // obtain pointer to foo's MMXXClass instance
fooClass->FindSymbol("bar", &myFunctor); // find method named "bar" and bind myFunctor to it
x_ptr<foo> fooObject = new foo; // instantiate foo
MMXXMetaobject fooMetaobject = fooObject._GetMO(); // obtain MMXXMetaobject for *fooObject
int result = myFunctor(fooMetaobject, 123.456); // same as result = fooObject->bar(123.456)
Note that myFunctor could be rebound to a different method (with the same signature)
of a completely different class. Also note that if fooMetaobject did not in fact refer to an instance of foo or one of its subclasses, a bad_cast exception would be thrown when myFunctor is invoked.
Here's a quick summary of functor types:
xf_constructor_0
xf_constructor_[1..10]<partypes...> |
Constructor functors, where 1..10 is the number of parameters in the signature as listed in partypes... |
xf_destructor |
Destructor functors. |
xf_method_0[C][N]
xf_method_[0-10][R][C][N]<[restype][partypes...]> |
Non-static member methods, where C optionally denotes a const method, N a method with an empty throw declaration, R a method that returns a non-void result as specified in restype, and 0..10 is the number of parameters whose types are listed in partypes... |
xf_static_0[N]
xf_static_[0-10][R][N]<[restype][partypes...]> |
Static member method, where N optionally denotes a static method with an empty throw declaration,
R a static method that returns a non-void result as specified in restype, and 0..10 is the number of parameters whose types are listed in partypes... |
Metastubs interface
The metastubs interface provides an extra layer of abstraction
on methods that can be used by advanced metacode gateways. Metastub
pointers are similar to functors, except that they're also fully
generic on signatures and can be invoked by signature-independent
code.
Synthetic classes
A (backward-compatible) future release of MMXX will provide support
for synthetic classes, or classes defined at runtime from an arbitrary
set of base classes and (possibly overriding) methods.
MMXX, CORBA and COM
MMXX, CORBA and COM have some overlap in functionality, but have
radically different feature sets and areas of applicability. The
greatest common denominator is that all three systems can be used
to address the fragile base class problem, and that all three
offer interface repositories. Yet it is largely an undue generalization
to consider them as substitutes. For an overview of how these
systems differ, please see the MMXX Frequently Asked Questions.