4 ms·
I feel that some of these issues occur due to an over-reliance on object-oriented practices, in particular 'encapsulation' of state into objects and triggering
by jononor 10y ago
I feel that some of these issues occur due to an over-reliance on object-oriented practices, in particular 'encapsulation' of state into objects and triggering side-effects in callbacks. The usage of Rust brings painpoints to the fore, but I'll argue that the code is better done otherwise, independently of borrow checker.
For instance in the 'Callback through closures' example, there is code like:
setTimeout(|| {
activityLed.toggle();
}, 2000);
First, in an embedded system, you cannot have multiple sources of truth. So its not just because of Rust ownership reasoning that `activityLed` cannot be shared between callbacks - if multiple events is to be considered for a given output they should be syncronized explicitly.
Instead I think the code should be more like the following, which models calculation of new state as a pure function. Side-effects are separated out by storing the desired effect as data, dedicated function for realizing. Time is just data, time-based logic without depending on hidden state.
This increases testability significantly. And it can be fully reasoned about by Rust, no conflicts with the borrow checker.
# for each tick
# calculate new state
if (now >= state.toggleLedTime) {
state.activityLed = !state.activityLed;
state.toggleLedTime = now + 2000;
}
# and realize it
updatePin(config.ledPin, state.activityLed);
- hexmiles 10y agothe updatePin call, shouldn't be inside the if body?, or i'm missing something?
- deleted 10y ago[deleted]
- jononor 10y agoBecause it sets what is stored in `state` it does not have to be. It could be inside the if, but there two major reasons why it should not: 1) Testability. With it separated, we can write automated tests for our state calculation logic that are hardware independent, like 'normal' code. We just need to create `currentState` data, pass it into our `calculate()` function, and verify the new `state` returned. This can run both on our host-system, and on-target. We can even build simulations if we want, http://www.jonnor.com/2017/03/host-based-simulation-for-embedded-systems/ http://www.jonnor.com/2017/03/host-based-simulation-for-embe... (shameless plug). 2) Deterministic execution time. Many embedded systems have soft or hard real-time constraints, action must always be performed within X microseconds. The more branches we have the harder it is to reason about whether we can keep this guarantee. By eliminating branches we are executing our 'worst case' always, giving it much more testing.
- jononor 10y agoThis assumes that the call to updatePin is idempotent (calling multiple times with same inputs does not do anything). An update of a I/O pin usually is, but other hardware (or APIs...) might not be. In this case the realization of the state should still be in a separate function for testability. But it can take both `newState, currentState` as arguments, and then do: if (newState.thingon != currentState.thingon) { nonIdempotentUpdate(newState.thingon); } If the device is latency critical, then the functions should be tested in all its permutations. You can use generative techniques like fuzzing to generate the `state` inputs.
- greenleafjacob 10y agocomponentDidUpdate for embedded systems?
- IshKebab 10y agoThat sounds like an incredibly awkward way to program. You'll end up with a huge function with all your callback bodies in it, and you won't be able to easily sleep the CPU.
- jononor 10y agoSplitting of work to dedicated functions is done as usual. General functions, which don't impact state, should be free-standing. Non-trivial independent functionality should be in separate functions, passing down the relevant state (subsets of the overall state). As long as there are no side-effects, can do as much higher-order functions as desired. Sleeping is not affected? Can trigger that whenever you want. Only difference is that there is no 'implicit' sleeping from registering a callback. But if using an exisiting RTOS, there might be serious impedance mismatch causing this approach to not worth it.
- dllthomas 10y agoI think the concern around sleeping is that you don't know how long you can safely sleep for without knowing to inspect state.toggleLedTime (and every similar value - potentially error prone).