Embedded Instances

From RTSC-Pedia

Jump to: navigation, search
revision tip
—— LANDSCAPE orientation
[printable version]  [offline version]offline version generated on 18-Nov-2017 00:11 UTC

Embedded Instances

When (and when not) to use the type Object

This article introduces an advanced RTSC programming feature known as embedded instances, which affords the developer of RTSC modules some additional opportunities for optimizing run-time performance. Building upon the foundation put in place within the RTSC Module Primer—material we assume you've already mastered—the embedded instances feature described here largely boils down to disciplined use of the type Object within module source files.

The XDCtools product has supported embedded instances for quite some time now, albeit as an "undocumented" feature used only by a select set of BIOSv6 module developers. Consider, then, this article as the first publication of the embedded instances feature!

NOTE:  The XDCtools 3.10 product, while supporting this feature, could do a better job of flagging incorrect use of the type Object.
NOTE:  The programming rules for using Object enumerated throughout this article will be more strictly enforced in XDCtools 3.15.
NOTE:  Current developers using embedded instances should review their code along these lines, to avoid breakage in the future.

Contents

The types Handle, Struct, and Object

Client perspective.  By way of review, clients of a RTSC module hypothetically named ModA will interact with underlying instance objects (presumably) managed by this module using pointer-sized values of type ModA_Handle within their C code. As illustrated in Lesson 4 of the RTSC Module Primer, values of type ModA_Handle can originate from a variety of sources:

  • from calls to the ModA_create function, which returns values of this type;
  • from the special conversion function ModA_handle, applied to a ModA_Struct pointer; and
  • from extern variables of this type referencing static instances, created during program configuration.

