【问题标题】:Run-time implementation of std::functionstd::function 的运行时实现
【发布时间】:2020-12-20 16:30:26
【问题描述】:

为了安全起见,我已经在我的 DLL 调用中使用了旧式函数指针,如下所示:

// DLL
typedef int (__stdcall* ty)();
void test(ty t)
{
    if (t)
    {
        int r = t(); 
        ....
    }
}

而我可以使用这个:

void test(std::function<int()> t)
{
}

然而,众所周知,后者使用type erasurestd::function 不能是原始函数指针(因为它可以传递一个具有捕获的 lambda,因此不能是原始指针)。

因此,在 Visual Studio 中,使用包含 std::function 的函数签名的 DLL 版本在发布模式下在调试模式下从可执行版本中使用时会崩溃,反之亦然。即使它是调试或发布模式,行为也不相同。有时它会崩溃,有时它会工作。

是否存在定义的运行时行为,我们可以依赖它来使用std::function?或者这是编译器专门根据我传递给它的内容而专门编译的东西,因此不能预先假定运行时行为?

在汇编级别,预编译单元上的函数签名必须是已知的。据我所知std::function runtime 实现没有很好的定义。

所以,例如,当我在编译时,那么

void test(std::function<int()> t)

可以通过类型擦除接受任何参数,如[]() -&gt; int, [&amp;]() -&gt; int, [=]() -&gt; int 等。但是,当这是预编译的,因此只在运行时,可以接受什么? std::function 是如何实现的?使用类指针?有没有明确的方法?

我不是在寻找一个必然与 VS 相关的解决方案,而是std::function 的标准定义,如果有的话。

【问题讨论】:

  • 我不确定我是否理解这个问题。 Debug 和 Release 组件不能混合的原因有很多,不仅仅是这个。
  • @PaulSanders 我编辑过,调试/发布就是一个例子。当实际使用任何预编译的内容时,它也会失败。主要思想是了解std::function的运行时实现是如何定义的。
  • 类型擦除是关于实现的,你想对语言的指定行为/含义说什么?

标签: c++


【解决方案1】:

因此,在 Visual Studio 中,使用包含 std::function 的函数签名的 DLL 版本在发布模式下在调试模式下从可执行版本中使用时会崩溃,反之亦然。

这永远不会起作用,因为它会更改 ABI。

即使是调试或发布模式,行为也不相同。有时它会崩溃,有时它会工作。

如果您注意编译器标志、依赖的静态库和动态库、C 和 C++ 标准库的编译方式、异常等,可能会起作用。这是一个复杂的主题,取决于编译器供应商保证什么。

是否有一个定义好的运行时行为,我们可以依赖它来使用std::function

一般来说,您最好的选择(也是为其他语言创建绑定最有用的选择)是避免使用 C++ 接口并使用普通类型的普通 C 接口。

也就是说,如果您想传递 C++ 类型,请将它们作为不透明类型传递,并且只从一侧操作它们。

我不是在寻找与 VS 相关的解决方案,而是在寻找 std::function 的标准定义,如果有的话。

C++ 标准不强制使用任何特定的 ABI,也不提供诸如绝大多数类型的数据成员之类的实现细节。

这就是为什么即使您以完全相同的方式编译所有内容,混合不同的 STL 库也是一个问题。

【讨论】:

  • 这不仅仅是动态/静态或调试/发布的问题,就像其他 C++ 方面一样。即使所有选项都相同,我也看不到它如何工作的保证。我不是在寻找与 VS 相关的解决方案,而是在寻找 std::function 的标准定义,如果有的话。
  • @MichaelChourdakis 添加到答案和问题中。
【解决方案2】:

旧式回调应该是一个函数指针一个空指针。

这样,您可以发送一个 std 函数(但不能存储它)。

您可以将这些操作封装在标准布局类中,这样可以 99.99% 安全地跨越 DLL 边界。运行/注入的代码无法提前卸载,但这与 C 风格的回调没有什么不同。

为了安全地存储可调用数据,您需要 call-with-state 和 destroy-state 操作。

要以完全 dll 安全的方式执行此操作...

template<class Sig>
struct callback;

template<class R, class...Args>
struct callback<R(Args...)>{
  // standard layout data:
  void* state=0;
  R(*operate)(void*, Args...)=0;
  void(*cleanup)(void*)=0;

  voud clear(){ if(cleanup){cleanup(state);}state=0; operate=0; cleanup=0; }
  callback(callback const&)=delete;
  callback(callback&&o):state(o.state),operate(o.operate),cleanup(o.cleanup){o.cleanup=0;o.clear();}
  callback& operator=(callback&&o){
    if(this==&o) return *this;
    clear();
    state=o.state;
    operate=o.operate;
    cleanup=o.cleanup;
    o.clear();
    return *this;
  }

  ~callback(){ clear(); }
  R operator()(Args...args)const{return operate(state, std::forward<Args>(args)...);}
};

您可以很容易地将 std 函数转换为上述函数,反之亦然。

这是标准布局,因此应该可以安全地跨越 DLL 边界。

但如果是极度偏执,你可以解压并一个一个地发送 2 个函数指针和一个 void 指针。

将一个有状态的对象放入上面:

template<class T> void(*deleter)(void*)=+[](void*ptr){delete static_cast<T*>(ptr);};
template<class F, class R, class...Args> R(*caller)(void*,Args...)=
  +[](void* ptr, Args...args)->R{
    return (*(F*)(ptr))(std::forward<Args>(args)...);};

 template<class F, class R, class...Args>
 callback<R(Args...)> make_callback(F&& f){
   callback<R(Args...)> retval;
   retval.state=new std::decay_t<F>(std::forward<F>(f));
   retval.operate=caller<std::decay_t<F>, R, Args...>;
   retval.cleanup=deleter<std::decay_t<F>>;
   return retval;
}

适用于 lambda 或 std 函数。

一个原始函数:

template<class R,class...Args>
callback<R(Args...)> make_callback(R(*pf)(Args...)){
  callback<R(Args...)> retval;
  retval.state=reinterpret_cast<void*>(pf);// note on some obscure platforms this does not work.
  retval.operate=caller<R(Args...), R, Args...>;// ditto
}

无需清理。

这个callback 是一个只移动的精简标准函数实现。可以在保持 dll 安全的同时进行更多改进。

【讨论】:

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