如何使用C++11接口封装C回调函数?

12

假设这是一个需要被封装的C函数:

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

C函数指针回调的两个主要问题是:

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

我想知道如何包装这些函数以实现这样的功能。第一个问题尤其适用于成员函数回调,第二个问题适用于使用周围变量的内联定义,但这些并不是唯一的用途。

这些特定函数指针的另一个属性是它们需要使用__stdcall调用约定。据我所知,这完全排除了Lambda表达式作为选项,并且在其他情况下有点麻烦。我希望至少允许 __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回调的调用约定(如果这是至关重要的,用户可以传递具有正确调用约定的内容)
  • 线程安全/可重入

注意:显而易见的解决方案是利用应该存在的void *参数,如Mikael Persson的答案所述。然而,由于无能力,这很遗憾不是一个万无一失的选择。对于那些没有此选项的函数,存在什么可能性才是有趣的地方,并且是获得非常有用的答案的主要途径。


这不是可重入的。如果在使用它之前重新分配变量,则旧函数将永远不会被调用(不考虑多线程问题)。 如果发生这种情况,那么你是否正在从两个不同的线程中设置相同的回调?除非您注册的回调系统可以处理这一点,否则无论您的包装系统是否可以处理都无关紧要。无论是在您的代码还是在他们的代码中,破损就是破损。 - Nicol Bolas
@NicolBolas,当然可以。我有些犹豫要不要提到这个问题是源自Windows API的CreateWindow,据我所知,它应该可以同时从两个线程中调用,并且还有一个(更隐蔽的)void *选项。犹豫的原因是我希望这个问题比那个具体一点。不过,这确实是一个值得思考的好点子。 - chris
2
@NicolBolas 所描述的情况是当使用相同的包装函数(回调指针指向的函数)用于注册多个回调时(在多次调用 C API 函数后),如果您只有一个全局函数对象(指向实际的回调函数/lambda/等),则无法正常工作。这与多线程本身无关,尽管多线程会引入更多问题。 - Mikael Persson
@MikaelPersson,我对自己感到震惊,但出于某种原因,这甚至没有在我的脑海中闪过。这真的很重要。 - chris
@chris:你的问题只适用于不带用户定义数据参数的回调函数(即CreateWindow所使用的lpParam)。任何具有此类存储的回调函数都将正常工作。唯一需要诉诸全局手段的时候是那些不接受这种参数的非常恼人的回调函数。 - Nicol Bolas
@NicolBolas,是的,这个问题的意图是更广泛地解决问题,而不仅仅是激发它的半个问题利用那个方法就可轻易解决。如果我能找到一种不破坏现有答案的措辞,我可能会编辑问题以更好地反映这一点。 - chris
3个回答

6
很遗憾,你没有运气了。
有一些方法可以在运行时生成代码,例如您可以阅读LLVM trampoline intrinsics,其中您可以生成一个存储附加状态的转发函数,非常类似于lambda但是在运行时定义。
不幸的是,这些方法都不是标准的,因此你被困住了。
最简单的传递状态的解决方案是...实际上传递状态。啊! 良好定义的 C 回调将采用两个参数:
  • 指向回调函数本身的指针
  • void*
后者未被代码本身使用,只是在调用回调时传递给回调。根据接口,回调要么负责销毁它,要么由供应商销毁,甚至可以传递第三个"destroy"函数。
通过这样的接口,您可以以线程安全和可重入的方式有效地传递状态,在 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] = {};

1
小心使用trampolines,因为它们有时需要NX-Stacks(以及它们在GCC中的其他商标名称,如嵌套函数)。 NX-Stacks有时会违反安全编码/最佳实践,并且使用它们的代码无法通过安全门。 在Windows系统上,它们既违反了安全编码又违反了最佳实践。 - jww
@jww:确实会存在安全问题,感谢您指出。 - Matthieu M.

