如何在C中进行依赖注入?

时间:2022-09-02 09:44:53

I'm looking for a good technical solution to doing DI in C.

我在寻找一个好的技术解决方案来处理C中的DI。

I have seen some of the DI questions here already, but I haven't seen one with any actual examples or concrete implementation suggestions.

我已经在这里看到一些关于DI的问题,但是我还没有看到任何实际的例子或者具体的实现建议。

So, lets say we have the following situation:

假设我们有以下情况:

We have a set of modules in c; we want to refactor those modules so that we can use DI to run unit tests and so on.

我们在c中有一组模块;我们希望重构这些模块,以便使用DI运行单元测试等等。

Each module effectively consists of a set of c functions:

每个模块有效地由一组c函数组成:

module_function(...);

module_function(…);

Modules depend on each other. Ie. Typically you may have a call such as:

模块彼此依赖。Ie。通常,您可能会有这样的电话:

int module1_doit(int x) {
  int y = module2_dosomethingelse(x);
  y += 2;
  return(y);
}

What is the correct approach to DI for this?

这个问题的正确方法是什么?

Possible solutions seem to be:

可能的解决办法似乎是:

  • (1) Using function pointers for all module functions, and when invoking a function do this (or similar):

    (1)对所有模块函数使用函数指针,调用函数时(或类似):

    int y = modules->module2->dosomethingelse(x);

    int y =模块- > module2 - > dosomethingelse(x);

  • (2) Compile multiple libraries (mock, std, etc.) of with the same symbols and dynamically link in the correct implementation.

    (2)使用相同的符号编译多个库(mock、std等),并在正确的实现中动态链接。

(2) seems to be the correct way of doing it, but is difficult to configure and annoyingly forces you to build multiple binaries for each unit test.

(2)似乎是正确的方法,但是配置起来很困难,而且令人讨厌地迫使您为每个单元测试构建多个二进制文件。

(1) Seems like it might work, but at some point your DI controller is going to get stuck in a situation where you need to dynamically invoke a generic factory function (void ( factory) (...) say) with a number of other modules that need to be injected at runtime?

(1)似乎可以工作,但是在某个时候,您的DI控制器将陷入这样的境地:您需要动态地调用一个通用的工厂函数(比如void (factory)(…)),并在运行时注入许多其他模块?

Is there another, better way of doing this in c?

在c语言中还有更好的方法吗?

What's the 'right' way of doing it?

正确的做法是什么?

5 个解决方案

#1


7  

I've concluded that there is no 'right' way of doing this in C. It's always going to be more difficult and tedious than in other languages. I think it's important, however, not to obfuscate your code for the sake of unit tests, though. Making everything a function pointer in C may sound good, but I think it just makes the code horrific to debug in the end.

我已经得出结论,在c语言中没有“正确”的方法来实现这一点,它总是比在其他语言中更加困难和乏味。但是,我认为重要的是,不要为了单元测试而混淆代码。在C语言中把所有东西都变成函数指针听起来不错,但我认为这样做最终会让代码变得非常糟糕。

My latest approach has been to keep things simple. I don't change any code in C modules other than a small #ifdef UNIT_TESTING at the top of a file for externing and memory allocation tracking. I then take the module and compile it with all dependencies removed so that it fails link. Once I've reviewed the unresolved symbols to make sure they are what I want, I run a script that parses these dependencies and generates stub prototypes for all the symbols. These all get dumped in the unit test file. YMMV depending on how complex your external dependencies are.

我的最新方法是保持事情简单。除了在文件顶部的一个小#ifdef UNIT_TESTING外,我不改变任何代码,在文件的顶部进行外部和内存分配跟踪。然后我获取模块并编译它,去掉所有依赖项,这样它就会失败链接。一旦我检查了未解析的符号以确保它们是我想要的,我就运行一个脚本,解析这些依赖项并为所有符号生成存根原型。这些都被转储到单元测试文件中。YMMV取决于外部依赖的复杂程度。

If I need to mock a dependency in one instance, use the real one in another, or stub it in yet another, then I end up with three unit test modules for the one module under test. Having multiple binaries may not be ideal, but it's the only real option with C. They all get run at the same time, though, so it's not really a problem for me.

