From RTSC-Pedia

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

RTSC Module Primer/Lesson 9

Modules, packages, and testing — bringing it all together


The example presented in this lesson—considerably richer than any we've seen so far—once again reinforces material covered earlier while simultaneously introducing some more programming idioms. After first examining a new module that illustrates further dimensions of the instance object life-cycle, we'll then touch upon the all-important topic of module testing. Here, we'll explore a simple organizational pattern involving a pair of RTSC packages—one containing modules, another containing test programs—which the xdc command can nevertheless manage as an integrated set.

Being our last programming example, we've tried to forge a "grand finale" that not only brings together many of the principal themes established in earlier lessons but also exposes a few advanced aspects of RTSC demanding deeper investigation on your part moving forward. Our next (and final) lesson will direct you towards other documentation resources found in the RTSC-Pedia that build upon everything you've already learned about programming in RTSC.

Contents

Specifying the module

The module acme.filters.Fir manages instance objects representing finite-impulse-response filters, each parameterized with their own set of coefficients and each maintaining their own internal history. After creating a filter instance, client applications would use the per-instance function apply to transform a frame of input data into a corresponding frame of output—as you'll see later on in one of the module's test programs. Now, however, we'll focus on the Fir module from the perspective of its supplier and highlight some extensions to the general programming idioms for instance modules first introduced in Lesson 8.

acme/filters/package.xdc
 
 
 
 
/*! Collection of DSP filter modules */    
package acme.filters {    
    module Fir;	
};
 
acme/filters/Fir.xdc
 
1
2
 
 
 
 
 
 
 
 
 
 
 
 
3
 
 
 
 
 
 
 
 
 
4
 
 
 
 
 
 
/*! Simple Finite Impulse Response (FIR) filter */
@InstanceInitError
@InstanceFinalize
module Fir {
 
instance:
 
    /*! Number of data-values per frame */
    config Int frameLen = 64;
 
    /*! Create a new filter
     *
     *  @param(coeffs)      filter coefficients
     *  @param(coeffsLen)   number of coefficients
     */
    create(Int16 coeffs[], Int coeffsLen);
 
    /*! Apply this filter and update its internal history
     *  @param(inFrame)    readonly frame of input data
     *  @param(outFrame)   next frame of output data
     */
    Void apply(Int16 inFrame[], Int16 outFrame[]);
 
internal:
 
    struct Instance_State {
        Int16 coeffs[];         /* create argument */
        Int coeffsLen;          /* create argument */
        Int frameLen;           /* instance config */
        Int16 history[];        /* filter history */
    }
}

As hinted in the last lesson, the declaration of create at line 3 of Fir.xdc includes additional arguments—here, an array of coefficients used internally by the per-instance function apply when filtering a frame of input data. Unlike per-instance configs—which each have a "reasonable" default value that clients can elect not to overwrite—callers of a corresponding Mod_create function declared with additional arguments must pass a corresponding set of values.

As a design guideline for module suppliers, you should prefer per-instance configs (with suitable defaults, of course) over required arguments to Mod_create whenever possible. As your module evolves over time, adding new per-instance configs (with defaults) would not require existing clients to change their source code; for technical reasons beyond the scope of this Primer, your clients wouldn't even need to re-compile their sources against your module's newly-generated header declaring its newly-expanded Mod_Params structure. You would not, however, have the same flexibility to change the overall signature of Mod_create over time without explicitly breaking source-level compatibility in your existing client code-base.

Anticipating the target-implementation of Fir (coming momentarily), the Instance_State declaration beginning at line 4 of Fir.xdc includes a history array whose length is a function of coeffsLen and frameLen. This suggests that instances objects created dynamically through client calls to Fir_create or Fir_construct would internally lead to further dynamic allocation of this history array from some suitable memory heap. Details aside for a moment, the new @InstanceInitError attribute back at line 1 declares that intializing per-instance state inside Fir can potentially raise an error (say, if memory were unavailable). Similarly, the @InstanceFinalize attribute at line 2 declares that the Fir target-implementation will explicitly intercept client calls to Fir_delete or Fir_destruct—presumably to free the previously-allocated history array associated with the current instance object.

Though independent from one another in theory, this pair of XDCspec attributes often appear together in practice: internal resource acquisition performed during instance creation can, in general, fail (@InstanceInitError); and resources successfully acquired upon instance creation must subsequently be relinquished during instance deletion (@InstanceFinalize).

