使用 C++11 接口包装 C 回调的最佳方法是什么?

What's the best way to wrap a C callback with a C++11 interface?

本文关键字:最佳 方法 是什么 回调 C++11 接口 包装 使用      更新时间:2023-10-16

假设这是一个要包装的C函数:

void foo(int(__stdcall *callback)());

C函数指针回调的两个主要陷阱是:

  • 无法存储绑定表达式
  • 无法存储捕获的Lambda

我想知道包装这样的函数的最佳方法。第一种方法对于成员函数回调特别有用,第二种方法对于使用周围变量的内联定义特别有用,但它们并不是唯一的用途。

这些特定函数指针的另一个特性是,它们需要使用__stdcall调用约定。据我所知,这完全消除了lambdas的选择,在其他方面也有点麻烦。我也希望至少允许__cdecl

这是我能想出的最好的方法,而不会让事情开始转向依赖函数指针所没有的支持。它通常位于标头中。以下是Coliru的示例。

#include <functional>
//C function in another header I have no control over
extern "C" void foo(int(__stdcall *callback)()) {
    callback();
}
namespace detail {
    std::function<int()> callback; //pretend extern and defined in cpp
    //compatible with the API, but passes work to above variable
    extern "C" int __stdcall proxyCallback() { //pretend defined in cpp
        //possible additional processing
        return callback();
    }
}
template<typename F> //takes anything
void wrappedFoo(F f) {
    detail::callback = f;
    foo(detail::proxyCallback); //call C function with proxy 
}
int main() {
    wrappedFoo([&]() -> int {
        return 5;
    });   
}

然而,存在一个重大缺陷。这不是重新进入。如果在使用变量之前将其重新分配给,则永远不会调用旧函数(不考虑多线程问题)。

我尝试过的一件事是将std::function存储为数据成员并使用对象,这样每个对象都会对不同的变量进行操作,但无法将对象传递给代理。将对象作为参数会导致签名不匹配,并且绑定它不会使结果存储为函数指针。

我有一个想法,但没有玩过,那就是std::function的向量。然而,我认为从中擦除的唯一真正安全的时间是在没有使用它的情况下清除它。然而,每个条目首先添加到wrappedFoo中,然后在proxyCallback中使用。我想知道在前者中递增,在后者中递减,然后在清除向量之前检查零的计数器是否有效,但这听起来是一个比必要的更复杂的解决方案。

有没有任何方法可以用函数指针回调来包装C函数,使得C++包装的版本:

  • 允许任何函数对象
  • 允许的不仅仅是C回调的调用约定(如果关键是它是相同的,则用户可以传入具有正确调用约定的内容)
  • 线程安全/可重新进入

注意:作为Mikael Persson答案的一部分,显而易见的解决方案是使用应该存在的void *参数。然而,遗憾的是,这并不是一个一劳永逸的选择,主要是由于无能。对于那些没有这个选项的函数,存在什么可能性,这可能会变得有趣,并且是获得非常有用答案的主要途径

不幸的是,你运气不好。

有一些方法可以在运行时生成代码,例如,您可以阅读LLVM蹦床内部函数,其中您生成了一个存储附加状态的转发函数,非常类似于lambdas,但由运行时定义。

不幸的是,这些都不是标准的,因此你被困了。


传递状态的最简单的解决方案是…实际传递状态

定义良好的C回调将使用两个参数:

  • 指向回调函数本身的指针
  • void*

后者不被代码本身使用,并且在调用时简单地传递给回调。根据接口的不同,回调负责销毁它,或者供应商,甚至可以传递第三个"销毁"函数。

有了这样的接口,您可以在线程安全的&C级的重新进入时尚,因此自然地将其封装在具有相同属性的C++中。

