C ++中的函数挂钩?

Posted

技术标签:

【中文标题】C ++中的函数挂钩?【英文标题】:Function hooking in C++? 【发布时间】:2011-10-12 17:03:04 【问题描述】:

“挂钩”是指非侵入式地覆盖函数行为的能力。一些例子:

在函数体之前和/或之后打印一条日志消息。 将函数体包装在 try catch 体中。 测量函数的持续时间 等等……

我在各种编程语言和库中看到了不同的实现:

面向方面的编程 javascript 的第一类函数 OOP 装饰器模式 WinAPI 子类化 鲁比的method_missing SWIG 的 %exception 关键字旨在将所有函数包装在 try/catch 块中,可以(ab)用于挂钩目的

我的问题是:

IMO 这是一个非常有用的功能,我想知道为什么它从未被实现为 C++ 语言功能。是否有任何原因阻止这成为可能? 有哪些推荐的技术或库可以在 C++ 程序中实现这一点?

【问题讨论】:

Raymond Chen 最近wrote 讲述了 Windows 是如何做到这一点的 关于有哪些推荐的技术或库可以在 C++ 程序中实现这一点? 这是一个有效的问题。请不要投票关闭它。 @6502:你显然很脱节。模板元编程在 2003 年是如此。lambda 表达式现在是世界上最伟大的事情。接受吧,伙计! 我知道,迟早会有一些,呃,不明智的灵魂,在 C++ 标签中获得合法的 34 票赞成,并且不假思索地出现 尽管这是一个完全合理的问题,尤其是在它被改变以适应批评者之后,但还是把失踪的最后一个关闭了! 叹息。 投票重新开放。 【参考方案1】:

如果你说的是在函数体之前/之后调用一个新方法,而不改变函数体,你可以基于this,它使用自定义shared_ptr删除器来触发之后-身体机能。它不能用于try/catch,因为使用这种技术,前后需要是单独的函数。

另外,下面的版本使用shared_ptr,但是在C++11中你应该可以使用unique_ptr来获得同样的效果,而无需每次使用共享指针时创建和销毁共享指针。

#include <iostream>
#include <boost/chrono/chrono.hpp>
#include <boost/chrono/system_clocks.hpp>
#include <boost/shared_ptr.hpp>

template <typename T, typename Derived>
class base_wrapper

protected:
  typedef T wrapped_type;

  Derived* self() 
    return static_cast<Derived*>(this);
  

  wrapped_type* p;

  struct suffix_wrapper
  
    Derived* d;
    suffix_wrapper(Derived* d): d(d) ;
    void operator()(wrapped_type* p)
    
      d->suffix(p);
    
  ;
public:
  explicit base_wrapper(wrapped_type* p) :  p(p) ;


  void prefix(wrapped_type* p) 
     // Default does nothing
  ;

  void suffix(wrapped_type* p) 
     // Default does nothing
  

  boost::shared_ptr<wrapped_type> operator->() 
  
    self()->prefix(p);
    return boost::shared_ptr<wrapped_type>(p,suffix_wrapper(self()));
  
;




template<typename T>
class timing_wrapper : public base_wrapper< T, timing_wrapper<T> >

  typedef  base_wrapper< T, timing_wrapper<T> > base;
  typedef boost::chrono::time_point<boost::chrono::system_clock, boost::chrono::duration<double> > time_point;

  time_point begin;
public:
  timing_wrapper(T* p): base(p) 


  void prefix(T* p) 
  
    begin = boost::chrono::system_clock::now();
  

  void suffix(T* p)
  
    time_point end = boost::chrono::system_clock::now();

    std::cout << "Time: " << (end-begin).count() << std::endl;
  
;

template <typename T>
class logging_wrapper : public base_wrapper< T, logging_wrapper<T> >

  typedef  base_wrapper< T, logging_wrapper<T> > base;
public:
  logging_wrapper(T* p): base(p) 

  void prefix(T* p) 
  
    std::cout << "entering" << std::endl;
  

  void suffix(T* p) 
  
    std::cout << "exiting" << std::endl;
  

;


template <template <typename> class wrapper, typename T> 
wrapper<T> make_wrapper(T* p) 

  return wrapper<T>(p);



class X 

public:
  void f()  const
  
    sleep(1);
  

  void g() const
  
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  

;



int main () 

  X x1;


  make_wrapper<timing_wrapper>(&x1)->f();

  make_wrapper<logging_wrapper>(&x1)->g();
  return 0;

【讨论】:

这看起来很有趣。尤其是 Bjarne 论文的链接。【参考方案2】:

您可以利用特定于编译器的功能,例如 GCC 的 -finstrument-functions。其他编译器可能具有类似的功能。有关更多详细信息,请参阅此SO question。

另一种方法是使用类似Bjarne Stroustrup's function wrapping 的技术。

【讨论】:

而MSVC++对应的开关是/Gh:msdn.microsoft.com/en-us/library/c63a9b7h.aspx【参考方案3】:

回答你的第一个问题:

大多数动态语言都有method_missing 结构,php 有magic methods(__call__callStatic),Python 有__getattr__。我认为这在 C++ 中不可用的原因是它违背了 C++ 的类型化特性。在一个类上实现这个意味着任何拼写错误最终都会调用这个函数(在运行时!),这会阻止在编译时捕获这些问题。将 C++ 与鸭子类型混合似乎不是一个好主意。 C++ 力求尽可能快,因此一流的函数是不可能的。 AOP。现在这更有趣了,从技术上讲,没有什么可以阻止它被添加到 C++ 标准中(除了在已经非常复杂的标准中添加另一层复杂性可能不是一个好主意)。事实上,有些编译器可以编写代码,AspectC++ 就是其中之一。大约一年前它还不稳定,但看起来从那以后他们设法发布了带有相当不错的测试套件的 1.0,所以它现在可能可以完成这项工作。

有几种技术,这里有一个相关的问题:

Emulating CLOS :before, :after, and :around in C++.

【讨论】:

谢谢,AspectC++ 似乎满足了我的要求。不过,为这个语言扩展需要一个自定义编译器有点吓人:)【参考方案4】:

IMO 这是一个非常有用的特性,那么为什么它不是 C++ 语言特性呢?是否有任何原因阻止这成为可能?

C++ 语言不提供任何直接这样做的方法。但是,它也没有对此(AFAIK)构成任何直接约束。这种类型的功能在解释器中比在本机代码中更容易实现,因为解释器是一个软件,而不是 CPU 流式机器指令。如果你愿意,你可以提供一个支持钩子的 C++ 解释器。

问题是为什么人们使用 C++。很多人使用 C++ 是因为他们想要绝对的执行速度。为了实现这一目标,编译器以操作系统的首选格式输出本机代码,并尝试将尽可能多的内容硬编码到编译后的可执行文件中。最后一部分通常意味着在编译/链接时计算地址。如果你在那个时候修复了一个函数的地址(或者更糟糕的是,内联函数体),那么就不再支持钩子了。

话虽如此,种方法可以使挂钩变得便宜,但它需要编译器扩展并且完全不可移植。 Raymond Chen 的博客介绍了如何在 Windows API 中实现热补丁。他还建议不要在常规代码中使用它。

【讨论】:

【参考方案5】:

这不是 C++ 的事情,但为了完成你提到的一些事情,我在 *nix 系统中使用了 LD_PRELOAD 环境变量。使用这种技术的一个很好的例子是 faketime 库,它与时间函数挂钩。

【讨论】:

【参考方案6】:

至少在我使用的 c++ 框架上提供了一组纯虚拟类

class RunManager;
class PhysicsManager;
// ...

每一个都定义了一组动作

void PreRunAction();
void RunStartAction()
void RunStopAction();
void PostRunAction();

它们是 NOP,但用户可以在派生自 Parent 类的位置覆盖它们。

结合条件编译(是的,我知道“Yuk!”),你就能得到你想要的。

【讨论】:

【参考方案7】:

    必须有一种方法来实现功能而不影响不使用该功能的代码的性能。 C++ 的设计原则是您只需为使用的功能支付性能成本。在每个函数中插入 if 检查以检查其是否被覆盖对于许多 C++ 项目来说速度慢得令人无法接受。特别是,让它工作以便没有性能成本,同时仍然允许独立编译被覆盖和覆盖的函数将是棘手的。 如果您只允许编译时覆盖,那么执行起来会更容易(链接器可以处理覆盖地址),但是您正在比较 ruby​​ 和 javascript,它们可以让您在运行时更改这些内容.

    因为它会颠覆类型系统。如果有人可以覆盖函数的行为,那么函数是私有的还是非虚拟的意味着什么?

    可读性会受到很大影响。任何函数的行为都可能在代码的其他地方被覆盖!您需要了解函数功能的上下文越多,就越难找出大型代码库。 挂钩是一个错误,而不是一项功能。至少如果能够阅读你几个月后写的东西是一项要求的话。

【讨论】:

Hooking 是一个错误,而不是一个特性 - AOP 不同意你的观点,像带有非侵入式检测的分析器之类的工具也是如此。 bug的存在并不能反驳bug就是bug ;) 所以我觉得AOP的存在并不是特别有说服力。探查器不会插入任意代码来更改函数的行为。如果有人提出了一个 AOP 类型系统,可以证明覆盖不会产生违反代码用户假设的副作用,那么我可以加入它。 我同意 (1) 和 (2),但对于 (3),我认为挂钩不是比开放命名空间和允许覆盖更多的错误。他们都具有远距离行动的品质。当然你可以争辩说它们都是错误:) @j_random_hacker:我倾向于说他们都是 ;)

以上是关于C ++中的函数挂钩?的主要内容,如果未能解决你的问题,请参考以下文章

C ++中的延迟执行策略

C语言中的图形函数都有哪些

C语言中,其他文件可否调用文件中的static函数?

c语言函数。

c语言中的shuffle函数功能是啥?

C语言中的main函数参数作用