Implementing the module (target-domain)

Turning now to Fir.c, pay particular attention to the expanded signature of Fir_Instance_init—which generalizes the Mod_Instance_init pattern first seen in the context of RandGen back in Lesson 8. You should also take note of Fir_Instance_finalize—another special function supporting one particular aspect of the instance object life-cycle.

acme/filters/Fir.c
 
 
 
 
 
 
1
 
 
 
 
2
 
 
 
 
 
 
 
 
 
 
3
 
 
 
 
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#include <xdc/runtime/Error.h>
#include <xdc/runtime/Memory.h>
#include "package/internal/Fir.xdc.h"
 
#include <string.h>
 
static SizeT historySize(Fir_Object *obj)    /* helper function */
{
    return (obj->coeffsLen + obj->frameLen - 1) * sizeof (Int16);
}
 
Int Fir_Instance_init(Fir_Object *obj, Int16 coeffs[], Int coeffsLen,
                      const Fir_Params *params, Error_Block* eb)
{
    obj->coeffs = coeffs;
    obj->coeffsLen = coeffsLen;
    obj->frameLen = params->frameLen;
 
    obj->history = Memory_calloc(NULL, historySize(obj), 4, eb);
    return 0;
}
 
Void Fir_Instance_finalize(Fir_Object *obj, Int status)
{
    Memory_free(NULL, obj->history, historySize(obj));
}
 
Void Fir_apply(Fir_Object *obj, Int16 inFrame[], Int16 outFrame[])
{
    Int i, j;
    Int32 sum;
 
    Int coeffsLen = obj->coeffsLen;
    Int frameLen = obj->frameLen;
 
    Int16 *history = obj->history;
    Int16 *coeffs = obj->coeffs;
 
    memcpy(&history[coeffsLen - 1], inFrame, frameLen * sizeof (Int16));
 
    for (j = 0; j < frameLen; j++) {
        sum = 0;
        for (i = 0; i < coeffsLen; i++) {
            sum += history[i + j] * coeffs[i];
        }
 
        *outFrame++ = (Int16)(sum >> 15);
    }
 
    memcpy(history, &history[frameLen], (coeffsLen - 1) * sizeof (Int16));    
}

If you've had any prior experience with FIR filters, our "textbook" implementation of Fir_apply beginning at line 4 should seem straightforward. But if you don't have a clue what's happening inside this function, don't worry about it—nothing more will be said about Fir_apply. Rather, our focus here remains on the instance object life-cycle in general and the functions Fir_Instance_init and Fir_Instance_finalize in particular.

Turning first to the definition of Fir_Instance_init beginning at line 2, its expanded signature results from two separate declarations found in the module's spec file:

  • the create declaration at line 3 of Fir.xdc contributed the additional coeffs and coeffsLen arguments, which are subsequently retained in the instance object; and

  • the @InstanceInitError declaration at line 1 of Fir.xdc contributed the additional eb argument, which is subsequently used to handle memory allocation errors.

As already hinted, Fir_Instance_init internally calls upon Memory_calloc to allocate and zero-fill a block of memory large enough to hold the history array associated with this instance object. Generalizing the familiar calloc function of standard C, the extra arguments to Memory_calloc give the caller additional degrees of control:

  • arg #1 designates the heap to use for the allocation, with NULL implying a system-wide default;
  • arg #2 designates the number of bytes to allocate, here returned by historySize defined at line 1;
  • arg #3 designates the addressing alignment for the allocation, represented by a power-of-two; and
  • arg #4 passes an Error_Block* used to raise/handle errors, with NULL triggering a default strategy.

The last argument passed to Memory_calloc of course tracks the eb argument passed to Fir_Instance_init; and this eb argument in turn reflects the last argument passed by the client to Fir_create or Fir_construct—which also happens to be of type Error_Block*. Once again, passing NULL as this final argument would trigger a default error strategy—typically, print an appropriate message and then abort execution whenever the program raises an error. In the case of the Fir_Instance_init function defined back at line 2, a NULL value for eb here implies that control would not return from Memory_calloc should the latter function raise an out-of-memory error.

