Design by Contract

The CWL uses assertions to enforce a lightweight runtime API contract between the implementor and the library. The API contract ensures that the implementor is properly invoking the library at runtime and is designed to be used by both debug and production builds!

When the implementor breaks the contract, an assertion handler is invoked. Further execution is prevented, an error is reported, and the system controller is reset.

For some implementors this might sound like a terrible idea.

But it is the opposite, because it prevents both subtle and not so subtle runtime errors to be detected, identified, and fixed early in the development cycle, and rare field issues to be diagnosed remotely.

One nice artifact of Design by Contract is that there are fewer error codes returned by the CWL compared to a traditional library, and therefore the implementor's error handling code is far simpler.

Avoidable vs Unavoidable Errors

As with many tools, Design by Contract should not be used, and is not used indiscriminately either by the CWL or the implementor. Certain types of unavoidable errors should not and are not treated as contract violations.

For example, the CWL must verify the CRC of incoming messages on the serial interface. If the computed CRC does not match the expected CRC then the CWL will ignore the packet, but it will not throw an assertion. It is unavoidable that the data received by the CWL can't always be expected to conform to the requirements of the CWL contract because that data originates from another source.

In contrast, if an implementor invokes a CWL function with a parameter value not supported by the function then the CWL will throw an assertion instead of returning an error code. It is an avoidable error that the parameter value is outside of the allowable contract enforced range because it originates internally.

Disabling Assertions

Do not disable CWL assertions either globally by implementing an assertion handler that returns, or piecemeal by commenting out or disabling the Contract Primitives in part or in whole. Doing so may give temporary relief by bypassing a distracting assertion, however it also means that the CWL is executing outside of its stated design parameters. This will invariably lead to other more difficult to diagnose bugs and problems.

Rather than disable assertions in the CWL, implementors should use them in their own application code as well.

If there is a specific assertion occurring that is believed to be invalid, then please contact the CWL maintainers. There is always the possibility that an assertion is too restrictive or is being incorrectly applied to an unavoidable error.

Contract Primitives

There are four contact primitives that all invoke the same assertion handler. The primitives have subtle differences in their meaning that helps to document the contract.

CW_ASSERT(x)

CW_ASSERT(x) is a C macro where x is a Boolean expression. If the expression is false, then the assertion handler is invoked otherwise code execution continues normally. It is used to test for an invariant condition at some point in the code execution. For example, if a module must be initialized before being used every API call in the module would have the following assertion:

CW_ASSERT(isInitied == true);

If the implementor calls a module API function without first initializing it, then the assertion handler is invoked. It will be clear to the implementor that they failed to initialize the module.

CW_ASSERT(x)is the basis for the three other contract primitives.

CW_ERROR()

CW_ERROR() is a C macro that takes no arguments and always invokes the assertion handler. This is used when reaching a certain execution point in the code should never happen. For example, for some state machines implemented with a switch statement, the default case should never execute because that would indicate the state machine is in an invalid state.

State = INIT; switch (state) { case INIT: state = RX; break; case RX: state = TX; break; case TX: state = RX; break; default: CW_ERROR(); }

CW_REQUIRE(x)

CW_REQUIRE(x) is a C macro where x is Boolean expression. If the expression is false, then the assertion handler is invoked otherwise code execution continues normally. CW_REQUIRE(x) is used to specify function preconditions for passed parameters. For example, this formalizes the requirements that the buffer is pointing to valid memory and the length of the buffer must be smaller than or equal to the maximum payload size of the transmit function. This transmit function does not need to return an error code because it will always work.

void transmit(int8_t *buffer, int32_t len) {      CW_REQUIRE(buffer != NULL);      CW_REQUIRE(len <= MAXIMUM_TX_SIZE);      /* Begin transmit buffer here */ }

CW_ENSURE(x)

CW_ENSURE(x) is a C macro where x is Boolean expression. If the expression is false, then the assertion handler is invoked otherwise code execution continues normally. CW_ENSURE(x) is used to specify function postconditions before returning. For example, the transmit function may ensure that all the buffer data is transmitted by the driver_tx function.

Related pages