Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding global variables in embedded programming

In the type of embedded programming I'm getting into, determinism and transparency of the running code are highly valued. What I mean by transparency is, for instance, being able to look at arbitrary sections of memory and know what variable is stored there. So, as I'm sure embedded programmers expect, new is to be avoided if at all possible, and if it can't be avoided, then limited to initialization.

I understand the need for this, but don't agree with the way my coworkers have gone about doing this, nor do I know a better alternative.

What we have are several global arrays of structures, and some global classes. There is one array of structs for mutexes, one for semaphores, and one for message queues (these are initialized in main). For each thread that runs, the class that owns it is a global variable.

The biggest problem I have with this is in unit testing. How can I insert a mock object when the class I want to test #includes global variables that I don't?

Here's the situation in pseudo-code:

foo.h

#include "Task.h"
class Foo : Task {
public:
  Foo(int n);
  ~Foo();
  doStuff();
private:
  // copy and assignment operators here
}

bar.h

#include <pthread.h>
#include "Task.h"

enum threadIndex { THREAD1 THREAD2 NUM_THREADS };
struct tThreadConfig {
  char      *name,
  Task      *taskptr,
  pthread_t  threadId,
  ...
};
void startTasks();

bar.cpp

#include "Foo.h"

Foo foo1(42);
Foo foo2(1337);
Task task(7331);

tThreadConfig threadConfig[NUM_THREADS] = {
  { "Foo 1", &foo1, 0, ... },
  { "Foo 2", &foo2, 0, ... },
  { "Task",  &task, 0, ... }
};

void FSW_taskStart() {
    for (int i = 0; i < NUMBER_OF_TASKS; i++) {
        threadConfig[i].taskptr->createThread(  );
    }
}

What if I want more or less tasks? A different set of arguments in the constructor of foo1? I think I would have to have a separate bar.h and bar.cpp, which seems like a lot more work than necessary.

like image 559
Nate Parsons Avatar asked Aug 13 '09 20:08

Nate Parsons


People also ask

How do you avoid global variables in embedded C?

1) Use a static local variable within a function. It will retain its value between calls to that function but be 'invisible' to the rest of the program. 2) Use a static variable at file scope (ie declared outwith all functions in the source file).

How do you avoid global variables?

The simplest way to avoid globals all together is to simply pass your variables using function arguments. As you can see, the $productData array from the controller (via HTTP request) goes through different layer: The controller receives the HTTP request. The parameters are passed to the model.

Are global variables bad in embedded C?

Non-const global variables are evil because their value can be changed by any function. Using global variables reduces the modularity and flexibility of the program. It is suggested not to use global variables in the program.

Why should global variables be avoided?

Global variables can be altered by any part of the code, making it difficult to remember or reason about every possible use. A global variable can have no access control. It can not be limited to some parts of the program. Using global variables causes very tight coupling of code.


2 Answers

If you want to unit test such code first I would recommend reading Working Effectively With Legacy Code Also see this.

Basically using the linker to insert mock/fake objects and functions should be a last resort but is still perfectly valid.

However you can also use inversion of control, without a framework this can push some responsibility to the client code. But it really helps testing. For instance to test FSW_taskStart()

tThreadConfig threadConfig[NUM_THREADS] = {
  { "Foo 1", %foo1, 0, ... },
  { "Foo 2", %foo2, 0, ... },
  { "Task",  %task, 0, ... }
};

void FSW_taskStart(tThreadConfig configs[], size_t len) {
    for (int i = 0; i < len; i++) {
        configs[i].taskptr->createThread(  );
    }
}

void FSW_taskStart() {
    FSW_taskStart(tThreadConfig, NUM_THREADS);
}

void testFSW_taskStart() {
    MockTask foo1, foo2, foo3;
    tThreadConfig mocks[3] = {
          { "Foo 1", &foo1, 0, ... },
          { "Foo 2", &foo2, 0, ... },
          { "Task",  &foo3, 0, ... }
        };
    FSW_taskStart(mocks, 3);
    assert(foo1.started);
    assert(foo2.started);
    assert(foo3.started);
}

Now you can can can pass mock version of you're threads to 'FSW_taskStart' to ensure that the function does in fact start the threads as required. Unfortunatly you have to rely on the fact that original FSW_taskStart passes the correct arguments but you are now testing a lot more of your code.

like image 199
iain Avatar answered Nov 06 '22 12:11

iain


Would dependency injection help in your situation? This could get rid of all global variables and allow for easy substitution of dependencies in your unit tests.

Each thread main function is passed a map containing dependencies (drivers, mailboxes, etc.) and stores them in the classes that will use them (instead of accessing some global variable).

For each environment (target, simulator, unit test...) you create one "configuration" function that creates all needed objects, drivers and all threads, giving threads their list of dependencies. For example, the target configuration could create a USB driver and inject it into some comms thread, while the comms unit test configuration could create a stub USB driver that the tests controls.

If you absolutely need this "transparency" for important variable, create classes for them, which will hold them at a known address, and inject these classes where needed.

It's quite a bit more work than static lists of objects, but the flexibility is fantastic, especially when you hit some tricky integration issues and want to swap components for testing.

Roughly:

// Config specific to one target.
void configure_for_target_blah(System_config& cfg)
{   // create drivers
    cfg.drivers.push_back("USB", new USB_driver(...))
    // create threads
    Thread_cfg t;
    t.main = comms_main; // main function for that thread
    t.drivers += "USB"; // List of driver names to pass as dependencies
    cfg.threads += t;
}

// Main function for the comms thread.
void comms_main(Thread_config& cfg)
{
    USB_driver* usb = cfg.get_driver("USB");
    // check for null, then store it and use it...
}

// Same main for all configs.
int main()
{
    System_config& cfg;
    configure_for_target_blah(cfg);
    //for each cfg.drivers
    //    initialise driver
    //for each cfg.threads
    //    create_thread with the given main, and pass a Thread_config with dependencies
}
like image 3
squelart Avatar answered Nov 06 '22 11:11

squelart