Skip to content

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)

  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
1
2
3
4
5
6
7
8
package bob.pkg

import Mod1 as ModX     # omit redundant 'from' clause

export ModX             # a logical module

composite CompC             
end

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
package geof.pkg

from bob.pkg import Mod1      # concrete delegate
from dave.pkg import Mod2     # exposes abstract proxy

composite CompC
    # no features
end

def em$configure()
    Mod2.ModX ?= Mod1         # bind proxy to its delegate
end

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
package bob.pkg

module Mod1

    config flag: bool
        #   ^| Enable some functionality
    # other public features

private:

# etc...
dave.pkg/Mod2.em
package dave.pkg

module Mod2

    config count: uint8
        #   ^| Establish some threshold
    # other public features

private:

# etc...

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
package geof.pkg

from bob.pkg import Mod1
from dave.pkg import Mod2

composite CompC
end

def em$configure()
    Mod2.ModX ?= Mod1         # bind proxy to its delegate

    Mod1.flag ?= true         # assign config parameters
    Mod2.count ?= 100
end

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 con­figuration 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.

geof.pkg/CompC.em
package geof.pkg

from thom.pkg import GenT {flag: true, count: 100} as Clone1
from thom.pkg import GenT {flag: false, count: 200} as Clone2

export Clone1
export Clone2

composite CompC
end

def em$configure()
    # assign Clone<n> config parameters
    # bind Clone<n> proxies to delegates
    # delegate other proxies to Clone<n>
end

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
package thom.pkg

template GenT

    config flag: bool
        #   ^| Enable some functionality
    config count: uint8
        #   ^| Establish some threshold
end

def em$generateUnit(pn, un)
            |-> package `pn`
            |->
            |-> module `un`
            |->
    if flag
            |-> const MAX: Uint8 = `count`
    end
            # generate remainder of this module
end

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.