Our next and final lesson will direct you to documentation specifically illustrating each facet of the xdc.runtime.Error module. Offering a step-up from the age-old practice in C of simply returning "status-codes"—which clients ignore anyway—the Error module drives some discipline and structure in the raising/handling of runtime errors, more aligned with modern software practices (think Java exceptions) but without the runtime overhead normally associated with these mechanisms (think C++ exceptions).

Besides contributing its eb argument, the original @InstanceInitError declaration back at line 1 of Fir.xdc also changed the return type of the Fir_Instance_init function from Void to Int. In general, the value returned from this form of the Mod_Instance_init function serves as a completion status which, if non-zero, signifies some sort of failure has occurred while initializing the current instance object and that the function has abruptly returned. In the latter case, RTSC effectively reverses the effects of the outstanding call to create or construct by performing the internal equivalent of a delete or destruct call on the current instance object.

The Fir_Instance_finalize function defined at line 3 of Fir.c effectively complements Fir_Instance_init, relinquishing any resources successfully acquired in the latter function. Automatically invoked in response to client calls to Fir_delete or Fir_destruct, the Instance_finalize function here calls Memory_free within arguments similar to those passed earlier to Memory_calloc after line 2. Note that since Fir_Instance_init returns a completion status (due to @InstanceInitError), RTSC automatically forwards this value to Fir_Instance_finalize if non-zero. Client-initiated calls to Fir_delete or Fir_destruct—implying an earlier corresponding call to Fir_create or Fir_construct had succeeded—pass 0 as the status argument to Fir_Instance_finalize.

Notwithstanding our earlier comment about xdc.runtime.Error and status-codes, the status value returned by Mod_Instance_init and forwarded to Mod_Instance_finalize generally enables the latter function to quickly ascertain (say, using a C switch statement) just how much "clean-up" processing the current instance object may require. With functions like Memory_free and Mod_delete already acting like idempotent no-ops when passed NULL arguments (and with Mod_Object structures automatically zero-initialized by RTSC), module suppliers can often layout Mod_Instance_finalize functions as "straight-line" code laced with appropriate case labels testing successively higher status values returned from Mod_Instance_init, with the (normal) case of 0 executing the entire function from top to bottom. Beside emphasizing that finalization truly reverses the effects of initialization, this technique typically yields faster and more compact implementations of Mod_Instance_finalize functions.

Implementing the module (meta-domain)

The new dimension of memory allocation in the target-function Fir_Instance_init results in corresponding additions to the special XDCscript meta-function instance$static$init defined in Fir.xs. Here, however, the history array associated with each statically-created instance object is likewise statically-allocated.

acme/filters/Fir.xs
1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
3
 
 
 
 
4
 
 
var Error, Memory, Program;
 
function module$use()
{
    Error = xdc.useModule('xdc.runtime.Error');
    Memory = xdc.useModule('xdc.runtime.Memory');
    Program = xdc.useModule('xdc.cfg.Program');
}
 
function instance$static$init(obj, coeffs, coeffsLen, params)
{
    obj.coeffs = coeffs;
    obj.coeffsLen = coeffsLen;
    obj.frameLen = params.frameLen;
 
    Memory.staticPlace(obj.history, 4, null);
    obj.history.length = historySize(obj);
}
 
function historySize(obj)    /* helper function */
{
    var sz =  Program.build.target.stdTypes.t_Int16.size;
    return (obj.coeffsLen + obj.frameLen - 1) * sz;
}

Since RTSC happens to invoke the special function module$use before instance$static$init, we've leveraged the former function to initialize some file-level variables declared at line 1. In the case of Memory—referencing the xdc.runtime.Memory module—instance$static$init invokes one of its meta-functions at line 2 to statically locate the history array associated with the current instance object. While clearly different from the target-side call to Memory_calloc back at line 2 of Fir.c, the 2nd and 3rd arguments to Memory.staticPlace analogously designate the addressing alignment and memory section (heap) used for this allocation.

Lesson 10 will direct you towards an appropriate User's Guide containing a series of examples that focus upon the xdc.runtime.Memory module, not only its more common usage in the target-domain but also its role in guiding the placement of statically-allocated data from within the meta-domain.

Following an idiom rooted in JavaScript, we've dimensioned obj.history at line 3 of Fir.xs by simply assigning to the special length property available on all XDCscript arrays. In this case, we've used a (private) historySize meta-function defined in Fir.xs in exactly the same way as Fir_Instance_init used a (private) historySize target-function defined at line 1 of Fir.c. Though highly "dynamic" in the meta-domain, obj.history (like obj itself) at the end of the day becomes a static C variable in the target-domain.

