【问题标题】:How to prevent a function from being called from certain sections of code?如何防止从某些代码段调用函数?
【发布时间】:2018-02-05 10:17:11
【问题描述】:

我正在实现一个辅助类,它具有许多有用的功能,这些功能将在大量类中使用。但是,其中一些并非设计为在某些代码段内调用(从中断函数,这是一个嵌入式项目)。

然而,对于这个类的用户来说,为什么某些函数被允许而其他函数被禁止从中断函数中调用的原因可能不是很明显,并且在许多情况下,被禁止的函数可能会起作用,但可能会导致非常微妙和难以稍后再查找错误。

对我来说最好的解决方案是,如果从不应调用的代码部分调用有问题的函数,则会导致编译器错误。

我也考虑过一些非技术性的解决方案,但最好是技术性的解决方案。

  1. 在文档中用警告指出它。可能很容易被忽略,尤其是当函数看起来很明显时,比如read_byte(),为什么有人会研究文档,不管函数是否可重入?

  2. 在函数名中注明。丑陋。谁喜欢read_byte_DO_NOT_CALL_FROM_INTERRUPT() 这样的函数名?

  3. 在公共头文件中有一个全局变量,包含在每个文件中,在每个中断开始时将其设置为 true,在结束时设置为 false,并且有问题的函数会在开始时对其进行检查,如果已设置则退出。问题:中断可能会相互中断。此外,它不会导致编译时警告或错误。

  4. 类似于#3,有一个带堆栈的全局处理程序,以便可以处理嵌套中断。仍然存在仅在运行时工作的问题,并且还增加了很多开销。中断不应为此功能浪费超过一两个时钟周期(如果有的话)。

  5. 滥用预处理器。不幸的是,在每个中断的开头使用#define,在每个中断结束时使用#undef,在有问题的函数开头使用#ifdef这种幼稚的方式不起作用,因为预处理器并不关心范围。

  6. 由于中断总是无类函数,我可以将有问题的函数设为protected,并在所有使用它们的类中将它们声明为friends。这样,就不可能在中断中直接使用它们。由于main() 是无类的,因此我必须将其大部分放入类方法中。我不太喜欢这个,因为它可能变得不必要的复杂,并且它产生的错误并不明显(所以这个函数的用户可能会封装它们来“解决”问题,而没有意识到 真正的 em> 问题是)。像 "ERROR: function_name() is not to be used from within an interrupt" 这样的编译器或链接器错误消息会更可取。

  7. 检查函数内的中断寄存器有几个问题。在大型微控制器中,有很多寄存器需要检查。此外,当一个中断标志恰好在一个时钟周期之前设置时,误报的可能性很小但很危险,所以我的函数会失败,因为它认为它是从中断调用的,而中断会在下一个周期。此外,在嵌套中断中,中断标志被清除,导致误报。最后,这是另一个运行时解决方案。

前段时间我确实玩过一些非常基本的模板元编程,但我没有找到一个非常简单和优雅的解决方案的经验。在承诺自己尝试实现模板元编程膨胀软件之前,我宁愿尝试其他方式。

仅使用 C 中可用功能的解决方案也是可以接受的,甚至更可取。

【问题讨论】:

  • 听起来你的中断函数做的太多了……
  • 不要将函数声明放在定义中断并将警告视为错误的翻译单元中
  • “将在大量类中使用”-让我认为您应该将函数放在基类中,并从该基类继承以引入功能。如果这不起作用,那我错过了什么?
  • 中断函数与“正常”函数的区别是什么?您是否处于可以使用某种代码模式装饰所有中断函数的位置?
  • 投反对票的人能否说明他们的理由?在如此短的时间内如此大量的反对票通常只在完全无法理解或公然离题的问题中观察到。你认为这是一个XY问题的案例吗?转载请注明。你认为我写的次优解决方案列表很荒谬吗?我可以很乐意删除它们。你认为这个问题应该在另一个网站上问吗?请以接近的投票方式表明它。你认为这不是问题,还是我完全错过了一个非常明显的解决方案?那么请这样回答。

标签: c++ c c-preprocessor


【解决方案1】:

下面有一些cmets。作为警告,他们不会很有趣地阅读,但我不会通过不指出这里的问题来为您服务。

  • 如果您是从 ISR 内部调用外部函数,那么再多的文档或编码也无济于事。因为在大多数情况下,这样做是不好的做法。程序员必须知道他们在做什么,否则再多的文档或编码机制都无法挽救程序。

    程序员不会专门为从 ISR 内部调用而设计库函数。相反,程序员在设计 ISR 时要考虑到 ISR 的所有特殊限制:确保正确清除中断标志、保持代码简短、不要调用外部函数、不要阻塞 MCU 超过必要的时间、考虑重新-entrancy,考虑危险的编译器优化(使用 volatile)。不知道这一点的人不足以编写 ISR。

  • 如果您实际上有一个函数int read_byte(int address),那么这表明程序设计一开始就不好。这个函数可以做以下两件事之一:

    • 它要么可以读取一些外围硬件的一个字节,在这种情况下,函数名称很糟糕,应该更改。
    • 或者它可以从地址读取任何通用字节,在这种情况下,该函数是 100% 无用的“膨胀软件”。您可以放心地假设一个有一定能力的 C 程序员可以从内存地址读取一个字节,而无需一些英国媒体报道软件牵着他们的手。

    在任何一种情况下,int 都不是一个字节。它是一个 16 位或 32 位的字。该函数应该返回uint8_t。同样,如果传递的参数用于描述 MCU 的内存映射地址,则其类型应为 void*uint8_t*uintptr_t。其他都错了。

    值得注意的是,如果您使用int 而不是stdint.h 进行嵌入式系统编程,那么整个讨论对您来说是最少的问题,因为您甚至还没有掌握基本的基础知识。您的程序将充满未定义的行为和隐含的提升错误。