如果我需要在一个实例中模拟一个依赖项,在另一个实例中使用一个真实的依赖项,或者在另一个实例中存根它,那么我最终会得到一个正在测试的模块的三个单元测试模块。拥有多个二进制文件可能并不理想,但这是c的唯一选择,它们都同时运行,所以对我来说并不是什么问题。

#2


8  

I don't see any problem with using DI in C. See:

我认为在c使用DI没有任何问题。

http://devmethodologies.blogspot.com/2012/07/dependency-injection.html

http://devmethodologies.blogspot.com/2012/07/dependency-injection.html

#3


7  

This is a perfect use-case for Ceedling.

这是Ceedling的一个完美的用例。

Ceedling is sort umbrella project that brings together (among other things) Unity and CMock, which together can automate a lot of the work you're describing.

Ceedling是一种伞状项目,它将Unity和CMock(以及其他东西)结合在一起,它们可以将您所描述的许多工作自动化。

In general Ceedling/Unity/CMock are a set of ruby scripts that scan through your code and auto-generate mocks based on your module header files, as well as test runners that find all the tests and makes runners that will run them.

一般来说,Ceedling/Unity/CMock是一组ruby脚本,它们扫描您的代码,并基于模块头文件自动生成模拟,以及找到所有测试并使运行者运行它们的测试运行器。

A separate test runner binary is generated for each test suite, linking in the appropriate mock and real implementations as you request in your test suite implementation.

为每个测试套件生成一个单独的测试运行程序二进制文件,并在您在测试套件实现中请求时在适当的模拟和实际实现中进行链接。

I was initially hesitant to bring in ruby as a dependency to our build system for testing, and it seemed like a lot of complexity and magic, but after trying it out and writing some tests using the auto-generated mocking code I was hooked.

我最初不太愿意将ruby作为一个依赖于我们的构建系统来进行测试,它看起来非常复杂和神奇,但是在尝试了它并使用自动生成的mock代码编写了一些测试之后,我被吸引住了。

#4


2  

A little late to the party on this but this has been a recent topic where I work.

在这个问题上参加聚会有点晚,但这是我最近工作的一个话题。

The two main ways that I've seen it done is using function pointers, or moving all dependencies to a specific C file.

我看到的两种主要方法是使用函数指针,或者将所有依赖项移动到特定的C文件。

A good example of the later is FATFS. http://elm-chan.org/fsw/ff/en/appnote.html

后面的一个很好的例子就是FATFS。http://elm-chan.org/fsw/ff/en/appnote.html

The author of fatfs provides the bulk of the library functions and relegates certain specific dependencies for the user of the library to write (e.g. serial peripheral interface functions).

fatfs的作者提供了大部分库函数,并将某些特定的依赖项归为库的用户编写(例如,串行外围接口函数)。

Function pointers are another useful tool, and using typedefs help to keep the code from getting too ugly.

函数指针是另一个有用的工具,使用typedef有助于防止代码变得太丑。

Here's some simplified snippets from my Analog to Digital Converter (ADC) code:

下面是我模拟到数字转换器(ADC)代码的一些简化片段:

typedef void (*adc_callback_t)(void);

bool ADC_CallBackSet(adc_callback_t callBack)
{
    bool err = false;
    if (NULL == ADC_callBack)
    {
        ADC_callBack = callBack;
    }
    else
    {
        err = true;
    }
    return err;
}

// When the ADC data is ready, this interrupt gets called
bool ADC_ISR(void)
{
    // Clear the ADC interrupt flag
    ADIF = 0;

    // Call the callback function if set
    if (NULL != ADC_callBack)
    {
        ADC_callBack();
    }

    return true; // handled
}

// Elsewhere
void FOO_Initialize(void)
{
    ADC_CallBackSet(FOO_AdcCallback);
    // Initialize other FOO stuff
}

void FOO_AdcCallback(void)
{
    ADC_RESULT_T rawSample = ADC_ResultGet();
    FOO_globalVar += rawSample;
}

Foo's interrupt behavior is now injected into the ADC's interrupt service routine.

Foo的中断行为现在被注入到ADC的中断服务例程中。

