Assembling application elements
By leveraging the proxy
– interface
design pattern, EM modules exhibit component-like qualities – enabling third-party integrators to effectively compose a set of modules that otherwise have no direct knowledge of one another.
To facilitate general aggregation and assembly of discrete modules into larger application entities, the EM language introduces a multi-faceted construct known as a composite
to service these needs.
Aggregating modules
Like modules and interfaces, each EM composite resides under a named package and will import other units – modules, interfaces, even composites – into its top-level namespace. Consistent with their role as higher-level collection points for discrete modules, EM composites will often import surprisingly large numbers of modules in practice.(1)
- ti.distro.cc23xx/McuC for example
In their most elementary form, EM composites will selectively export
a group of concrete modules under logical names known to higher-level clients.
bob.pkg/CompC.em | |
---|---|
After re-labeling bob.pkg/Mod1
at line 3 using an as
clause, the export
directive at line 5 publicizes the name ModX
; clients importing CompC
can then use the (logical) module named ModX
, otherwise oblivious to its true identity as bob.pkg/Mod1
. While not declared explicitly, ModX
in fact belongs to the ModI
family of modules – serving as a concrete delegate suitable for binding to an abstract proxy implementing a common programmatic interface.
Component-oriented programming
Programming with components – which takes modularity, separation of concerns, and resilience in the face of change to entirely new levels – dictates that (abstract) interfaces define all functional interactions between independently-replaceable elements. As such, each (concrete) software component "provides" and "requires" services defined by some set of interfaces – not unlike the standardized "plugs" and "sockets" found in hardware components. Through disciplined use of export
directives within EM composites and proxy
declarations within EM modules, we can emulate the provides – requires paradigm that forms the backbone of all component-oriented systems.
Binding proxies
Besides aggregating modules, EM composites will often take on the task of "wiring" together a set of modules into a more integrated assembly – especially modules like our last version of dave.pkg/Mod2
, which utilize local proxies to further decouple themselves from potential suppliers. To illustrate:
geof.pkg/CompC.em | |
---|---|
Reflecting its higher-level position within the application hierarchy, this particular composite reaches across multiple packages starting at line 3 when importing a set of modules. The actual binding of bob.pkg/Mod1
to dave.pkg/Mod2
via the latter's ModX
proxy then occurs at line 11, using a special single-assignment operator [ ?=
] which we'll explain later. We'll also have more to say about the role played by the intrinsic function em$configure
.
EM distro packages
To use the EM language, you'll need an EM distro – a multi-tiered sub-system that melds portable runtime content in packages like em.utils
with hardware-specific content in packages like ti.mcu.cc23xx
. By design, each EM distro will publish a top-level composite with a fully-qualified name like ti.distro.cc23xx/BoardC. This BoardC
composite in turn builds upon other composites – whether hardware-specific ti.distro.cc23xx/McuC or else portable em.mcu/CommonC.
Configuring parameters
Just as individual functions may have parameters, EM modules as a whole can publicize a special kind of parameter known as a config
which clients in turn can selectively assign inside of em$configure
. To illustrate typical usage, let's expand our earlier versions of bob.pkg/Mod1
and dave.pkg2/Mod2
to now expose some new client-visible features in the form of configuration parameters.
bob.pkg/Mod1.em | |
---|---|
dave.pkg/Mod2.em | |
---|---|
As you might glean from the special documentation comments, the flag
parameter declared at Mod1
6 and the count
parameter declared at Mod2
6 should each render these modules more flexible than before, and hence increase opportunities for clients to (re-)use Mod1
and Mod2
in a wider range of applications. Syntactically similar to const
and var
declarations within the language, config
parameters have a very special semantic property:
An EM config
behaves like an assignable var
at build-time, but like a read-only const
at run-time.
Once we delve into the EM program life-cycle – from build-time through run-time – you'll understand the rationale behind this paradox, as well as more fully appreciate the implications of module configuration for resource-constrained embedded applications. For now, suffice it to say that EM composites serve as an ideal site for assigning module config
parameters when assembling application programs. Expanding our earlier example:
geof.pkg/CompC.em | |
---|---|
Just as we bound proxies to delegates, the single-assignment statements at lines 12 and 13 bind parameter values consistent with the types used in corresponding config
declarations found at Mod1
6 and Mod2
6. In this light, clients can treat proxies as a special kind of configuration parameter – assigned module
values implementing a declared interface
type.
More configuration examples
The em$preconfigure
and em$configure
functions defined within McuC provide a realistic example of configuration – starting with a call to BoardInfo.readRecordH
, which returns a data-structure of board-specific parameter values read from a YAML source file. The (portable) implementation of readRecordH
found within the em.utils/BoardInfo module hints at the range of programmability we can bring to bear during configuration.
Instantiating templates
The EM language provides a general-purpose template
mechanism for synthesizing other source-level artifacts – from fragments of C code to complete .em
files – during the program build process. The latter scenario, which we'll focus on here, enables suppliers to deliver concrete modules in a more generic embodiment – one that clients will frequently instantiate within the context of an EM composite.
Starting from the client's perspective, the following composite effectively manufactures a pair of new modules – locally aliased as Clone1
and Clone2
– by instantiating a common imported template named thom.pkg/GenT
.
Using an expanded form of import
directive at lines 3 and 4, this composite works with (synthesized) modules Clone1
and Clone2
no differently than how we employed the bob.pkg/Mod1
or dave.pkg/Mod2
modules defined earlier. Since Clone1
and Clone2
have no prior outside identity, composites will often export
these newly-formed modules for use by higher-level clients; composites may further configure and assemble these synthesized modules within the body of their own em$configure
function.
The flag
and count
values bound at lines at lines 3 and 4 above – which shape the essential characteristics of the synthesized Clone1
and Clone2
modules – ultimately correspond to public configuration parameters declared within thom.pkg/GenT
:
thom.pkg/GenT.em | |
---|---|
Like any other config
, the parameters declared after line 5 become assignable variables at program build-time – in this case, via import
directives beginning at line 3 of CompC
. The GenT
template will then consume these configuration parameters within the body of its em$generateUnit
function, which synthesizes lines of source code using special EM output statements prefixed by the |->
symbol.
The flag
config declared at 6 impacts the generated output by controlling execution flow within em$generateUnit
; by contrast, this function directly interpolates the count
config declared at 9 within the generated output. When invoked, em$generateUnit
receives pn
and un
arguments bound to strings like "geof.pkg"
and "CompC__Clone1"
– reflecting the original context in which the geof.pkg/CompC
composite imported and instantiated the GenT
template back on the earlier line 3.
More template examples
Our McuC composite instantiates a module-per-pin using this GpioT template. Inspecting this template's em$generateUnit
function, note how the pin
config ultimately shapes the synthesized module through interpolation within the |->
output statements. Though not a rule, synthesized modules will often implement an interface that captures their commonality – em.hal/GpioI in the example at hand.
To further grasp the special role played by composite
and template
units, let's proceed onward to Chapter 3 and explore EM's rather unique build flow.