【问题标题】:What's the best way to wrap a C callback with a C++11 interface?用 C++11 接口包装 C 回调的最佳方法是什么?
【发布时间】:2013-08-12 17:07:06
【问题描述】:

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

【问题讨论】:

  • "这不是可重入的。如果变量在使用之前被重新分配,则永远不会调用旧函数(不考虑多线程问题)。"如果发生这种情况,那么您不是正在从两个不同的线程设置相同的回调吗?除非您注册的回调系统可以处理这个问题,否则您的包装系统是否可以处理并不重要。 Broken 被破坏了,无论是在你的代码中还是在他们的代码中。
  • @NicolBolas,嗯,绝对可以。我不愿提及这个问题是从 Windows API CreateWindow 产生的,它应该可以同时从两个线程调用 AFAIK,并且还有一个(更隐藏的)void * 选项。犹豫的原因是我的意思是这个问题比这更抽象一点。不过,这是一个值得考虑的好点。
  • @NicolBolas 所描述的情况是当同一个包装函数(回调指针指向的)用于注册多个回调(在多次调用 C API 函数之后),如果您只有一个全局函数对象(指向实际的回调函子 / lambda / 其他)。这与多线程本身无关,尽管多线程确实引入了更多问题。
  • @MikaelPersson,我对自己感到震惊,但出于某种原因,我什至没有想到这一点。这真的很重要。
  • @chris:您的问题应该只适用于不采用用户定义数据参数的回调(即:CreateWindow 采用的lpParam)。任何具有此类存储的回调都可以。唯一需要诉诸全局恶作剧的是那些不采用此类参数的真正烦人的回调。

标签: c++ c++11 lambda callback stdbind


【解决方案1】:

不幸的是,你运气不好。

有一些方法可以在运行时生成代码,例如,您可以阅读 LLVM trampoline intrinsics,在其中生成存储附加状态的转发函数,非常类似于 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] = {};

【讨论】:

  • 小心蹦床,因为它们有时需要 NX-Stacks(以及它们的其他商品名称,如 GCC 中的嵌套函数)。 NX-Stacks 有时违反了安全编码/最佳实践,使用它们的代码无法通过安全门。在 Windows 系统上,它们违反了安全编码和最佳实践。
  • @jww:这确实是一个安全问题,感谢您指出。
【解决方案2】:

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

  • 我不得不说游泳池的想法很有趣。我确实有一些决定要放弃什么,但至少大多数合理的例子确实有void * 参数,这是一个相当合理的答案。对于那些不支持的人,并非所有人都支持同时从两个线程调用,因此再次缩小了列表范围。从那里开始,是时候做出决定了。
  • 我对这个答案有两个问题。首先,也许只有我一个人,但我不明白为什么需要生成运行时代码。如果我们在进行黑客攻击,为什么我们不进行黑客攻击,以便注入指向状态本身的指针(仅是数据)?另一件事是运行时代码生成本身并不是“几乎不可能”。如果是这样,那么为编程语言解释器创建 JIT 编译器也几乎是不可能的。但是,我们有 LLVM (!)、Nitro、JVM 等。
  • +1 仅用于“另一种选择是避免使用由从未听说过不可避免且非常有用的 void* user_data 参数的小丑设计的 C API。”我会选择“白痴”。任何设计此类回调设置代码但在回调中没有可用参数的 lib 开发人员都应立即被解雇(或者,如果有小队可用,则被解雇)。
  • @H2CO3:我同意,虽然大部分答案都很好地引出了,但关于代码生成的部分完全是错误的......它甚至不是类似于反射的东西! Mikael Persson:我建议您阅读运行时生成的蹦床。它们实际上不是标准的,特别是因为在许多代码和数据严格分离的平台上 JITting 是不可能的(并且 JITting 需要将数据转换为代码),但是它们可以用来解决上述问题和一些编译器(例如 LLVM ) 原生支持它们。
  • @MatthieuM。感谢您指出了这一点。正如我所说,这样做“远远超过我的技能水平”。而“几乎不可能”是指以标准的便携式方式执行此操作。 LLVM 的这个特性看起来很有趣,我得研究一下。
【解决方案3】:

如前所述,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() 分配其所需的回调。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-28
    • 1970-01-01
    • 1970-01-01
    • 2016-05-08
    • 1970-01-01
    • 2022-01-19
    • 2013-05-18
    • 2013-08-10
    相关资源
    最近更新 更多