You can take it a step further and pass a function pointer into FOO_Initialize so all dependency issues are managed by the application.

您可以进一步向FOO_Initialize传递函数指针,以便应用程序管理所有依赖项问题。

//dependency_injection.h
typedef void (*DI_Callback)(void)
typedef bool (*DI_CallbackSetter)(DI_Callback)

// foo.c
bool FOO_Initialize(DI_CallbackSetter CallbackSet)
{
    bool err = CallbackSet(FOO_AdcCallback);
    // Initialize other FOO stuff
    return err;
}

#5


1  

There are two approaches that you can use. Whether you really want to or not, as Rafe is pointing out, are up to you.

有两种方法可以使用。正如雷夫所指出的,你是否真的想要,取决于你自己。

First: Create the "dynamically" injected method in a static library. Link against the library and simply substitute it during tests. Voila, the method is replaced.

首先:在静态库中创建“动态”注入方法。链接到库并在测试期间简单地替换它。瞧,这个方法被取代了。

Second: Simply provide compile-time replacements based on preprocessing:

第二:简单地提供基于预处理的编译时替换:

#ifndef YOUR_FLAG

    /* normal method versions */

#else

    /* changed method versions */

#endif

/* methods that have no substitute */

#1


7  

I've concluded that there is no 'right' way of doing this in C. It's always going to be more difficult and tedious than in other languages. I think it's important, however, not to obfuscate your code for the sake of unit tests, though. Making everything a function pointer in C may sound good, but I think it just makes the code horrific to debug in the end.

我已经得出结论,在c语言中没有“正确”的方法来实现这一点,它总是比在其他语言中更加困难和乏味。但是,我认为重要的是,不要为了单元测试而混淆代码。在C语言中把所有东西都变成函数指针听起来不错,但我认为这样做最终会让代码变得非常糟糕。

My latest approach has been to keep things simple. I don't change any code in C modules other than a small #ifdef UNIT_TESTING at the top of a file for externing and memory allocation tracking. I then take the module and compile it with all dependencies removed so that it fails link. Once I've reviewed the unresolved symbols to make sure they are what I want, I run a script that parses these dependencies and generates stub prototypes for all the symbols. These all get dumped in the unit test file. YMMV depending on how complex your external dependencies are.

我的最新方法是保持事情简单。除了在文件顶部的一个小#ifdef UNIT_TESTING外,我不改变任何代码,在文件的顶部进行外部和内存分配跟踪。然后我获取模块并编译它,去掉所有依赖项,这样它就会失败链接。一旦我检查了未解析的符号以确保它们是我想要的,我就运行一个脚本,解析这些依赖项并为所有符号生成存根原型。这些都被转储到单元测试文件中。YMMV取决于外部依赖的复杂程度。

If I need to mock a dependency in one instance, use the real one in another, or stub it in yet another, then I end up with three unit test modules for the one module under test. Having multiple binaries may not be ideal, but it's the only real option with C. They all get run at the same time, though, so it's not really a problem for me.

如果我需要在一个实例中模拟一个依赖项,在另一个实例中使用一个真实的依赖项,或者在另一个实例中存根它,那么我最终会得到一个正在测试的模块的三个单元测试模块。拥有多个二进制文件可能并不理想,但这是c的唯一选择,它们都同时运行,所以对我来说并不是什么问题。

#2


8  

I don't see any problem with using DI in C. See:

我认为在c使用DI没有任何问题。

http://devmethodologies.blogspot.com/2012/07/dependency-injection.html

http://devmethodologies.blogspot.com/2012/07/dependency-injection.html

#3


7  

This is a perfect use-case for Ceedling.

这是Ceedling的一个完美的用例。

Ceedling is sort umbrella project that brings together (among other things) Unity and CMock, which together can automate a lot of the work you're describing.

Ceedling是一种伞状项目,它将Unity和CMock(以及其他东西)结合在一起,它们可以将您所描述的许多工作自动化。

In general Ceedling/Unity/CMock are a set of ruby scripts that scan through your code and auto-generate mocks based on your module header files, as well as test runners that find all the tests and makes runners that will run them.

