在嵌入式编程中避免全局变量

发布于 2024-08-02 09:00:36 字数 1318 浏览 3 评论 0原文

在我正在研究的嵌入式编程类型中,运行代码的确定性和透明度受到高度重视。例如,我所说的透明度是指能够查看内存的任意部分并知道其中存储了什么变量。因此,正如我确信嵌入式程序员所期望的那样,如果可能的话,应该避免 new ,如果无法避免,则仅限于初始化。

我理解这样做的必要性,但不同意我的同事这样做的方式,我也不知道更好的选择。

我们拥有的是几个全局结构数组和一些全局类。有一组用于互斥体的结构体,一组用于信号量,一组用于消息队列(这些结构体在 main 中初始化)。对于每个运行的线程,拥有它的类是一个全局变量。

我遇到的最大问题是单元测试。当我想要测试 #include 的全局变量但我不想测试的类时,如何插入模拟对象?

伪代码的情况如下:

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(  );
    }
}

如果我想要更多或更少的任务怎么办? foo1 的构造函数中的一组不同的参数?我想我必须有一个单独的 bar.h 和 bar.cpp,这似乎比必要的工作多得多。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

ゃ懵逼小萝莉 2024-08-09 09:00:36

如果您想首先对此类代码进行单元测试,我建议您阅读有效地使用遗留代码 另请参阅

基本上使用链接器插入模拟/假对象和函数应该是最后的手段,但仍然完全有效。

但是,您也可以使用控制反转,如果没有框架,这可能会将一些责任推给客户端代码。但这确实有助于测试。例如,要测试 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);
}

现在,您可以将线程的模拟版本传递给“FSW_taskStart”,以确保该函数确实根据需要启动线程。不幸的是,您必须依赖原始 FSW_taskStart 传递正确参数的事实,但您现在正在测试更多代码。

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.

心安伴我暖 2024-08-09 09:00:36

依赖注入对您的情况有帮助吗?这可以摆脱所有全局变量,并允许轻松替换单元测试中的依赖项。

每个线程主函数都会传递一个包含依赖项(驱动程序、邮箱等)的映射,并将它们存储在将使用它们的类中(而不是访问某些全局变量)。

对于每个环境(目标、模拟器、单元测试...),您创建一个“配置”函数,该函数创建所有需要的对象、驱动程序和所有线程,并为线程提供依赖项列表。例如,目标配置可以创建 USB 驱动程序并将其注入到某些通信线程中,而通信单元测试配置可以创建测试控制的存根 USB 驱动程序。

如果您绝对需要重要变量的这种“透明度”,请为它们创建类,这会将它们保存在已知地址,并在需要的地方注入这些类。

它比静态对象列表要多做很多工作,但灵活性非常好,特别是当您遇到一些棘手的集成问题并想要交换组件进行测试时。

大致:

// 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
}

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
}
为人所爱 2024-08-09 09:00:36

您可以使用 malloc 分配内存,然后使用 new 运算符将对象放置在该位置,

void* mem = malloc(3*sizeof(SomeClass));
SomeClass *a = new(mem) SomeClass();
mem += sizeof(SomeClass);
SomeClass *b = new(mem) SomeClass();
mem += sizeof(SomeClass);
SomeClass *c = new(mem) SomeClass();

这样您就可以 malloc 所有内存,然后根据需要分配它。注意:请确保手动调用解构,因为调用删除时不会这样做

You can allocate memory using malloc and then get the new operator to make the object at that position

void* mem = malloc(3*sizeof(SomeClass));
SomeClass *a = new(mem) SomeClass();
mem += sizeof(SomeClass);
SomeClass *b = new(mem) SomeClass();
mem += sizeof(SomeClass);
SomeClass *c = new(mem) SomeClass();

so you can malloc all the memory then allocate it as you wish. Note: make sure you call the deconstruction manually as it wont when you call delete

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文