In all cases, clients should treat a Handle as an opaque reference to some underlying instance object originally manufactured in one of several different ways. Though technically a C typedef that equates to a ModA_Object* pointer, clients of ModA must absolutely ignore this fact. As a client, you should instead regard ModA_Handle as a unique C type (distinct from any other module's Handle) whose true meaning derives from the per-instance functions of ModA which ultimately manipulate values of this type.

From XDCtools 3.15 onward, ModA_Object will remain as an incomplete C struct during client compilation—further preventing clients of ModA from making any assumptions whatsoever about the size of this structure, let alone its contents. For all practical purposes, clients of ModA must never declare variables of type ModA_Object in their code.

Clients wishing to explicitly allocate space for an instance object can always declare a C variable of type ModA_Struct, passing its address to the ModA_construct function to complete the initialization process; as noted above, you can obtain corresponding values of type ModA_Handle referencing newly-constructed instance objects via the ModA_handle conversion function. If necessary, you should review this programming example in Lesson 4 to clarify the relationship between the types ModA_Handle and ModA_Struct.

While certainly not an incomplete C struct (like ModA_Object, whose size remains unknown at compile-time), clients should nevertheless treat ModA_Struct as an opaque type—one representing a "chunk" of memory large enough to hold the internal state of an instance object, but without any further details exposed at this point.

From XDCtools 3.15 onward, the definition of type ModA_Struct will have fictionalized field names, preventing (accidental) attempts by clients to access supplier-proprietary instance state variables via structures of this type.

Supplier perspective.  Unlike its clients, the supplier of the ModA module both defines and manipulates internal state variables associated with each ModA instance object. Originally defined as elements of a well-known Instance_State structure found in the internal section of the module's specification (ModA.xdc), these state variables become fields of the type ModA_Object within the module's target-implementation in C. A quick glance through Lesson 8 of the RTSC Module Primer should refresh your memory on these points.

Though no longer an incomplete struct, the ModA supplier will nevertheless access ModA_Object fields—corresponding to instance state variables—exclusively via pointers to this type of structure, conventionally passed as the first argument to all per-instance functions. Whereas clients of ModA shouldn't even mention the type ModA_Object by name within their own source code, declarations of type ModA_Object* will conventionally appear throughout the supplier's target-implementation in C.

Even the ModA supplier must never declare variables of type ModA_Object, as you'll come to understand shortly. Unlike ModA clients—who would encounter compile-time errors from XDCtools 3.15 onward—module suppliers (for now) must exercise vigilance, following prescribed programming idioms that rely upon pointers of type ModA_Object* instead.

Performance impact of linking versus embedding

Often, the supplier of ModA becomes a client of another module ModB—in particular, when the implementation of ModA instance objects internally rely upon ModB instances. Practical examples of this pattern occur throughout "real-world" products composed out of RTSC modules, such as BIOSv6: Semaphore instances internally manage a Queue instance; Mailbox instances internally manage two Semaphore instances plus a Queue instance of their own; and so forth.

As a rule, ModA interacts with ModB no differently than any other client; and above all, ModA can make no assumptions about the internal implementation of ModB (which, in general, could reside in a different package). Following protocol, ModA may specify instance state variables of type ModB.Handle in the ModA.xdc specification; these in turn become fields of type ModB_Handle in ModA.c, manipulated as part of the module's implementation in the target-domain.

pkg1/ModA.xdc
 
 
 
 
 
1
 
 
 
import pkg2.ModB {
module ModA {
    ...
internal:    
    struct Instance_State {
        ModB.Handle mbInst;  
        Int x;
    };
}
pkg1/ModA.c
 
 
 
 
 
2
 
 
 
#include <pkg2/ModB.h>
#include "package/internal/ModA.xdc.h"
 
Void ModA_Instance_init(ModA_Object *obj, const ModA_Params *params)
{
    obj->mbInst = ModB_create( ... );
    obj->x = ... ;
}
 

This pattern extends analogously to the implementation of ModA in the meta-domain, where the special instance$static$init function defined in ModA.xs would mimic line 2 and assign obj.mbInst the results of calling ModB.create during program configuration.

Taken to the limit, instance objects managed by one RTSC module can (recursively) compose instances managed by other RTSC modules—with opaque Handle references at the center of the client-supplier contract found at each level of this arbitrarily-rich hierarchy of modules. While easy to comprehend and straightforward to implement, this general design pattern does have one major drawback:

Excessive use of opaque Handle references—especially when composing small and/or frequently-accessed instance objects—can potentially degrade run-time performance in time as well as space.

Suppose, now, that the per-instance state of a ModB object required only two words of memory: the pointer-sized ModB_Handle then retained by ModA within each of its own instance objects would introduce a space overhead of 50% under this scenario. Even with somewhat larger instance objects, what effectively becomes pointer-based linkage between instances can potentially increase fragmentation of memory pools and decrease locality of reference during program execution—especially since each call to ModA_create (or ModA_construct) at run-time would trigger a corresponding call to ModB_create; calls to ModA_delete (or ModA_destruct) would likewise invoke ModB_delete.

For the record, BIOSv6 Queue and Semaphore instance objects respectively consume two (2) and six (6) words of memory apiece.

The ModB_Handle retained by ModA also represents a potential time overhead, logically requiring an additional memory fetch of obj->mbInst each time a ModA per-instance function becomes a client of ModB. Even when applying the most aggressive modes of whole-program optimization—in which ModB calls become automatically inlined—retention of the ModB_Handle by ModA will always incur some residual cost.

The answer, quite simply, boils down to embedding a ModB instance object directly within the representation of an ModA instance object, rather than linking to a ModB instance via the mbInst state variable currently of type ModB_Handle. The real question, then, becomes finding an appropriate type (other than Handle) for mbInst per se.

Seemingly, declaring mbInst as a ModB.Struct at line 1 above would signify instance embedding versus instance linking; not so, since the XDCspec language doesn't even recognize Struct as a legal type!  More to the point, however, the C type ModB_Struct will generally reserve more storage than actually required to hold a ModB instance object—whose final size, it turns out, depends upon program configuration parameters known after compilation of ModA.c. At the end of the day, we need a way to reconcile C's innate requirement for fixed struct sizes at compile-time versus RTSC's unique ability to impact the size of module instance objects at config-time.

An idiomatic example of embedded instances

As hinted by the sub-title of this article, the type Object does play a central role when realizing embedded instances. Turning first to ModA.xdc—where we had originally declared mbInst of type ModB.Handle back at line 1—we'll alternatively ascribe the XDCspec type ModB.Object to the same state variable.

pkg1/ModA.xdc
 
 
 
 
 
3
 
 
 
import pkg2.ModB {
module ModA {
    ...
internal:
    struct Instance_State {  
        ModB.Object mbInst;  
        Int x;
    };
}

While we've effectively banned direct use of the C struct type ModB_Object by clients and suppliers alike, you will encounter the corresponding XDCspec type ModB.Object within module specifications—though only in a limited set of contexts. As a rule, the type Object can only appear inside the special internal structures Instance_State and Module_State, respectively used to define the per-instance and module-wide state variables that form the backbone of a module's proprietary implementation.

From XDCtools 3.15 onward, the XDCspec translator will flag any inappropriate usage of the type Object as an error.

Turning now to ModA.c, we've finally come to the heart of the matter—a new C programming idiom used within RTSC module implementations to retrieve a Handle referencing a corresponding Object embedded within this module's state.

pkg1/ModA.c
 
 
 
 
 
4
 
5
 
 
#include <pkg2/ModB.h>
#include "package/internal/ModA.xdc.h"
 
Void ModA_Instance_init(ModA_Object *obj, const ModA_Params *params)
{
    ModB_Handle mbH = ModA_Instance_State_mbInst(obj);
 
    ModB_construct(ModB_struct(mbH), ... );
    obj->x = ... ;
}

Conceptually, the automatically-generated "getter" function called at line 4 means &(obj->mbInst), an expression denoting the address of the ModB_Object stored in the mbInst field of obj (that is, a value of type ModB_Handle); in reality, the ModA_Object structure has no field named mbInst whatsoever. Since ModA cannot make any assumptions about the actual size of a ModB_Object, the definition of the ModA_Object structure seen when compiling ModA.c cannot include an incomplete field of this type.

From XDCtools 3.15 onward, the expression obj->mbInst (even within ModA.c) would result in a compile-time error.

The name itself of the special getter called at line 4 suggests this idiom extends not only to multiple per-instance state variables holding embedded objects (each of a potentially different Object type), but to module-wide state variables as well. In the latter case, parameter-less calls of the form Mod_Module_State_var() inside of Mod.c conceptually mean the same thing as the expression &(module->var), yielding a Handle to the embedded Object held in the module-wide state variable named var.

From the perspective of ModA, its module-wide state variables not only can hold embedded ModB.Object instances but in fact can hold one or more of its own ModA.Object instances as well; not as exotic as you might imagine, this design pattern effectively enables the ModA implementation to maintain its own private pool of statically-created instances apart from those fabricated through calls by others to ModA.create in the meta-domain. At the same time, an unresolvable self-referential type definition would prevent the per-instance state of ModA from itself further embedding any ModA.Object instances.

Wrapping up ModA.c, the value of type ModB_Handle returned by the getter at line 4 subsequently becomes input to the ModB_construct call at line 5—with the special conversion function ModB_struct effectively casting mbH into a corresponding value of type ModB_Struct*. Though not shown here, an implementation of ModA_instance_finalize would feature an analogous call to ModB_destruct with a similar set of preparatory steps. Needless to say, the implementation of any per-instance ModA function that in turn becomes a client of some ModB per-instance function would likewise invoke the ModA_Instance_State_mbInst getter to obtain a suitable ModB_Handle for input.

And finally, let's take a quick look at the special meta-function instance$static$init defined in ModA.xs:

pkg1/ModA.xs
 
 
 
 
6
 
 
var ModB = xdc.useModule('pkg2.ModB');
    ...
function instance$state$init( obj, ... )
{
    ModB.construct(obj.mbInst, ... );
    obj.x = ... ;
}

While clearly mirroring line 5 of ModA.c, the rather uncluttered call to ModB.construct here at line 6 reflects upon the higher-level nature of the XDCscript meta-language versus standard C.

Implications for binary compatibility

Ideally, "new-and-improved" versions of ModB would maintain binary backward-compatibility whenever possible. While the supplier of ModB continues to innovate—not only by adding new functionality, but also by improving the internal implementation of current functionality—clients such as ModA need not continuously track these sorts of changes within a programmatic contract already put in place. Said another way, re-compiling ModA.c should become unnecessary—"old" ModA libraries will continue to link and run against "new" (binary-compatible) versions of ModB.

Notwithstanding the growing trend towards "open-source" software—enabling integrators to re-build all inbound components at will—we firmly believe the discipline required to maintain binary compatibility significantly boosts overall software quality by keeping contracts between suppliers and their clients front-and-center. If nothing else, binary compatibility re-affirms the application integrator's ability to mix-and-match otherwise discrete, independently developed software elements—a distinguishing characteristic of any robust component-based ecosystem.

More to the point, suppose the supplier of ModB in some way changes the number or type of (internal) instance state variables required in the module's (internal) implementation—whether to optimize performance of current client-visible functionality in some way, or else to support expanded client-visible functionality in a new version of ModB. Either way, the actual size of the structure representing a ModB instance object could change—normally implying that clients such as a ModA who elected to embed ModB instances within their own internal state would need to re-build and re-release their own content against these changes.

Even without new versions of ModB that change the size of its instances, program integrators can optionally configure ModB to automatically retain a textual name (optionally) provided by clients upon instance creation. Setting the special module-wide config parameter ModB.common$.namedInstance to true effectively enlarges the ModB instance object by adding an internal field that retains the value of the special per-instance config parameter instance.name. Since various diagnostic capabilities of the xdc.runtime package described here will leverage these names if present within an instance object, the program integrator can weigh the cost of the extra storage per ModB instance against decreased visibility when troubleshooting an errant program at runtime. Either way, the supplier of ModA electing to embed ModB instance objects should remain insulated from these downstream decisions by the integrator.

By following the programmatic idioms exemplified earlier, the supplier of ModA can indeed remain insulated from any changes to the size of a ModB instance object. While understanding how this actually works lies outside the scope of this article (requiring some knowledge of the C language binding used internally by RTSC), suffice it to say that the (seemingly complete) type ModA_Object referenced in ModA.c in reality serves as a prefix for the "real" definition of this structure laid down in the aftermath of RTSC program configuration.

Having a global perspective of the program under configuration not seen by individual module suppliers, the "real" definition of ModA_Object can now refer to the "real" definition of ModB_Object; and both of these "real" structure definitions may or may not contain an extra internal field, depending upon the setting of common$.namedInstance for these modules. Based upon the actual definitions known only at config-time, RTSC then generates the run-time apparatus needed to access embedded instances at run-time using the special "getters" illustrated earlier.

And thanks to the transformational power of whole-program optimization, the call to ModA_Instance_State_mb at line 4 effectively devolves into an inline reference to &(obj->mbInst)—incurring no more overhead than if the "real" definitions of both ModA_Object along with the ModB_Object it embeds were known when compiling ModA.c in the first place.

See also

RTSC Module Primer/Lesson 4 Instance object life-cycle — using the RandGen module
RTSC Module Primer/Lesson 8 Instance modules — implementing bravo.math.RandGen
Personal tools
package reference