The Program.build property—yet another XDCscript object with many more properties of its own—encapsulates a wealth of information about the build context of the program currently being configured. One piece of information contained therein reflects the current program's RTSC target, whether originally named through an addExecutable call inside a package.bld script or else through a -t option to an xs xdc.tools.configuro command inside a client makefile. The Program.build.target object in turn has further properties that enable us to ascertain (among other things) the actual size of a standard type (here, Int16) on this particular target; use of this rather long-winded idiom at line 4 of Fir.xs directly parallels the C sizeof operator appearing after line 1 of Fir.c.

Testing the module

Suppliers of RTSC modules—like all good software engineers—will no doubt maintain a suite of test programs that systematically exercise all client-visible features of each module ("black-box" tests) as well as various aspects of each module's internal implementation ("white-box" tests). Consider, for example, a black-box test of acme.filters.Fir that applies a newly-created filter to a known frame of data and outputs the results. As a byproduct, this test program also uses the acme.utils.Bench module to measure the elapsed time of the call to Fir_apply.

FirTest1.c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
 
 
 
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#include <acme/filters/Fir.h>
#include <acme/utils/Bench.h>
#include <xdc/runtime/System.h>
 
#define COEFFSLEN 4
#define FRAMELEN 20
 
static Int16 coeffs[COEFFSLEN] = {20000, 21000, 22000, -3000}; 
 
static Int16 inFrame[FRAMELEN] = {0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9};
static Int16 outFrame[FRAMELEN];
 
static Void printOut();
 
Int main()
{
    Fir_Handle fir;
    Fir_Params params;
 
    Fir_Params_init(&params);
    params.frameLen = FRAMELEN;
 
    fir = Fir_create(coeffs, COEFFSLEN, &params, NULL);  /* create filter */
    Bench_begin("\t--> Portable Baseline");              /* start benchmark */
    Fir_apply(fir, inFrame, outFrame);                   /* run filter */
    Bench_end();                                         /* stop and display timing */
    printOut();                                          /* display results */
    Fir_delete(&fir);                                    /* delete filter */
 
    return 0;
}
 
static Void printOut()
{
    Int i;
    String comma = "";
 
    System_printf("\toutFrame = {");
    for (i = 0; i < FRAMELEN; i++) {
        System_printf("%s%d", comma, outFrame[i]);
        comma = ",";
    }
    System_printf("}\n");
}

As you would expect, the call to Fir_create at line 1 above takes two additional arguments, matching the declaration back at line 3 of Fir.xdc; and as we already noted, all of these parameters (including the NULL-valued Error_Block*) eventually arrive at Fir_Instance_init defined at line 2 of Fir.c. Similarly, the call to Fir_delete at line 2 above would internally invoke Fir_Instance_finalize defined at line 3 of Fir.c.

Turning to the corresponding meta-program for FirTest1, configuring the Bench module here takes on a new twist.

FirTest1.cfg
 
 
 
 
 
 
 
var Program = xdc.useModule('xdc.cfg.Program');
 
var Bench = xdc.useModule('acme.utils.Bench');
var Fir = xdc.useModule('acme.filters.Fir');
var System = xdc.useModule('xdc.runtime.System');
 
Bench.enableFlag = (Program.build.target.isa == "64P");

Once again, we've leveraged the wealth of information attached to the special xdc.cfg.Program module—here, to conditionally assign Bench.enableFlag based on the current program's target. Though somewhat contrived since Bench does work on all targets (though perhaps with less-than-useful accuracy on some), this fragment nevertheless illustrates a RTSC meta-programming technique that often eliminates the need for an #ifdef __64plus__ preprocessor directives in the corresponding target-program.

Not that C #ifdef directives are necessarily bad (though they often lead to ever-compounding clutter within never-quite-finished source files), you can largely avoid using #ifdef's altogether through the potent combination of meta-domain configuration, whole-program optimization, plus some design-patterns involving RTSC interfaces—the central focus of a companion Primer we'll have more to say about in the next lesson. Wherever and whenever possible, RTSC promotes a "write-once" approach to module implementation (especially in the target-domain) where a single C source file—one that's clean, readable, robust, and ultimately finished—can be effectively re-used across a wide range of targets, platforms, and applications without constant modification.

