From RTSC-Pedia

Jump to: navigation, search
revision tip
—— LANDSCAPE orientation
[printable version]  offline version generated on 04-Aug-2010 21:08 UTC  

RTSC Interface Primer/Lesson 12

Proxy modules — generalizing the implementation of Bench

As in the last lesson, we'll once again define a single RTSC interface that abstracts external functionality shared across a family of RTSC modules—each with their own unique implementation of an otherwise common set of client-visible functions. But unlike the last lesson—which emphasized implementing concrete modules like SineGen that inherited abstract interfaces like ISineGen—the focus here will now extend to using these sorts of abstracted modules from the client's perspective.

To illustrate, we'll revisit the implementation of the Bench module first seen in Lesson 7 and then—by relying upon an abstract interface used internally within Bench.c—will actually render the module even more adaptable (and hence reusable) than before. This particular design technique—termed the proxy-delegate pattern—has proven so overwhelmingly practical and so widely applicable that RTSC has actually promoted the concept to first-class keyword status within the XDCspec language.

Contents

Abstracting the clock function

Recalling some fragments of our earlier implementation of acme.utils.Bench in the target-domain, we've relied here on direct calls to the standard C function clock to obtain the current time.

acme/utils/Bench.c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#include <xdc/runtime/Startup.h>
#include <xdc/runtime/System.h>
#include "package/internal/Bench.xdc.h"
 
#include <time.h>
 
    ...
Void Bench_begin(String msg)
{
    if (Bench_enableFlag) {
        module->beginClock = clock();
        module->beginMsg = msg;
    }
}

Though theoretically supported by any ANSI C compiler—and hence by any RTSC target—practically speaking the clock function implementation supplied with many tool chains often proves inadequate for use within deeply embedded systems. Indeed, some cross-compilers targeting "bare-deck" CPUs without operating system support will simply stub out the body of clock altogether; embedded programs requiring clock services in these sorts of systems may instead rely upon a timer peripheral in the underlying hardware platform or perhaps a free-running instruction-cycle counter supported by the CPU itself.

Given a range of possible implementations for the clock function—and coupled with the possibility that several of these alternate implementations may need to co-exist within the same program—let's postulate a new RTSC interface canonically named acme.utils2.clocks.IClock that specs a single function for retrieving the current time.

acme/utils2/clocks/IClock.xdc
 
 
 
 
1
 
 
2
 
/*! Abstract clock */
interface IClock {
 
   /*! Time value type */
   typedef Bits32 TimeValue;
 
   /*! Get the current time */
   TimeValue getTime();
}

Unlike the IGen or ISineGen interfaces seen in the last lesson, the abstract functionality captured within the IClock interface would apply module-wide rather than per-instance; inheriting modules would implement the getTime function spec'd at line 2 without any additional argument representing some sort of instance object handle. To make IClock a little more expressive, we've elected to define a logical type at line 1 corresponding to our chosen representation for time values—an unsigned integer of exactly 32-bits, implied by our use of the built-in RTSC type Bits32.

Here's one area where RTSC interfaces diverge from similarly-named constructs in Java or C#:  interfaces in these languages can only declare functions that apply to (class) instances. But with modules playing a central role in RTSC programming—and with modules naturally embodying the object-disorientation of C—it seems only logical to allow RTSC interfaces to abstract any client-visible elements normally found in RTSC module specs.

Alternate IClock implementations

ClockStd.  Given that the Bench module originally relied upon direct calls to the standard C clock function—certainly appropriate in some environments—this immediately suggests one possible IClock implementation in which the getTime function directly calls clock. Consider, then, the portable acme.utils2.clocks.ClockStd module which first inherits and then implements the acme.utils2.clocks.IClock interface.

acme/utils2/clocks/ClockStd.xdc
 
1
 
 
/*! Portable IClock module */
module ClockStd inherits IClock {
    /* no additional features or internal details */
}
 