总体而言,您建议的所有解决方案都是不可接受的。这里问题的根源似乎是程序设计。处理这个问题,而不是用可怕的元编程发明方法来保护损坏的设计。

【讨论】:

  • 我实际上使用 uint8_t 作为我的字节,该函数应该是从外部 I2C 存储器读取的东西的通用示例。我很抱歉它造成的误解。我建议的解决方案实际上并不是解决方案,我也没有建议它们。我实际上解释了为什么它们不是好主意。为什么我有时会在中断中使用函数?因为在我可以容忍调用开销的情况下,我宁愿调用一个将定时器/计数器或串行通信端口设置为特定状态的函数,而不是编写 10 多行寄存器分配。
  • @vsz 在这种情况下,您将使用 local 函数 inline:d 而不是外部函数。
  • 确实如此,但这并不妨碍有人找到其中一个函数并从他们不应该的地方调用它。也许他们调查了它的来源,并说服自己它是安全的,但问题可能非常微妙,只有在经过大量测试或思考后才能发现(并且无论函数是否内联都无关)。这就是我想要阻止的。当然,好的源 cmets 是一种解决方案,如果有人在考虑在中断中使用这样的函数时没有阅读它们,他们是罪魁祸首,但为了方便起见,我想放置一个额外的安全网。
  • @vsz 这实际上与从 ISR 内部调用标准库函数时的情况相同。你不能假设它是如何实现的,所以总是期待最坏的情况。
  • “使用volatile”。好吧,在这种情况下,更好的建议是“阅读手册,了解您的 ISR 要求,并验证 volatile 是否足够和必要”。问题的根源在于 ISR 发生在程序集级别,而不是 C 或 C++ 级别。从高级语言状态来看,处理器状态并不是很明显。
【解决方案2】:

我建议选项 8 和 9。

同行评审和断言。

您在 cmets 中声明您的中断函数很短。如果确实如此,那么审查它们将是微不足道的。在标题中添加 cmets 将使任何人都可以看到发生了什么。在添加断言时,虽然您使调试构建将在错误中返回错误结果可行,但它也将确保您将捕获任何调用;并在测试期间为您提供解决问题的机会。

最终,宏处理将不起作用,因为您可以做的最好的事情是捕获是否包含标头,但是如果调用堆栈通过另一个包装器(没有 cmets),那么您就不能抓住那个。

或者你可以让你的助手成为一个模板,但这意味着你的助手的每个包装器也必须是一个模板,这样才能知道你是否处于中断例程中......这最终将是你的全部代码库。

【讨论】:

    【解决方案3】:

    如果您有一个文件用于所有中断例程,那么这可能会有所帮助: 在类头中定义一个宏,比如 FORBID_INTERRUPT_ROUTINE_ACCESS。 并在中断处理程序文件中检查该宏定义:

    #ifdef FORBID_INTERRUPT_ROUTINE_ACCESS
    #error : cannot access function from interrupt handlers.
    #endif
    

    如果有人为该类添加头文件以在中断处理程序中使用该类,那么它将引发错误。

    注意:您必须通过指定警告将被视为错误来构建目标。

    【讨论】:

    • 这不取决于包含标题的顺序吗?这似乎不太理想......
    • @SeanBurton:它如何取决于包含的顺序?想法是将上述#ifdef 放在 ISR 的源文件中,而不是放在头文件中。唯一重要的顺序是 #ifdef 在所有标题包含之后。它可以放在源文件的末尾,并附上一条注释,说明将其保留在那里并解释原因。
    【解决方案4】:

    这里是 C++ 模板函数建议。 我不认为这是元编程或膨胀软件。

    首先创建 2 个类,它们将定义用户将使用函数的上下文:

    class In_Interrupt_Handler;
    class In_Non_Interrupt_Handler;
    

    如果您将在 2 个上下文之间有一些共同的实现,可以添加一个基类:

    class Handy_Base
    {
     protected:
        static int Handy_protected() { return 0; }
     public:
        static int Handy_public() { return 0; }
    };
    

    主要模板定义,没有任何实现。实现将由专业化类提供:

    template< class Is_Interrupt_Handler >
    class Handy_functions;
    

    还有专业。

    // Functions can be used when inside an interrupt handler
    template<>
    struct Handy_functions< In_Interrupt_Handler >
        : Handy_Base
    {
        static int Handy1() { return 1; }
        static int Handy2() { return 2; }
    };
    
    // Functions can be used when inside any function
    template<>
    struct Handy_functions< In_Non_Interrupt_Handler > 
        : Handy_Base
    {
        static int Handy1() { return 4; }
        static int Handy2() { return 8; }
    };
    

    这样,如果 API 的用户想要访问函数,唯一的方法就是指定需要什么类型的函数。

    使用示例:

    int main()
    {
        using IH_funcs = Handy_functions<In_Interrupt_Handler>;
        std::cout << IH_funcs::Handy1() << '\n';
        std::cout << IH_funcs::Handy2() << '\n';
    
        using Non_IH_funcs = Handy_functions<In_Non_Interrupt_Handler>;
        std::cout << Non_IH_funcs::Handy1() << '\n';
        std::cout << Non_IH_funcs::Handy2() << '\n';
    
    }
    

    最后,我认为问题归结为使用您的框架的开发人员。以及您的框架需要多少开发人员来样板化。

    以上内容不会阻止开发人员从中断处理程序内部调用非中断处理程序函数。

    我认为这种类型的分析需要某种类型的静态分析检查系统。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多