Managing a test-suite

Though we've only presented a single test program here, you could well imagine a package with multiple modules that in turn each implement multiple functions having multiple test programs—dozens, if not hundreds; and the problem only compounds itself when a single supplier offers multiple packages (acme.filters, acme.utils, and so on).

As in last few lessons, the package.bld script for a package like acme.filters will only build target libraries containing compiled implementations for each module in the package—but no executable programs.

acme/filters/package.bld
 
 
 
 
 
 
 
 
 
var Build = xdc.useModule('xdc.bld.BuildEnvironment');
var Pkg = xdc.useModule('xdc.bld.PackageContents');
 
var LIB_NAME = "lib/" + Pkg.name;
var LIB_SRCS = ["Fir.c"];
 
for each (var targ in Build.targets) {
    Pkg.addLibrary(LIB_NAME, targ).addObjects(LIB_SRCS);
}

Instead, all executable test programs pertaining to the acme.filters package will themselves reside in another package named (by convention) acme.filters.test that otherwise contains no modules. The package.xdc spec file for this new package in turn explicitly requires (using a new XDCspec keyword) the presence of other RTSC packages upon which the content of acme.filters.test in someway depends—including, but not limited to, our original acme.filters package.

acme/filters/test/package.xdc
1
 
 
 
 
 
 
requires acme.filters;
requires acme.utils;
 
/*! Companion tests for acme.filters */    
package acme.filters.test {    
    /* no modules */	
};

Turning now to the package.bld script for our acme.filters.test package—which easily scales when managing more than one test—the script features a doubly-nested for each loop used to not only iteratively build the same test program for multiple targets but to iterate over a set of tests as well.

acme/filters/test/package.bld
 
 
 
 
 
 
 
 
 
 
var Build = xdc.useModule('xdc.bld.BuildEnvironment');
var Pkg = xdc.useModule('xdc.bld.PackageContents');
 
var TEST_NAMES = ["FirTest1"];
 
for each (var targ in Build.targets) {
    for each (var testName in TEST_NAMES) {
        Pkg.addExecutable(testName, targ, targ.platform).addObjects([testName + ".c"]);
    }
}

We've said it before, and we'll say it again—package.bld is a program. And like any program written in any programming language, for loops enable us to aggregate a "closed" set of processing statements that otherwise can operate across an "open-ended" set of data values—in this case, the names of the individual tests in our test-suite. Taking this one step further, the names of the tests themselves often exhibit a pattern-like regularity—such as ModTesti—that itself suggests further opportunities for nested for loops; and complex mappings that share a common meta-program .cfg file across a set of related target-program .c files might be captured in a corresponding XDCscript data object likewise processed through some generic iterators. Whatever the case may be, you can "shape" the package.bld script to best reflect the inherent structure of the problem at hand.

In practice, development of modules inside a package like acme.filters and development of test programs for these modules inside a package like acme.filters.test will proceed in parallel. To streamline the repetitive re-builds of either package as well as to ensure both packages remain in sync with one another before executing any tests, we'll rely upon an extended form of the xdc command that operates on a set of packages designated using the -P option and its variants.

xdc [goal ...] -P  pkg-dir ... build goal(s) in each package directory
xdc [goal ...] -PR dir ... build goal(s) in all packages found by recursively searching each directory
xdc [goal ...] -PD pkg-dir ... build goal(s) in each package directory along with all dependent packages

For example, you could invoke a single xdc test -PR . command from within the «examples»/acme/filters directory to systematically build both packages before executing all test programs. Or, if you prefer, sitting inside the «examples»/acme/filters/tests package you can achieve the same results by invoking xdc test -PD . which automatically recognizes acme.filters as a dependent package (per line 1 of its package.xdc spec). In the latter case, the -PD option will also re-build the dependent acme.utils package—just in case you happened to make a change to (say) the Bench module.

All we can say about this incredibly powerful generalization of the already-potent xdc command is:  try it, you'll like it!

See also

Command - xdc eXpanDed C package build command
XDCspec - @InstanceInitError This module's instance initialization can fail
XDCspec - @InstanceFinalize This module's instances require finalization
 
xdc.runtime.Error.Block Client documentation for xdc.runtime.Error.Block

[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