acme/utils2/clocks/ClockStd.c
 
 
2
 
 
 
3
 
#include "package/internal/ClockStd.xdc.h"
 
#include <time.h>
 
ClockStd_TimeValue ClockStd_getTime()
{
    return (ClockStd_TimeValue) clock();
}

Note that since the ClockStd module and the IClock interface both reside in the same acme.utils2.clocks package (and hence in the same namespace), we need not reference IClock with its fully-qualified name at line 1. In general, excessive use of canonical names in any source file tends to impair readability; whenever possible, we'll prefer mechanisms that allow us to name RTSC modules or interfaces using their short names instead.

As you'll see with the next IClock module —which does not reside in the acme.utils2.clocks package—the XDCspec language supports a special import statement that enables us to reference modules/interfaces from other packages using short names, as if they were defined locally.

Looking now at line 2 of ClockStd.c, we should once again emphasize that target-implementations of RTSC modules can freely #include legacy header files that declare legacy functions. In the case of clock—declared of type long in <time.h>—we've explicitly cast its result at line 3 to match the return type of getTime originally specified in IClock.xdc. As with the target-implementation of any module inheriting an interface, references to all names spec'd in the interface become C identifiers prefixed with the name of the module—getTime becomes ClockStd_getTime and TimeValue becomes ClockStd_TimeValue.

Finally, because our target-implementation does not rely upon any internal variables requiring static initialization, we can field an empty implementation in the meta-domain by simply omitting the usual ClockStd.xs file that would otherwise contain definitions of special functions like module$static$init.

In a similar way, you could imagine modules whose spec contains no target functions at all—only client-visible constants, types, and even module-wide configs; in these cases, you could likewise omit the usual Mod.c file that would otherwise contain definitions of target-domain functions.

Clock64P.  As it turns out, all Texas Instruments TMS320C64+ processors—the family of CPUs covered by the ti.targets.C64P RTSC target—support a free-running on-chip counter with 64-bits of resolution, providing a very efficient means for measuring elapsed execution time at the granularity of a single instruction cycle. Consider, then, an alternate implementation of the acme.utils2.clocks.IClock interface by a Clock64P module residing in a different package—say, txn.clocks which might contain similar modules supporting other families of Texas Instruments processors.

txn/clocks/Clock64P.xdc
1
 
 
 
2
 
 
import acme.utils2.clocks.IClock;
 
/*! TMS320C64+ IClock module */
@ModuleStartup
module Clock64P inherits IClock {
    /* no additional features or internal details */
};

txn/clocks/Clock64P.c
 
 
 
3
 
 
 
4
 
 
 
 
 
5
 
#include <xdc/runtime/Startup.h>
#include "package/internal/Clock64P.xdc.h"
 
extern volatile cregister unsigned int TSCL;
 
Int Clock64P_Module_startup(Int state)
{
    TSCL = 0;
    return Startup_DONE;
}
 
Clock64P_TimeValue Clock64P_getTime()
{
    return (Clock64P_TimeValue) TSCL;
}
txn/clocks/Clock64P.xs
 
 
6
 
function module$use()
{
    xdc.useModule('xdc.runtime.Startup');
}

As we mentioned earlier, the import statement at line 1 effectively adds the identifier IClock to the txn.clocks package namespace, which in turn allows us to avoid using a fully-qualified name in the inherits clause at line 2. Though perhaps a little pedantic in this particular situation, the import statement proves particularly helpful when the current spec contains multiple references to individual RTSC units—concrete modules as well as abstract interfaces. The current spec can then reference client-visible elements defined within the scope of these modules or interfaces using a UnitName.elementName idiom.

Turning now to the Clock64P.c file—a body of C code clearly tied to the TMS320C64+ processor and compiler tools—the target-implementation simply declares, enables, and then subsequently reads a special on-chip cycle counter named TSCL at lines 3, 4, and 5 respectively. Given its dependency on xdc.runtime.Startup by virtue of the @ModuleStartup attribute before line 2 of Clock64P.xdc, the (non-empty) meta-implementation in Clock64P.xs predictably contains a corresponding xdc.useModule directive at line 6.

