c++中的函数挂钩

Function hooking in C++?

本文关键字:函数 c++      更新时间:2023-10-16

我所说的"hook "是指非侵入性地覆盖函数行为的能力。一些例子:

  • 在函数体前后打印日志信息
  • 将函数体封装在try catch体中。
  • 测量函数持续时间
  • 等等…

我见过各种编程语言和库的不同实现:

    面向方面编程
  • JavaScript的第一类函数
  • OOP装饰模式
  • WinAPI子类化
  • Ruby的method_missing
  • SWIG的%exception关键字意味着将所有函数包装在一个try/catch块中,可以(ab)用于挂钩
  • 的目的

我的问题是:

  • 在我看来,这是一个非常有用的功能,我想知道为什么它从来没有作为c++语言功能实现。有什么原因阻止这一切成为可能吗?
  • 有哪些推荐的技术或库可以在c++程序中实现这一点?

如果您正在谈论导致在函数体之前/之后调用新方法,而不改变函数体,您可以基于此,它使用自定义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;
}

您可以利用编译器特定的特性,例如GCC的- fininstrument -functions。其他编译器可能也有类似的特性。有关更多细节,请参阅这个SO问题。

另一种方法是使用Bjarne Stroustrup的函数包装技术。

回答第一个问题:

  • 大多数动态语言都有自己的method_missing结构,PHP有一个神奇的方法(__call__callStatic), Python有__getattr__。我认为这在c++中不可用的原因是它违背了c++的类型化本质。在类上实现这一点意味着任何错别字最终都会调用这个函数(在运行时!),这可以防止在编译时捕获这些问题。将c++与duck类型混合在一起似乎不是个好主意。
  • c++试图尽可能快,所以第一类函数是没有问题的。
  • AOP。现在更有趣的是,从技术上讲,没有什么可以阻止它被添加到c++标准中(除了在已经非常复杂的标准上添加另一层复杂性可能不是一个好主意)。事实上,有一些编译器可以转换代码,aspectc++就是其中之一。大约一年前,它还不稳定,但看起来从那以后,他们设法发布了1.0,并提供了一个相当不错的测试套件,所以现在它可能已经完成了工作。

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

模拟c++中的CLOS:before,:after和:around。

在我看来,这是一个非常有用的特性,那么为什么它不是c++语言的特性呢?有什么原因阻止这一切成为可能吗?

c++语言并没有提供任何直接执行此操作的方法。然而,它也没有对这构成任何直接的约束(AFAIK)。这种类型的特性在解释器中比在本机代码中更容易实现,因为解释器是一个软件,而不是CPU流机器指令。如果你愿意,你完全可以提供一个支持钩子的c++解释器。

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

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

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

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

class RunManager;
class PhysicsManager;
// ...

每一个都定义了一组动作

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

,它们是nop,但用户可以在从Parent类派生的地方重写它们。

与条件编译相结合(是的,我知道"哎呀!"),你可以得到你想要的。

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

  2. 因为它会破坏类型系统。如果某人无论如何都可以重写函数的行为,那么函数是私有的或非虚拟的意味着什么?

  3. 可读性会受到很大影响。任何函数的行为都可能在代码的其他地方被重写!了解函数所做的工作所需的上下文越多,就越难弄清楚大型代码库。钩子是一个bug,而不是一个特性