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 11

Module abstraction — specifying & implementing the IGen interface

We'll begin by introducing a new module named SineGen which, like the RandGen module of Lesson 8, manages instance objects representing distinct numeric sequences. Using the per-instance function SineGen_next, clients can retrieve successive values of a sine wave generated by an underlying object; and using per-instance configuration parameters for designating frequency and sampling rate, clients can shape the characteristics of the generated sine wave when creating a SineGen instance.

While you certainly wouldn't expect any sharing of code in the implementation of these two modules—the algorithms and state variables for generating random numbers and sine waves have little in common—the specification of RandGen and SineGen reveals an obvious similarity in their external behavior which we'll then formally capture as a RTSC interface named IGen.

Contents

Another sequence generator

Though we've gone out of our way to basically clone SineGen.xdc from the original RandGen.xdc, preserving common feature names wherever possible, differences nevertheless arise between the specs of these otherwise similar modules. For instructional purposes, we've omitted some important details here to facilitate a more meaningful side-by-side comparison.

RandGen.xdc
 
 
 
 
 
 
 
 
 
 
1a
 
 
 
 
 
 
 
 
/*! Random-number sequence generator */
module RandGen {
 
    const Int16 MAXVALUE = (1U << 15) - 1;   
 
instance:
 
    create();
 
    config Int16 range = MAXVALUE;
    config UInt seed = 1;
 
 
    Int16 next();
    Void nextn(Int16 *buffer, Int count);
 
internal:    
    /* implementation details ... */
}
SineGen.xdc
 
 
 
 
 
 
 
 
 
 
1b
 
 
 
 
 
 
 
 
/*! Sine wave sequence generator */
module SineGen {
 
    const Int16 MAXVALUE = (1U << 15) - 1;   
 
instance:
 
    create();
 
    config Int range = MAXVALUE;
    config Int freq = 1;
    config Int rate = 256;
 
    Int16 next();
    Void nextn(Int16 *buffer, Int count);
 
internal:    
    /* implementation details ... */
}

Dismissing some obvious differences we'd expect to find following the XDCspec internal keyword in each spec file—remember, these modules share nothing in their underlying implementations—the per-instance configs declared after lines 1a and 1b respectively reflect unique aspects of creating RandGen versus SineGen instances. But except for assigning these few configs, you could easily imagine virtually identical client programs using these two modules.

RandGenProg.c
 
 
 
 
 
 
 
 
 
 
2a
 
 
 
3a
 
 
 
 
 
#define COUNT 10
 
Int main()
{
    RandGen_Handle rgInst;
    RandGen_Params rgParams;
    Int i;
 
    RandGen_Params_init(&rgParams);
    /* selectively assign configs ... */
    rgInst = RandGen_create(&rgParams, NULL);
 
    for (i = 0; i < COUNT; i++) {
        System_printf("%d ",
            RandGen_next(rgInst)); 
    }
    System_printf("\n");
 
    return 0;
}
SineGenProg.c
 
 
 
 
 
 
 
 
 
 
2b
 
 
 
3b
 
 
 
 
 
#define COUNT 10
 
Int main()
{
    SineGen_Handle sgInst;
    SineGen_Params sgParams;
    Int i;
 
    SineGen_Params_init(&sgParams);
    /* selectively assign configs ... */
    sgInst = SineGen_create(&sgParams, NULL);
 
    for (i = 0; i < COUNT; i++) {
        System_printf("%d ",
            SineGen_next(sgInst)); 
    }
    System_printf("\n");
 
    return 0;
}

Interesting to note, had we instead passed NULL to the Mod_create calls at lines 2a and 2b—accepting default values for all per-instance configs—this pair of client programs would become perfect clones of one another. Regardless of how we've created rgInst and sgInst, though, both programs use identical logic to manipulate these instance objects around lines 3a and 3b.

Lesson 15 explores techniques for effectively collapsing these sorts of "program clones" into a single generic client application that could comprehend any sort of sequence-generation module—present or future.

Abstracting RandGen and SineGen

As programmers, we're constantly looking for opportunities to remove unnecessary redundancy from our source code. Termed the DRY principle by others—meaning Don't Repeat Yourself—good software engineering practice encourages us to define programmatic entities no more than once. Indeed, the very thought of taking a "copy-and-modify" approach to software construction—whether specifying new modules or developing client programs—should always propel us to find a single, more generic solution to the problem at hand.

Clearly, the specs of RandGen and SineGen afford us with ample opportunities to find some common ground that captures the essence of both modules—indeed, all modules that manage instance objects representing distinct numeric sequences. Consider, then, our first XDCspec interface definition canonically named stdsorg.math.IGen.

