12

假设这是一个要包装的 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 *应该存在的参数。然而,遗憾的是,这不是一个万能的、最终的选择,主要是由于无能。对于那些没有此选项的功能,存在哪些可能性是有趣的地方,并且是获得非常有用的答案的主要途径。

4

3 回答 3

6

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

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

主要挑战是将运行时状态封装到普通函数指针中,其签名不允许“用户数据”void*指针(就像任何半体面的 C API 通常具有的那样)。这个问题与语言(C、C++03、C++11)无关,几乎不可能解决。

您必须了解任何“母语”语言(以及大多数其他语言)的基本事实。代码在编译后是固定的,只有数据在运行时发生变化。因此,即使是一个类成员函数,它看起来好像是属于对象的一个​​函数(运行时状态),但它不是,代码是固定的,只是对象的身份发生了变化(this指针)。

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

因此,能够创建一个(某种)可重入*函数,该函数仅可使用普通函数指针调用,并且封装任何通用(有状态)函数对象(绑定调用、lambda 或其他) ,每次调用都需要一段唯一的代码(不是数据)。换句话说,您需要在运行时生成代码,并将指向该代码的指针(回调函数指针)传递给 C 函数。这就是“几乎不可能”的来源。这是不可能通过任何标准的 C++ 机制实现的,我 100% 确信这一点,因为如果这在 C++ 中是可能的,那么运行时反射也是可能的(但事实并非如此)。

从理论上讲,这可能很容易。您所需要的只是一段已编译的“模板”代码(不是 C++ 意义上的模板),您可以复制它,将指向您的状态(或函数对象)的指针作为一种硬编码的局部变量插入,然后放置代码到一些动态分配的内存中(使用一些引用计数或其他任何东西来确保它只要需要就存在)。但要做到这一点显然非常棘手,而且非常“黑客”。老实说,这远远超出了我的技能水平,所以我什至无法指导你如何做到这一点。

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

很抱歉,这个答案并没有给你一个很好的解决方案,但有时只是没有任何灵丹妙药。

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

*“某种”重入,因为它仍然指的是“全局”状态,但它是重入的,因为不同的回调(需要不同的状态)不会相互干扰,就像你原来的问题一样。

于 2013-08-11T07:16:20.163 回答
6

不幸的是,你运气不好。

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

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


传递状态的最简单解决方案是......实际传递状态。啊!

定义良好的 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] = {};
于 2013-08-11T12:05:51.687 回答
1

如前所述,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() 都使用相同的状态,“状态”。也可以使用两个不同的 void 指针将不同的状态传递给两者。

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

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

于 2013-08-11T09:57:01.527 回答