Using a spec'd proxy module

Returning to Bench.c—which will no longer call the clock function directly, but will instead presumably use some concrete module that inherits the IClock interface—we now have an interesting dilemna:  the target-domain implementation of Bench simply cannot know the true identity of the IClock module it will actually use at runtime; some sort of indirection appears necessary in the source code, so that we can compile Bench.c once and for all.

One could imagine solving this problem using C preprocessor directives that #define a well-known macro inside of Bench.c, a macro which would then expand (at compile-time) into an explicit call to some concrete IClock module; similar techniques would ensure inclusion of the client header file associated with the chosen IClock module. This implies, of course, that any client of Bench has access to the module's implementation source files—something module suppliers in general may not elect to make broadly available; it also imposes some additional integration steps outside of the standard RTSC configuration flow we've used consistently from the very outset. Needless to say, we reject this approach.

To maintain the anonymity of the actual IClock module used inside Bench.c, we'll rely upon a special proxy module whose (auto-generated) implementation simply forwards any calls it may receive to an associated delegate module—the true provider of the requested service. Unlike an ordinary RTSC module (or RTSC interface)—"top-level" units contained within the namespace of some package—specifying a RTSC proxy actually resembles declarations of other module-wide features that themselves reside with the scope of some containing module (or interface). Consider, now, our new spec for acme.utils2.Bench.

acme/utils2/Bench.xdc
1
 
 
 
 
 
 
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
import acme.utils2.clocks.IClock;
 
/*! Benchmark timing support */
@ModuleStartup
module Bench {
 
    /*! Selectable IClock service provider */
    proxy PClock inherits IClock;
 
    /*! Enable/disable benchmark functions */
    config Bool enableFlag = true;
 
    /*! Start the benchmark */
    Void begin(String msg);
 
    /*! End the benchmark and print the current time */
    Void end();
 
internal:
 
    struct Module_State {
        String beginMsg;    /* current begin() message */
        Int beginClock;     /* begin() start time */
        Int overhead;       /* timing overhead factor */
    };
}

Consistent with other scoped features like enableFlag, we'll reference the PClock proxy declared at line 2 as Bench_PClock within the target-domain and as Bench.PClock within the meta-domain; but unlike enableFlag, we're not talking about Bool values here. Using the same sort of inherits clause seen in earlier examples, we've effectively spec'd the module type of our PClock proxy—it belongs to the family of modules inheriting the IClock interface.

Setting aside for the moment how we'll actually bind the PClock proxy to some suitable delegate module—which of course must inherit IClock—use of the proxy within Bench.c has a familiar look-and-feel. Once again, we'll show only a fragment of Bench.c to illustrate idiomatic use of proxy modules within target-implementations.

acme/utils2/Bench.c
 
 
1
 
 
 
 
2
 
 
 
#include <xdc/runtime/Startup.h>
#include <xdc/runtime/System.h>
#include "package/internal/Bench.xdc.h"
    ...
Void Bench_begin(String msg)
{
    if (Bench_enableFlag) {
        module->beginClock = Bench_PClock_getTime();
        module->beginMsg = msg;
    }
}

Unlike "top-level" modules such as xdc.runtime.Startup whose associated client header requires explicit inclusion, the Bench module's self-contained internal header already read in at line 1 automatically enables use of the Bench_PClock proxy module without requiring any further #include directives. Typical calls to proxy functions such as Bench_PClock_getTime at line 2—even with their slightly longer C identifiers—remain completely consistent with every other RTSC programming idiom we've already seen in target-domain implementations.