stdsorg/math/package.xdc
 
 
 
 
/*! Abstract math support */  
package stdsorg.math {
    interface IGen;
};
 
stdsorg/math/IGen.xdc
 
1
 
 
 
 
 
 
 
 
 
2
 
 
 
 
 
 
 
 
 
 
 
/*! Abstract sequence generator */
interface IGen {
 
   /*! Largest possible number in a sequence */
   const Int16 MAXVALUE = (1U << 15) - 1;
 
instance:
 
   create();
 
    /*! Numbers between (0, `range`] */
    config Int16 range = MAXVALUE;
 
   /*! Generate the next number in this sequence */
   Int16 next();
 
   /*! Generate the next `count` numbers in this sequence
    *
    *  @param(buffer) output buffer
    *  @param(count) number of numbers
    */
    Void nextn(Int16 *buffer, Int count);
}

Unlike RTSC modules—characterized by a public specification and an internal implementation—a RTSC interface by itself has no implementation; you'll never see the keyword internal in an interface spec like IGen.xdc, and you'll never find a corresponding IGen.c file implementing the functions declared in this interface. Put another way, a RTSC interface is "pure spec"—an abstraction that ultimately defines an open-ended family of conforming modules that each implement a common set of specified functions in their own unique manner.

A bit of history.... The concept of an abstract interface devoid of any implementation first achieved prominence with the IDLs or interface definition languages [sic] that lie at the heart of component-models like COM and CORBA, used in concert with C++ several decades ago; interestingly enough, C++ still hasn't admitted interface into its set of language keywords, though as a practical matter one can simulate the construct through disciplined use of (non-final) classes defining only constants, types, and so-called pure virtual functions. More recently, languages like Java and C# took the logical next-step and elevated the concept of an abstract interface to first-class status through a corresponding keyword; XDCspec has simply followed the lead of others. Though embodied a little differently in each language environment, one common practice surrounding interfaces seems to have stuck from outset—a convention whereby the names of interfaces always begin with 'I', as in IGen. We don't plan to rock the boat here.

To declare its potential membership in such a family, a module simply inherits a previously defined interface as the conceptual starting point for its own specification. As an illustration, the (new) package bravo.math2 contains a RandGen and a SineGen module that each inherit the stdsorg.math.IGen interface. Compared with the original bravo.math.RandGen module introduced back in Lesson 8, our new RandGen.xdc only contains declarations not already covered within the IGen.xdc interface definition.

bravo/math2/package.xdc
 
 
 
 
 
 
 
requires stdsorg.math;
 
/*! Collection of math modules */  
package bravo.math2 {
    module RandGen;
    module SineGen;
};
 
bravo/math2/RandGen.xdc
 
3
 
 
 
 
4
 
 
 
 
 
 
 
 
/*! Random-number sequence generator */
module RandGen inherits stdsorg.math.IGen {
 
instance:
 
    /*! Random-number seed value */
    config UInt seed = 1;
 
internal:    
 
    struct Instance_State {
        Int16 range;  /* range parameter value */
        ULong next;   /* next value in sequence */
    };
}

The inherits clause on line 3 conceptually prefixes our new RandGen.xdc spec with all of the declarations following line 1 of IGen.xdc, as if we had explicitly written them out—which we originally did back in Lesson 8. In general, a RTSC module that inherits a RTSC interface will extend the latter's spec by introducing additional client-visible features. In the case of RandGen, line 4 of its module spec introduces another per-instance config (named seed) above and beyond the range config already declared back at line 2 of IGen.xdc.

The spec for bravo.math2.SineGen follows suit, not only by inheriting stdsorg.math.IGen but also by augmenting the programmatic features already defined in this interface with additional client-visible elements.

bravo/math2/SineGen.xdc
 
 
 
 
5
 
 
 
 
6
 
 
 
 
 
 
 
7
 
8
 
 
 
 
 
/*! Sine wave sequence generator */
module SineGen inherits stdsorg.math.IGen {
 
    /*! Size of internal sine table */
    config Int sineTabSize = 256;
 
instance:
 
    /*! Sine wave frequency */	
    config Int freq = 1;
 
    /*! Sine wave sample rate */	
    config Int rate = 256;
 
internal:    
 
    /*! internal sine table supporting generation */
    config Int16 sineTab[];
 
    struct Instance_State {
        Int shift;  /* gain adjustment factor */
        Int step;   /* index increment value */
        Int index;  /* current table index */
    };
}