template <typename Result, typename... Args)
Result wrapper(void* state, Args... args) {
    using FuncWrapper = std::function<Result(Args...)>;
    FuncWrapper& w = *reinterpret_cast<FuncWrapper*>(state);
    return w(args...);
}
template <typename Result, typename... Args)
auto make_wrapper(std::function<Result(Args...)>& func)
    -> std::pair<Result (*)(Args...), void*>
{
    void* state = reinterpret_cast<void*>(&func);
    return std::make_pair(&wrapper<Result, Args...>, state);
}

如果C接口不提供这样的设施,你可以四处破解,但最终你会受到限制。正如前面所说,一个可能的解决方案是使用全局变量从外部保持状态,并尽最大努力避免争用。

这里有一个粗略的草图:

// The FreeList, Store and Release functions are up to you,
// you can use locks, atomics, whatever...
template <size_t N, typename Result, typename... Args>
class Callbacks {
public:
    using FunctionType = Result (*)(Args...);
    using FuncWrapper = std::function<Result(Args...)>;
    static std::pair<FunctionType, size_t> Generate(FuncWrapper&& func) {
        // 1. Using the free-list, find the index in which to store "func"
        size_t const index = Store(std::move(state));
        // 2. Select the appropriate "Call" function and return it
        assert(index < N);
        return std::make_pair(Select<0, N-1>(index), index);
    } // Generate
    static void Release(size_t);
private:
    static size_t FreeList[N];
    static FuncWrapper State[N];
    static size_t Store(FuncWrapper&& func);
    template <size_t I, typename = typename std::enable_if<(I < N)>::type>
    static Result Call(Args...&& args) {
        return State[I](std::forward<Args>(args)...);
    } // Call
    template <size_t L, size_t H>
    static FunctionType Select(size_t const index) {
        static size_t const Middle = (L+H)/2;
        if (L == H) { return Call<L>; }
        return index <= Middle ? Select<L, Middle>(index)
                               : Select<Middle + 1, H>(index);
    }
}; // class Callbacks
// Static initialization
template <size_t N, typename Result, typename... Args>
static size_t Callbacks<N, Result, Args...>::FreeList[N] = {};
template <size_t N, typename Result, typename... Args>
static Callbacks<N, Result, Args...>::FuncWrapper Callbacks<N, Result, Args...>::State[N] = {};

这个问题有两个挑战:一个很容易,另一个几乎不可能。

第一个挑战是从任何可调用的"事物"到简单函数指针的静态类型转换(映射)。这个问题只需一个简单的模板就可以解决,没什么大不了的。这解决了调用约定问题(简单地用另一种函数包装一种函数)。std::function模板已经解决了这个问题(这就是它存在的原因)。

主要的挑战是将运行时状态封装到一个普通函数指针中,该指针的签名不允许使用"user-data"void*指针(任何半成品的C API通常都会这样)。这个问题独立于语言(C,C++03,C++11),几乎不可能解决。

你必须了解一个关于任何"母语"的基本事实(以及大多数其他语言)。代码在编译后是固定的,只有数据在运行时才会更改。因此,即使是看起来像是属于对象的一个函数的类成员函数(运行时状态),它也不是,代码是固定的,只有对象的标识(this指针)发生了更改。

另一个基本事实是,函数可以使用的所有外部状态都必须是全局的或作为参数传递。如果删除后者,则只能使用全局状态。根据定义,如果函数的操作依赖于全局状态,则不能重入。

因此,为了能够创建一个(有点-)可重入的*函数,该函数只需一个普通函数指针即可调用,并封装任何通用(状态ful)函数对象(绑定调用、lambdas或其他任何对象),您需要为每个调用提供一段唯一的代码(而不是数据)。换句话说,您需要在运行时生成代码,并将指向该代码的指针(回调函数指针)传递给C函数。这就是"几乎不可能"的由来。这在任何标准C++机制中都是不可能的,我百分之百肯定,因为如果这在C++中是可能的,那么运行时反射也是可能的(事实并非如此)。

