Accessing variables in the Blech environment

Access to C variables in the environment creates singleton sub-programs.

Environment variables

Environment variables are either read-only inputs or read-write outputs. Every environment variables has a counterpart in the Blech environment. Therefore, they are annotated as @[CInput ...] or @[COutput ...]. Environment variables are classified by the keyword extern. A extern let definition must be annotated with a @[CInput ...]. An extern var definition must be annotated with a @[COutput ...].

@[CInput (binding = "theSensor", header = "sensors.h")]
extern let sensor: uint8 

@[COutput (binding = "spiIsReady", header = "spi.h")]
extern var spiReady: bool

Since the external C variables bound to the environment variables live in the environment they do not follow the synchronous semantics. They might be volatile changing their value during a reaction. They might be read or written asynchronously by the environment.

In order to synchronize these C variables, the Blech program holds a buffered value of the external value in the environment variable.

The Blech runtime system takes care of the synchronisation. Before the reaction, the values of all external C variables are copied into the environment variable. After the reaction, the values of the extern var variables are copied into the external C variables.

The implementation is allowed to distribute these copying operations into the code generated for each activity, that defines extern variables.

Environment variables can only be declared in an activity. Functions can only access environment variables via the parameter list.

External read-write C variables create singletons

An external C variable is a global variable, which is in danger to be accessed concurrently via the environment variable. While Blech in general prevents this danger by not allowing the declaration of global variables at all, it needs more effort to guarantee this for external C variables.

activity handleCVariables()
    
    @[CInput (binding = "theSensor", header = "sensors.h")]
    extern let sensor: uint8
    @[COutput (binding = "spiIsReady", header = "spi.h")]
    extern var spiReady: bool

    repeat
        if sensor > 10 then
            spiReady = true
        end
        await true
    end
end

In order to comply to the single-writer principle, declaring an extern var variable in an activity restricts this activity to be instantiated only once. The declared activity is a singleton and does not allow multiple instances. An singleton activity cannot be called concurrently, but only sequentially. Note that this is only necessary for extern var declarations. An extern let variable does not create a singleton. Different instances of such an activity can have separate buffers of the external C variable, which might have different values in the same reaction if the external C variable is volatile. It is the responsibility of the programmer not to share external C variables in different extern var declarations. Two or more extern let declarations are allowed to have the same annotation @[CInput ...].

Singletons and separate compilation

If a module exports a singleton activity, the signature needs to reflect this in order to enable a correct causality analysis. The signature for the above activity handleCVariables looks like the following

singleton handleCVariables.spiReady 
activity handleCVariables()

It shows the activities prototype, exposes the unique names of the external variables and shows their annotations. Showing the annotations allows to check the binding to C variables also for modules where the implementation is hidden.

The diamond call problem

If an activity is a singleton, it still can be called from several activities.

activity firstUsage()
    run handleCVariables()
end

activity secondUsage()
    run handleCVariables()
end

Again firstUsage and secondUsage can not be called concurrently.

In order to check this via the signature, the calling activities, inherits the singletons from the called activity. In a module signature these activities occur as follows

singleton handleCVariables.spiReady
activity firstUsage()

singleton handleCVariables.spiReady
activity secondUsage()

If we combine activities with different singletons, they must not be called concurrently, if they share common singletons. For example assume the following module signature.

signature Module 

singleton handleOtherCVariable.theExternVar
activity handleOtherCVariable()

A calling activity might inherit all singletons

import Module

activity thirdUsage()
    cobegin
        run handleCVariables()
    with
        run handleOtherCVariable()
    end
end

The deduced signature is

import Module

singleton handleCVariables.spiReady, 
          Module.handleOtherCVariable.theExternVar
activity thirdUsage()

Activity thirdUsage cannot be called concurrently to firstUsage or secondUsage because their singletons overlap.

Structured access to external variables

Environment variables can also be referenced from a struct type, like normal Blech variables.

struct MyCVariables
    var x: int32
    let ref sensor: nat8
    var ref spiReady: bool
end
activity referToCVariables()
    @[CInput (binding = "theSensor", header = "sensors.h")]
    extern let sensor: nat8 

    @[COutput (binding = "spiIsReady", header = "spi.h")]
    extern var spiReady: bool

    var mcvs: MyCVariables = { x = 0, sensor = sensor, spiReady = spiReady }
end

If we use both activities referToCVariables and handleCVariables in a Blech program we obviously made a mistake. Both activities write to the same external C Variable "spiIsReady". The single-writer principle is broken. The semantics of the program is undefined concerning the environment access. In general working with externals is unsafe. In this particular case, it is the responsibility of the programmer to take care, not to share the same COutput between different extern var declarations.

The signatures of both activities cannot reveal this error.

singleton referToCVariables.spiReady
activity referToCVariables()

singleton handleCVariables.spiReady
activity handleCVariables()

Idea: It should be the responsibility. of the compiler to collect the environment of a Blech program across all modules in order to allow for a code review supported by such a description file.

Hints: extern var and extern let declarations cannot be declared inside functions. Use extern let declarations when possible in order to prevent the propagation of the singleton property.

Since signatures are deduced by the compiler, the whole checking of singletons and the generation of appropriate signatures is done by the compiler.

Note: A conventional embedded program is full of singletons, not only created by extern var declarations, but especially by global variables. In order to maintain the single-writer principle by programming discipline it is almost inevitable, that all functions are only called once in a task list. This makes reuse and testing extremely difficult. Only the rather small number of services is reusable and rather easily testable.

Last modified April 28, 2020: Updates on language evolution (5efeab3)