Besides a pair of per-instance configs declared after line 6, SineGen also extends the IGen spec with a module-wide configuration parameter named sineTabSize declared at line 5. This config enables SineGen clients to control the size of an internal table of sine values used in the implementation of SineGen_next, essentially allowing generation of higher-frequency sine waves by increasing the module's memory footprint.

As you'll see momentarily in the implementation of the SineGen module, we've elected to represent the sine table itself as an internal module-wide config declared at line 7—a static array of const data in the target-domain that we'll dimension and initialize in the meta-domain. To maximize runtime performance, you should always prefer module-wide configs over state variables whenever representing readonly information in the target-domain.

Beyond the special Module_State or Instance_State structure definitions seen in most of our examples, the internal section of a module .xdc file can declare other sorts of programmatic entities—constants, types, configs, even functions—that in some way support the module's private implementation; and of course, all of these internal declarations logically remain hidden from the client.

Implementing IGen modules

Before turning to SineGen—and its rather noteworthy technique of leveraging meta-domain functions to compute target-domain data-sets—let's quickly address the implementation of bravo.math2.RandGen which (like SineGen) inherits much of its specification from the stdsorg.math.IGen interface. As you've already seen, despite migrating to the new bravo.math2 package—and now featuring a more concise XDCspec source file—the module's internal details haven't changed from the original RandGen.xdc file from Lesson 8.

And now comes the interesting part.... The implementation of bravo.math2.RandGen is absolutely identical to the RandGen.c target-implementation first seen in Lesson 8; and ditto for the RandGen.xs meta-implementation seen later on in the same lesson. Even though the new RandGen module inherits client-visible functions like next and nextn from the IGen interface, the module supplier continues to implement these functions as if we had directly specified them in RandGen.xdc—which, by virtue of the inherits clause at line 3 of this file, we effectively have.

Each RTSC module defines a distinct programmatic scope that in turn introduces named elements such as configs or functions; and like any scope, no two elements contained therein can have the same name. By inheriting a RTSC interface—which itself may declare other elements—an heir module basically pre-populates its own scope with a common set of declarations. The names of any additional elements specified by the inheriting module cannot, of course, conflict with any earlier declarations associated with the inherited interface.

Like RandGen, the target-implementation of SineGen contains no direct reference to the IGen interface—as if we had explicitly declared the functions next and nextn in SineGen.xdc.

bravo/math2/SineGen.c
 
 
1
 
 
 
 
 
 
 
 
 
 
 
 
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#include "package/internal/SineGen.xdc.h"
 
Void SineGen_Instance_init(SineGen_Object *obj, const SineGen_Params *params)
{
    Int r;
 
    obj->shift = 15;
    for (r = params->range; r > 0; r >>= 1) {
        --obj->shift;
    }
 
    obj->step = SineGen_sineTabSize * params->freq / params->rate;
    obj->index = 0;
}
 
Int16 SineGen_next(SineGen_Object *obj)
{
    Int res;
 
    res = SineGen_sineTab[obj->index] >> (obj->shift);
    obj->index = (obj->index + obj->step) % SineGen_sineTabSize;
 
    return res;
}
 
Void SineGen_nextn(SineGen_Object *obj, Int16 *buffer, Int count)
{
    Int i;
 
    for (i = 0; i < count; i++) {
        *buffer++ = SineGen_next(obj);
    }
}

The function SineGen_next defined at line 2 retrieves successive values in the sequence by stepping through the data held in the internal sineTab config and then scaling the result. The special Instance_init function defined at line 1 computes the obj->step value based on the module-wide config sineTabSize as well as the per-instance configs freq and rate; the per-instance config range similarly determines the value for obj->scale used later.

Turning to the meta-domain, SineGen.xs defines two of the special functions seen in earlier module implementations.

bravo/math2/SineGen.xs
 
 
3
 
 
 
4
 
 
 
5
 
 
 
 
 
 
 
 
 
function module$static$init(obj, mod)
{
    mod.sineTab.length = mod.sineTabSize;
    var incr = Math.PI / (mod.sineTabSize / 2);
 
    for (var i = 0; i < mod.sineTabSize; i++) {
        mod.sineTab[i] = Math.round(Math.sin(incr * i) * mod.MAXVALUE);
    }
}
 
function instance$static$init(obj, params, mod)
{
    obj.shift = 15;
    for (var r = params.range; r > 0; r >>= 1) {
        --obj.shift;
    }
 
    obj.step = mod.sineTabSize * params.freq / params.rate;
    obj.index = 0;
}