一般来说,Ceedling/Unity/CMock是一组ruby脚本,它们扫描您的代码,并基于模块头文件自动生成模拟,以及找到所有测试并使运行者运行它们的测试运行器。

A separate test runner binary is generated for each test suite, linking in the appropriate mock and real implementations as you request in your test suite implementation.

为每个测试套件生成一个单独的测试运行程序二进制文件,并在您在测试套件实现中请求时在适当的模拟和实际实现中进行链接。

I was initially hesitant to bring in ruby as a dependency to our build system for testing, and it seemed like a lot of complexity and magic, but after trying it out and writing some tests using the auto-generated mocking code I was hooked.

我最初不太愿意将ruby作为一个依赖于我们的构建系统来进行测试,它看起来非常复杂和神奇,但是在尝试了它并使用自动生成的mock代码编写了一些测试之后,我被吸引住了。

#4


2  

A little late to the party on this but this has been a recent topic where I work.

在这个问题上参加聚会有点晚,但这是我最近工作的一个话题。

The two main ways that I've seen it done is using function pointers, or moving all dependencies to a specific C file.

我看到的两种主要方法是使用函数指针,或者将所有依赖项移动到特定的C文件。

A good example of the later is FATFS. http://elm-chan.org/fsw/ff/en/appnote.html

后面的一个很好的例子就是FATFS。http://elm-chan.org/fsw/ff/en/appnote.html

The author of fatfs provides the bulk of the library functions and relegates certain specific dependencies for the user of the library to write (e.g. serial peripheral interface functions).

fatfs的作者提供了大部分库函数,并将某些特定的依赖项归为库的用户编写(例如,串行外围接口函数)。

Function pointers are another useful tool, and using typedefs help to keep the code from getting too ugly.

函数指针是另一个有用的工具,使用typedef有助于防止代码变得太丑。

Here's some simplified snippets from my Analog to Digital Converter (ADC) code:

下面是我模拟到数字转换器(ADC)代码的一些简化片段:

typedef void (*adc_callback_t)(void);

bool ADC_CallBackSet(adc_callback_t callBack)
{
    bool err = false;
    if (NULL == ADC_callBack)
    {
        ADC_callBack = callBack;
    }
    else
    {
        err = true;
    }
    return err;
}

// When the ADC data is ready, this interrupt gets called
bool ADC_ISR(void)
{
    // Clear the ADC interrupt flag
    ADIF = 0;

    // Call the callback function if set
    if (NULL != ADC_callBack)
    {
        ADC_callBack();
    }

    return true; // handled
}

// Elsewhere
void FOO_Initialize(void)
{
    ADC_CallBackSet(FOO_AdcCallback);
    // Initialize other FOO stuff
}

void FOO_AdcCallback(void)
{
    ADC_RESULT_T rawSample = ADC_ResultGet();
    FOO_globalVar += rawSample;
}

Foo's interrupt behavior is now injected into the ADC's interrupt service routine.

Foo的中断行为现在被注入到ADC的中断服务例程中。

You can take it a step further and pass a function pointer into FOO_Initialize so all dependency issues are managed by the application.

您可以进一步向FOO_Initialize传递函数指针,以便应用程序管理所有依赖项问题。

//dependency_injection.h
typedef void (*DI_Callback)(void)
typedef bool (*DI_CallbackSetter)(DI_Callback)

// foo.c
bool FOO_Initialize(DI_CallbackSetter CallbackSet)
{
    bool err = CallbackSet(FOO_AdcCallback);
    // Initialize other FOO stuff
    return err;
}

#5


1  

There are two approaches that you can use. Whether you really want to or not, as Rafe is pointing out, are up to you.

有两种方法可以使用。正如雷夫所指出的,你是否真的想要,取决于你自己。

First: Create the "dynamically" injected method in a static library. Link against the library and simply substitute it during tests. Voila, the method is replaced.

首先:在静态库中创建“动态”注入方法。链接到库并在测试期间简单地替换它。瞧,这个方法被取代了。

Second: Simply provide compile-time replacements based on preprocessing:

第二:简单地提供基于预处理的编译时替换:

#ifndef YOUR_FLAG

    /* normal method versions */

#else

    /* changed method versions */

#endif

/* methods that have no substitute */