In the included "basictest" example, there are only two MMXX-enabled classes, according to the following hierarchy:
In module testhost: | class testme; |
In module Module.dll: | class derius : public testme; |
The derivation relationship crosses a module boundary, an all-too-important fact that can be largely ignored from the standpoint of the code that manipulates instances of those classes. While this is also true of traditional C++ dynamic linking, not only cross-module inheritance but also mere cross-module method invocation is enough to trigger the fragile base class problem in that context. Fortunately, both are possible, FBC-free, and relatively transparent if MMXX is used.
Application code does need to utilize MMXX facilities to deal with situations that C++ just can't handle directly (such as instantiating a class that was entirely invisible to the compiler at compile time) as well as to make up for those circumstances where the C++ constructs are simply insufficient for cross-module operation in the MMXX scenario, such as by replacing object pointers and references in cross-module function signatures with module-safe object handles (x_ptr<>, x_cptr<>, x_ref<> and x_cref<>), using the VirtualDelete() method instead of delete, etc.
However, you'll find that familiarizing yourself with these features won't take long, and that the MMXX infrastructure does not limit C++ freedom nearly as much as one might expect.
From the standpoint of the C++ compiler, the hierarchy above is modified by introducing a shadow class for class testme in Module.dll (to differentiate among the testme real and the testme shadow classes, we'll use the $real and $shadow suffices):
+---------------+ testhost: | testme$real | +---------------+ /|\ |A ---------------------|----------------------------------------------- (module boundary) | \|/ + - - - - - - - + +---------------+ Module.dll: : testme$shadow : <------------- | derius | + - - - - - - - + B +---------------+In the arrows above, B indicates the consuete C++ derivation relationship. The C++ compiler used to build Module.dll is aware of this relationship, and manages it in the traditional fashion. The A arrow indicates a peer-shadow cross-module relationship instead. This relationship is implemented entirely in MMXX support code and metadata, and the C++ compiler is unaware of it.
An important implication of this is that whenever class derius is instantiated, MMXX ends up creating two objects from the compiler's perspective - an instance of testme$real in testhost, and an instance of derius in Module.dll - which are permanently bound together and indeed behave like a single object (and should in all circumstances be treated as such) from the application's perspective. For the remainder of this document, we'll use the terms compiler object or compiler instance to refer to what the compiler sees as distinct objects, but are really only portions of what an application sees as a single logical object or instance.
class testme { ... public: double divideby(double); // non-virtual, implemented in testme virtual double queryval() const = 0; // pure virtual, to be implemented by subclass virtual double getscaling(); // virtual, can be overridden by subclass ... }; class derius : public testme { ... virtual double queryval(); // implements pure virtual method from testme virtual double getscaling(); // overrides testme::getscaling() ... };This is what happens when those methods are invoked by code in testhost:
// inside testhost: testme *deriusObj = ...; // assume deriusObj is an instance of derius manipulated // through a testme* base class pointer double a = deriusObj->divideby(10.0); // not too special - regular C++ behavior, goes to // testme$real::divideby(), no support code gets invoked double b = deriusObj->queryval(); // this invokes testme$real::queryval(), whose definition // was automatically provided by MMXX (the =0 was omitted // in the declaration.) The support code crosses the // module boundary and invokes testme$shadow::queryval(). // As a matter of fact, the MMXX auto-generated definition // for testme$shadow::queryval() simply returns 0.0, BUT // ...it never gets invoked, since the method was // overridden by derius::queryval(), which is invoked // instead (through regular C++ inheritance.)This is what happens when these methods are invoked by code in Module.dll instead:
// inside Module.dll derius *deriusObj = ...; // since Module.dll has knowledge of class derius, it // can use a derius* to refer to this instance, // however even if we used a testme* the behavior would // be identical since all these methods are declared in // class testme. double a = deriusObj->divideby(10.0); // since there is no derius::divideby(), this // invokes testme$shadow::divideby() through // regular C++ inheritance, whose definition was // automatically provided by MMXX. The support // code there crosses the module boundary and // invokes testme$real::divideby(). double b = deriusObj->queryval(); // not too special - regular C++ behavior, goes to // derius::queryval(), no support code gets invoked.
// inside testhost: double c = deriusObj->getscaling(); // the definition of testme$real::getscaling() was // automatically provided by MMXX. If the method // hadn't been overridden by derius::getscaling(), // MMXX would invoke the user-provided // testme$real::_I_getscaling() implementation, // but since it has, the support code crosses the // module boundary and invokes // testme$shadow::getscaling(), which was itself // provided by MMXX. However, the latter never gets to // run, since it's overridden by derius::getscaling(), // which itself simply invokes derius::_I_getscaling() // which is the correct user-provided implementation.Here's what happens when getscaling() is invoked from Module.dll code instead:
// inside Module.dll: double c = deriusObj->getscaling(); // this invokes derius::getscaling(), which itself // simply invokes derius::_I_getscaling() which is // the correct user-provided implementation.To be complete in our exposition, let's assume hypothetically that getscaling() had not been overridden by class derius. In that case, invoking it from testhost would result in the MMXX-provided testme$real::getscaling() method calling the user-provided testme$real::_I_getscaling() method directly. Invoking it from Module.dll instead would result in the invocation of the MMXX-provided testme$shadow::getscaling() method, which itself would (by way of the MMXX-provided testme$shadow::_I_getscaling() method to be precise) cross the module boundary and result in the invocation of testme$real::_I_getscaling() as well.
The moral of this long exposition is that through various "magic tricks," MMXX filled in enough support code for us not to have to worry about whether our implementations are module-local or not. Once you've acquired enough confidence in MMXX and have gotten used to its required constructs, you can safely forget about shadow classes and support code altogether, just like you don't keep vtables and inheritance layouts in mind when you write C++ code.
In module 1: | class A { }; class B { }; |
In module 2: | class C : public A, public B { }; |
From the application's perspective, this works in the same way as the previous example - namely that the implementation locality of the classes should be treated as largely insignificant. In this case, MMXX uses three compiler objects to make up a single application object:
+----------+ +----------+ Module 1: | A$real | | B$real | +----------+ +----------+ /|\ /|\ | | --------------------|--------------------------|------------ module boundary |P/S |P/S | | \|/ \|/ +- - - - - + +- - - - - + : A$shadow : : B$shadow | + - - - - -+ + - - - - -+ |\~ ~/| \ / \ / Module 2: \Der /Der \ / \ / +------------+ | C | +------------+Note that we've taken to mark the arrows either P/S for peer/shadow relationships or Der for derivation relationships. A subtle implication of this is that an instance of class C is super-polymorphic from the perspective of module 1, since it's both an A and a B even though the compiler had no knowledge of any class C that derives from both A and B. For this reason, dynamic casts between A* and B* performed through MMXX_DynCast() can succeed even in some circumstances where they would fail if dynamic_cast was used.
In module 1: | class D { }; class E { }; class F : public D { }; |
In module 2: | class G : public E, public F { }; |
This is constructed as follows:
+----------+ +----------+ +----------+ Module 1: | D$real | <----- | F$real | | E$real | +----------+ Der +----------+ +----------+ /|\ /|\ /|\ | | | -------------------|-------------------|------------------|------- module boundary |P/S |P/S |P/S | | | \|/ \|/ \|/ +- - - - - + +- - - - - + +- - - - - + : D$shadow : <----- : F$shadow : : E$shadow : + - - - - -+ Der + - - - - -+ + - - - - -+ |\~ ~/| Module 2: \Der /Der \ / +----------+ | G | +----------+Note that the hierarchy of shadows closely mimics the hierarchy of real classes. This particular arrangement allows us to always dispatch methods correctly, in a fashion that's compatible with a variety of inheritance layouts. In this case, there are also 3 compiler objects (a compiler instance of F$real and one of E$real in module 1, and one of G in module 2) which as always are bound into a single indivisible application object.
In module 1: | class H { }; |
In module 2: | class I : public H { }; |
In module 3: | class J : public I { }; class K : public H { }; |
This is more complicated in that the linker will utilize different peer-shadow link arrangements depending on the particular class being instantiated. This is constructed as follows:
+------------+ Module 1: | H$real | +------------+ /|\ /|\ | | | +---|---------------------------------------- | : |P/S2 1/2 boundary | : `------. Module 2: | : | | : \|/ | : +- - - - - - + +------------+ --------------|------: : H$shadow@2 | <----- | I$real | 1/3 boundary | : + - - - - - -+ Der +------------+ |P/S1 : /|\ /|\ | : | | | +----------|------------------|-------------- | | | 2/3 boundary | .------' | | |P/S3 |P/S4 Module 3: | | | \|/ \|/ \|/ +- - - - - - + +- - - - - - + : H$shadow@3 : <------------------ : I$shadow@3 : + - - - - - -+ Der + - - - - - -+ /|\ /|\ | | |Der |Der | | +------------+ +------------+ | K | | J | +------------+ +------------+When K is instantiated, the linker installs the P/S1 peer-shadow link, and everything works as in the previous examples. Notice that module 2 is not at all involved with instances of K. However, when J is instantiated, class I and its entire derivation hierarchy must be shadowed through module 2. Since the derivation hierarchy includes H, the linker installs the P/S2 link to shadow H into module 2, and the P/S3 link to shadow H's module 2 shadow into module 3. It also installs the P/S4 link to shadow I into module 3. Once again, this all happens transparently from the standpoint of the application.
The second scenario is more general. It does not only apply to when a handle to a host-created object is passed to a module, but in general to all circumstances in which a handle to an object is passed to a module in the form of a pointer or reference to a class Z (which may be the object's true class or one of its bases,) and the module does not contribute a shadow of Z to that object's active peer-shadow relationships.
Note that in order to obtain a Z pointer or reference, the module in question already has access to Z as a visible MMXX-enabled class. In other words, there is a shadow of Z available to the compiler as a class, this shadow simply does not take part in the network of peer-shadow relationships for the object in question; more simply, there is no local compiler instance. In this scenario, MMXX creates a ghost, or an extra compiler instance of the module's Z shadow, and attaches it to the object by installing a ghost link (to be specific, the link is connected to the compiler instance of Z$real.) Once again, this happens transparently from the standpoint of the application, which always sees a single application object.
Now suppose that module 3 were to pass a pointer to an instance of class K instead, again to module 2 and again in the form of an x_ptr<H>. This time, H$shadow@2 is not involved in the peer-shadow relationships graph, since H$shadow@3 is bound directly to H$real for instances of class K. In this case, as soon as module 2 dereferences the x_ptr<H>, MMXX will instantiate H$shadow@2 into a ghost compiler object, then link it to H$real with a ghost link. Here's an illustration of the layout after the ghost is installed:
+------------+ Module 1: | H$real | +------------+ /|\ /|\ | | | +---|---------------------------------------- | : |GHOST LINK 1/2 boundary | : `------. Module 2: | : | | : | | : +- - - - - - + --------------|------: : H$shadow@2 | 1/3 boundary | : + - - - - - -+ |P/S1 : | : | +-------------------------------------------- | 2/3 boundary | | Module 3: | \|/ +- - - - - - + : H$shadow@3 : + - - - - - -+ /|\ | |Der | +------------+ | K | +------------+Method invocations on ghosts are routed through the corresponding methods on the module/compiler instance combination that the ghost link points to, which results in the expected behavior. Note that each module that gains access to an object in this fashion will result in a ghost being created, unless an appropriate ghost is already available in which case it's returned again (indeed, dereferenced pointers can even be tested for equality as usual.) All ghosts attached to the MMXX class graph are retained until the object is deleted.
It is not unusual for more than one ghost to be created for the
same object/module combination. In the second example (the one
illustrating classes A, B and C), an hypothetical third module might have shadows for both A and B but not for C. If this module was separately passed an x_ptr<A> and an x_ptr<B>, both referring to the same C instance by way of different base class pointers, MMXX would
create two ghosts for the third module alone. Even in the context
of this third module, MMXX_DynCast() would be able to convert back and forth between pointers to the
two ghosts, so the particular arrangement employed by MMXX remains
transparent to application code. Note that if the third module
did have a shadow for C, a single ghost of C would be created and ghost-linked to the C$real compiler instance (ghosts are placed at the most specific joining
point available.)