6
这个问题有两个挑战:一个容易的,一个几乎不可能。
第一个挑战是将任何可调用的“东西”静态类型转换(映射)为简单的函数指针。这个问题可以通过一个简单的模板解决,没什么大不了的事情。这解决了调用约定问题(只是用另一种类型的函数包装另一种类型的函数)。这已经被 std::function 模板解决了(这就是它存在的原因)。
主要挑战是将运行时状态封装到一个普通的函数指针中,而该指针的签名不允许使用“用户数据”void* 指针(正常情况下任何半好的 C API 都会有这个)。这个问题与语言无关(C、C++03、C++11),几乎不可能解决。
你必须理解任何“本地”语言(以及大多数其他语言)的一个根本事实。代码在编译后是固定的,只有数据在运行时更改。因此,即使是作为对象(运行时状态)的一部分看起来像是属于该对象的一个函数成员,实际上并不是,代码是固定的,只有对象的身份(this 指针)会改变。
另一个基本事实是,函数可以使用的所有外部状态必须是全局的或作为参数传递。如果你消除了后者,你只能使用全局状态。而且根据定义,如果函数的操作取决于全局状态,则它不能是可重入的。
因此,要能够创建一个(几乎)可重入的函数,只需使用普通的函数指针调用,并封装任何一般的(有状态的)函数对象(绑定调用、lambda 或其他任何东西),你需要为每次调用生成唯一的代码块(不是数据)。换句话说,你需要在运行时生成代码,并将指向该代码的指针(回调函数指针)交给 C 函数。这就是“几乎不可能”的原因。这不可能通过任何标准的 C++ 机制实现,我对此非常确定,因为如果在 C++ 中可能做到这一点,运行时反射也可能实现(而事实并非如此)。
理论上,这可能很容易。你只需要一小段编译好的“模板”代码(不是 C++ 中的模板),可以将其复制、插入一个指向状态(或函数对象)的指针,作为一种硬编码的局部变量,并将该代码放入一些动态分配的内存中(使用某些引用计数或其他方式确保它存在的时间与需要的时间一样长)。但是,实现这个过程非常棘手,而且很大程度上属于“黑客”领域。老实说,这已经超出了我的技能水平,所以我甚至无法指导你如何确切地去做这件事。
实际上,现实的选择是不要尝试这样做。你的解决方案使用全局(extern)变量来传递状态(函数对象),在妥协方面朝着正确的方向前进。你可以有一些类似于函数池的东西,每个函数都有自己的全局函数对象可供调用,并且你会跟踪当前使用的回调函数,并在需要时分配未使用的回调函数。如果你用完了这些有限的函数,你必须抛出一个异常(或者其他你喜欢的错误报告方式)。这个方案与上面的“理论”解决方案本质上是等效的,但只能使用有限数量的并发回调。还有其他类似的解决方案,但这取决于特定应用程序的性质。
很抱歉,这个答案没有给你一个很好的解决方案,但有时候就是没有什么万能药。
另一个选择是避免使用由从未听说过不可避免且非常有用的void* user_data参数的蠢货设计的C API。
"*" "有点"可重入,因为它仍然涉及一个“全局”状态,但在不同的回调之间(需要不同的状态)不会相互干扰,这就是你最初的问题。

我必须说,池的想法有点有趣。我确实需要做出一些关于放弃什么的决定,但至少大多数合理的例子都有void *参数,这使得它成为一个相当合理的答案。对于那些没有的情况,它们并不是所有的都支持同时从两个线程调用,所以这又缩小了列表。从那里开始,就是做决定的时候了。 - chris
1
我对这个答案有两个问题。首先,也许只是我,但我不明白为什么需要运行时代码生成。如果我们在黑客,为什么不注入指向状态本身(仅为数据)的指针呢?另一件事是,运行时代码生成本身并不是“几乎不可能”的。如果是这样的话,那么为编程语言解释器创建JIT编译器也将是几乎不可能的;然而,我们有像LLVM!,Nitro,JVM等东西。 - user529758
+1 只是为了“另一个选择是避免使用由从未听说过不可避免且非常有用的void* user_data参数的蠢货设计的C API。” 我会选择“白痴”。任何设计此类回调设置代码而在回调中没有此参数可用的库开发人员应立即被解雇(或者如果有小队可用,则应被解雇)。 - Martin James
2
@H2CO3:我同意,虽然大部分答案都很好,但是关于代码生成的部分就完全错误了...它甚至与反射没有任何相似之处!Mikael Persson:我建议你阅读有关运行时生成跳板的内容。它们实际上不是标准的,特别是因为在一些平台上代码和数据是严格分离的(而JITting需要将数据转换为代码),但是它们可以用来解决上述问题,并且一些编译器(如LLVM)本地支持它们。 - Matthieu M.
@MatthieuM。感谢您指出这一点。正如我所说,做这件事情“相当超出了我的技能水平”。而“几乎不可能”是指以标准可移植的方式来完成这个任务。LLVM的这个特性看起来非常有趣,我需要深入研究一下。 - Mikael Persson
@MikaelPersson: 没问题,我们都在这里学习 :) 我也不是这个领域的专家...我从来没有自己写过JIT,甚至也没尝试过LLVM的一个。 - Matthieu M.

2

如前所述,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文件中),您甚至可以使用一个不太知名的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()不返回,新的匿名回调函数就可用。因此,通过调用传递的“cont”函数,makecallback()将控制权返回给调用代码。此示例假定实际回调cb()和正常继续函数cont()都使用相同的状态“state”。还可以使用两个不同的无类型指针将不同的状态传递给两者。
只有当不再需要回调时,“cont”函数才会返回(并应该返回以避免内存泄漏)。如果您的应用程序是多线程的,并且大多数情况下需要各种回调函数来执行各自的线程,则应该能够使每个线程通过makecallback()分配其所需的回调函数。
然而,如果您的应用程序已经是多线程的,并且如果您有(或可以建立)严格的回调到线程关系,则可以使用线程本地变量来传递所需的状态。当然,这只有在您的lib在正确的线程中调用回调时才有效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接