理论上,这可能很容易。你所需要的只是一段编译过的"模板"代码(不是C++意义上的模板),你可以复制它,插入一个指向你的状态(或函数对象)的指针作为一种硬编码的局部变量,然后把代码放在一些动态分配的内存中(有一些引用计数或其他什么,以确保它在需要的时候存在)。但要做到这一点显然非常棘手,而且在很大程度上是一种"黑客攻击"。老实说,这远远超出了我的技能水平,所以我甚至无法指导你如何做到这一点。

在实践中,现实的选择是甚至不要尝试这样做。使用全局(extern)变量传递状态(函数对象)的解决方案正朝着正确的方向前进。你可以有一个函数池,每个函数都有自己的全局函数对象要调用,你可以跟踪哪个函数当前用作回调,并在需要时分配未使用的函数。如果你用完了有限的函数,你将不得不抛出一个异常(或者你喜欢的任何错误报告)。该方案本质上等同于上述"理论上"的解决方案,但使用的并发回调数量有限。还有其他类似的解决方案,但这取决于特定应用程序的性质。

很抱歉,这个答案并没有给你一个很好的解决方案,但有时根本没有什么灵丹妙药。

另一种选择是避免使用C API,因为它是由从未听说过不可避免且非常有用的void* user_data参数的小丑设计的。

*"有点"可重入,因为它仍然指"全局"状态,但它是可重入的,因为不同的回调(需要不同的状态)不会相互干扰,这是您最初的问题。

如前所述,C函数指针不包含任何状态,因此不带参数调用的回调函数只能访问全局状态。因此,这种"无状态"回调函数只能在一个上下文中使用,其中上下文存储在全局变量中。然后为不同的上下文声明不同的回调。

如果所需回调的数量动态变化(例如,在GUI中,用户打开的每个窗口都需要一个新的回调来处理对该窗口的输入),那么预先定义一个简单的无状态回调的大池,该池映射到有状态回调。在C中,可以按如下方式进行:

struct cbdata { void (*f)(void *); void *arg; } cb[10000];
void cb0000(void) { (*cb[0].f)(cb[0].arg); }
void cb0001(void) { (*cb[1].f)(cb[1].arg); }
...
void cb9999(void) { (*cb[9999].f)(cb[99999].arg); }
void (*cbfs[10000])(void) =
    { cb0000, cb0001, ... cb9999 };

然后使用一些更高级别的模块来保留可用回调的列表。

使用GCC(但不使用G++,因此以下内容需要在一个严格的C文件中,而不是C++文件中),您可以通过使用一个不太为人所知的GCC特性,嵌套函数来创建新的回调函数:

void makecallback(void *state, void (*cb)(void *), void (*cont)(void *, void (*)()))
{
    void mycallback() { cb(state); }
    cont(state, mycallback);
}

在这种情况下,GCC为您创建必要的代码生成代码。缺点是,它将您限制在GNU编译器集合中,并且NX位不能再在堆栈上使用,因为即使您的代码也需要在堆栈上添加新代码。

从高级代码中调用makecallback(),以创建一个具有封装状态的新匿名回调函数。如果调用这个新函数,它将使用arg-state调用statefull回调函数cb。只要makecallback()不返回,新的匿名回调函数就可用。因此,makecallback()通过调用传入的"cont"函数将控制权返回给调用代码。本例假设实际的回调cb()和正常的continue函数cont()都使用相同的状态"state"。也可以使用两个不同的void指针来向两者传递不同的状态。

当不再需要回调时,"cont"函数只能返回(并且应该返回以避免内存泄漏)。如果你的应用程序是多线程的,并且需要各种回调,主要用于它的各种线程,那么你应该能够让每个线程在启动时通过makecallback()分配它所需的回调。

然而,如果你的应用程序是多线程的,并且你有(或可以建立)严格的回调到线程的关系,那么你可以使用线程本地变量来传递所需的状态。当然,只有当您的lib在正确的线程中调用回调时,这才会起作用。