For consistency with interface names (such as IClock), our examples will always use proxy names (such as PClock) that begin with a 'P'. Unlike the nearly universal 'I' naming convention used with interfaces, current use of proxies in packages like xdc.runtime favors even longer names (such as ClockProvider or ClockSupplier). For now, though, we'll stick with the more predictable PClock proxy that inherits the IClock interface.

Considerations for configuration

Within the meta-domain, a proxy such as PClock becomes an assignable property of the Bench module, not unlike module-wide configs such as enableFlag. A top-level prog.cfg meta-program might then look like this.

prog.cfg
 
1a
 
 
2a
 
 
var Bench = xdc.useModule('acme.utils2.Bench');
var Clock64P = xdc.useModule('txn.clocks.Clock64P');
 
if (Program.build.target.isa == "64P") {
    Bench.PClock = Clock64P;
    Bench.enableFlag = true;
}

Here again, explicit inclusion of the chosen IClock delegate module at line 1a followed by its assignment to Bench.PClock at line 2a leverages familiar meta-domain programming idioms you've seen all along. Note also that XDCscript enforces strict type-checking when binding delegate modules to proxies; assigning (say) the bravo.math2.SineGen module to Bench.PClock provokes the same sort of fatal type error as when assigning (say) the value 123 in lieu of a boolean to Bench.enableFlag.

Lesson 14 illustrates a technique for passing arguments to meta-programs like prog.cfg, enabling them to remain independent of any particular RTSC target (ti.targets.C64P) by removing explicit references to target-dependent modules (txn.clocks.Clock64P).

Unlike configs in general—which typically have default values—XDCspec (currently) provides no means for specifying a default proxy-delegate binding whenever Bench.PClock remains unassigned after prog.cfg completes execution in the meta-domain; rather, Bench.PClock—as with all RTSC proxies—takes on null as its default value. By convention, then, the meta-implementation of Bench must assume responsibility for binding some suitable default delegate to its PClock proxy if necessary.

acme/utils2/Bench.xs
 
 
 
 
 
 
 
 
1b
2b
 
 
 
 
 
function module$use()
{
    xdc.useModule('xdc.runtime.Startup');
    xdc.useModule('xdc.runtime.System');
 
    var Bench = xdc.useModule('acme.utils2.Bench');
 
    if (Bench.PClock == null) {
        var ClockStd = xdc.useModule('acme.utils2.clocks.ClockStd');
        Bench.PClock = ClockStd;
    }
}
 
function module$static$init(obj)
    ...

The statements in Bench.xs at lines 1b and 2b mimic similar code seen earlier in prog.cfg at lines 1a and 2a, though the code here only executes if Bench.PClock remains unbound at this point in the process. As a rule, you should leverage the special function module$use—which in general already introduces other modules into the current configuration—as the locale for binding all unbound proxies associated with this module. Any proxies left unbound going forward will cause the entire configuration process to terminate with an appropriate error message, thus inhibiting any subsequent target-program build steps.

Our convention of assigning the results of an xdc.useModule directive to a suitably named XDCscript variable is just that—a convention. Clearly, this practice makes sense whenever the current code accesses properties of the module more than once, as with the local variable Bench inside of the earlier module$use function; indeed, you've already seen several Mod.xs examples that first declare file-level variables corresponding to module names, that then assign these variables inside of module$use, and that finally use these variables inside subsequent functions within the meta-implementation.

Though one could technically fold lines 1b and 2b of Bench.xs into a single assignment statement, dropping the variable altogether, we prefer this somewhat more pedantic idiom to ensure consistency. At the same, you'll occasionally see xdc.useModule directives appearing as standalone expressions, as they do inside the same Bench.xs module$use function defined earlier; declaring unused local variables in this case feels excessive. At the end of the day, just use common sense—and always remember that while you may write a piece of code just once, many others will read the same code many more times.

See also

 

[printable version]  offline version generated on 04-Aug-2010 21:08 UTC  
Copyright © 2008 The Eclipse Foundation. All Rights Reserved
Views
Personal tools
package reference