Starting at the bottom, the meta-function instance$static$init beginning at line 5 tracks the corresponding Instance_init target-function defined back at line 1 in an obvious way (but with C's use of the underscore and arrow replaced here with the "object-oriented" dot notation of XDCscript). Though normally not necessary, this particular implementation of instance$static$init leverages a special third argument which references the current module—SineGen in this case—and provides direct access to module-wide configs such as sineTabSize.

With the internal config sineTab serving as a module-wide resource, the special meta-function module$static$init hence assumes responsibility for dimensioning our sine wave table at line 3 as well as assigning its individual elements at line 4. Note that the global identifier Math refers here to a built-in XDCscript object whose named properties—PI, round, and sin—represent corresponding mathematical constants and functions.

Here too, the special meta-function module$static$init uses an extra argument named mod to access module-wide properties such as sineTab and MAXVALUE. Unlike C, XDCscript functions can omit naming any trailing arguments like mod (and even params) when not needed within the function's implementation.

Further abstracting SineGen

As we'll see throughout this Primer, one could imagine several viable implementations for a module like SineGen—say, by making trade-offs between time and space or between speed and accuracy; indeed, one could even imagine several of these implementations co-existing in the same target program. But every RTSC module must stay a singleton—a unique pairing of a specification and an implementation together associated with a (long) globally-unique name such as bravo.math2.SineGen.

Though suppliers and clients alike will invariably employ short names such as SineGen_sineTab or SineGen_next when either implementing or using a module like bravo.math2.SineGen in the target-domain, these names in fact represent generated #define aliases that expand to the actual C identifier associated with each spec'd config or function—bravo_math2_SineGen_sineTab__C or bravo_math2_SineGen_next__E in this case. From the linker's perspective, no more than one definition of these symbols can of course occur within any particular program image. Though somewhat unnerving at first, long canonical identifiers prefixed with globally-unique package names and suffixed with a well-defined set of codes can actually help developers and integrators alike pinpoint the source of programming errors originating inside different packages produced by different suppliers.

Given the uniqueness constraint imposed upon each RTSC module, multiple implementations of the same module essentially implies multiple modules—each individually and distinctly named—that otherwise would share common elements in their respective specifications. Once again, we can express this commonality through an abstract interface that specifies those client-visible features characterizing any module that purports to generate sine waves.

ISineGen.xdc
 
1
 
 
 
 
2
 
 
 
 
/*! Abstract sine wave generator */
interface ISineGen inherits stdsorg.math.IGen {
 
instance:
 
    /*! Sine wave frequency */	
    config Int freq = 1;
 
    /*! Sine wave sample rate */	
    config Int rate = 256;
}

As you might have already imagined from looking at line 1, potential heirs of our original stdsorg.math.IGen interface can in general include any sort of spec'd RTSC unit—not just concrete modules like bravo.math2.RandGen or bravo.math2.SineGen but other abstract interfaces (such as ISineGen) as well. Here again, inheritance of stdsorg.math.IGen effectively pre-populates the ISineGen scope with a set of named elements that include the per-instance config range and the per-instance function next; the declarations beginning at line 2 of ISineGen.xdc then proceed to extend the scope of this interface with additional elements pertinent to sine wave generation.

By refactoring the interface hierarchy along these lines, the spec of our original SineGen module would now contain only those declarations relevant to the (unique) implementation of this particular module. Applying this pattern more broadly, you could imagine a family of modules that all provide the same external functionality—that spec'd in the ISineGen interface—but implemented differently from one module to the next.

SineGen.xdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/*! Sine wave sequence generator */
module SineGen inherits ISineGen {
 
    /*! Size of internal sine table */
    config Int sineTabSize = 256;
 
instance:
    /* nothing to add here */
 
internal:    
 
    /*! internal sine table supporting generation */
    config Int16 sineTab[];
 
    struct Instance_State {
        Int shift;  /* gain adjustment factor */
        Int step;   /* index increment value */
        Int index;  /* current table index */
    };
}
SineGenA.xdc
 
 
 
 
 
 
/*! Sine wave sequence generator A */
module SineGenA inherits ISineGen {
    /* additional client features */
internal:    
    /* implementation details */
}

SineGenB.xdc
 
 
 
 
 
 
/*! Sine wave sequence generator B */
module SineGenB inherits ISineGen {
    /* additional client features */
internal:    
    /* implementation details */
}

Remarkably, despite these changes in the inheritance hierarchy—reflected in the more abbreviated SineGen.xdc spec seen here—the source of our original SineGen.c target-implementation would remain absolutely identical.

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