将C++成员函数的指针传递给C API库

3

这是我第一次在这里发帖,我尝试查看了之前的类似帖子,但它们似乎都不起作用或者不能完全满足我的需求。

我有一些C代码,称之为library.c。我已经删除了很多代码并对其进行了简化。

// libary.c
// A callback function that takes an int
void (*library_cb)(int);

void init(void (*cb())) {
    // some other code
    library_cb = cb;
}

void sample() {
    int data;
    // execute a bunch of code and then call the callback function
    (*library_cb)(data);
}

现在我有一段C++代码,其中定义了回调函数,我想将其传递给library.c中的代码。
// someclass.cpp

class SomeClass {
    public:
        SomeClass() {

        };

        ~SomeClass() {

        };

        void callback(int data) {
            // Do some stuff
        }
};

然后在main.cpp中,我想做类似以下的操作:

// main.cpp
extern "C" {
    #include "library.h"
}
#include "someclass.h"

SomeClass some_class;

int main() {
    init(&some_class.callback) // obviously doesn't work

    while(true) {
        sample(); // this would call callback in SomeClass
    }
}

现在我知道的一个解决方案是将回调函数定义为
static void callback(int data)

但我在想是否有其他方法可以做到这一点。从我读的内容来看,std::function可能会有所帮助或者std::mem_fn。但我似乎无法弄清楚如何做。

我没有包含头文件,并且我写了这个代码作为我的问题的示例,因此可能会有一些语法错误,但希望问题/目标是清晰的。

编辑:

我应该提到我可以编辑c库。

阅读答案,似乎我可以更改c库以接受指向类对象的void*指针,以使其工作。请问有人能为这种情况给我一个示例吗?我对将c代码与c ++进行接口非常陌生。


如果C库函数只接受函数指针,那就没办法了。对此无可奈何。就这些了。但是大多数C库都足够聪明,它们知道C++存在于更广阔的世界中,并且会接受一个不透明的void *作为额外的回调参数,或者允许你在方便的地方将void *放进去。你可以使用它来存储附加的元数据并使自己逐步转向正确的C++方法调用。 - Sam Varshavchik
我编辑了我的帖子以表明我可以更改C库。我确实读到过可以使用void *来完成这个操作。但是我无法理解如何做到。你能给我一个简单的示例吗? - C. Dog
简单来说,你将this作为一个不透明的void *传递给C库,同时还要传递一个指向你的类的static成员函数的指针。这个成员函数将接收到C库返回的void *作为参数,并将其重新转换回对象指针,然后调用对象的方法。例如,这是一个从gnutls C库调用的回调函数,它会调用一个C++对象的方法。 - Sam Varshavchik
1
@SamVarshavchik "如果C库函数只接受函数指针,那就没办法了" - 这并不是真的。有一些技巧可以处理这种情况。比如将对象指针存储在全局或线程本地变量中,以便回调函数可以访问它。或者使用一个thunk作为回调函数,其中thunk内部编码了对象的地址,因此可以将调用委托给对象。所以,有一些可选方案可供选择。 - Remy Lebeau
@RemyLebeau 当然,所有这些“选项”都是为那些只在琐碎的示例中使用他们的代码或者根本不知道更好方法的库设计者提供的变通办法。当然,可以使用thunks,并且甚至有一些库可以自动化这个过程:但是对于一个有问题的API来说,这会带来很多麻烦。我同意这些变通办法的存在。但是,作为一个库设计者,在提交代码时最好不要招惹太多敌人。 - Kuba hasn't forgotten Monica
2个回答

3
将C++成员函数的指针传递给C API库是不可能的。从我所了解的情况来看,std::function 和 std::mem_fn 也无法在C中调用,但请继续阅读到最后。C仅支持常规的非成员函数指针,因此这些是C程序可以调用的唯一函数指针。在C++中,这样的指针可以指向自由函数或静态成员函数。在这样的静态或非成员函数的C++实现中,您当然可以执行任何C++的操作(尽管让异常逃逸函数会很糟糕),因此您确实可以在其中调用非静态成员函数。但是要调用非静态成员函数,需要存在一个实例。静态对象是一个简单的解决方案,但并不是非常灵活,并且只在少数情况下有用。在C中精心设计的回调API允许API的用户注册一个通用数据指针(即void *)以及函数指针,并将该数据指针转发到回调函数中。该设计允许回调具有状态,该状态存储在指向的对象中。使用这样的C API时,可以传递指向其成员函数的对象的指针,回调函数随后可以调用它们。或者,您可以传递指向std :: function或某些其他状态类型擦除函数包装器的数据指针,并使用一个通用自由函数,简单地将调用转发给包装器。

我有点理解你通过将回调函数的对象传递给指针来说什么。但如果你能给我一个示例代码就太好了。 - C. Dog

1

您的C API不太易用。这是我会这样做的:

  1. 回调函数必须至少接受一个用户提供的 void* 参数,库不会以任何方式解释它。没有这个参数,回调函数就是无用的。是的,他们真的是无用的,你的API用户会讨厌你

  2. 如果您希望回调函数能够修改其参数的值,可以传递 void* 参数的地址。这对于例如在注册时进行分配等参数在回调执行期间更改的情况非常有用。这使得库与指针的使用完全解耦:它不仅不解释指针,而且不保持其值的恒定。

  3. 库API符号都有前缀,以防止全局命名空间中的冲突。

  4. 必要时使用typedef确保可读性。手动输入函数指针类型很繁琐。

  5. 头文件受到多重包含的保护,即在一个翻译单元中多次包含它应该是可以的,而不会出现任何错误。

  6. 当在C++翻译单元中编译时,头文件声明了C接口,因为接口是C接口。C++会对符号名称进行编码,头文件将声明不兼容的二进制符号。

  7. 头文件在C++11中声明了C接口noexcept。这为C++用户提供了优化机会。

  8. 考虑库注册多个回调函数,以及可能在注册和注销时调用回调函数:这使得与其他编程语言的互操作性更加容易。

library.h - 可用于 C 和 C++

#pragma once

#ifdef __cplusplus
extern "C" {
#pragma GCC diagnostic push
// clang erroneously issues a warning in spite of extern "C" linkage
#pragma GCC diagnostic ignored "-Wc++17-compat-mangling"
#endif

#ifndef LIBRARY_NOEXCEPT
#if __cplusplus >= 201103L
// c.f. https://dev59.com/AGAf5IYBdhLWcg3wskiQ
#define LIBRARY_NOEXCEPT noexcept
#else
#define LIBRARY_NOEXCEPT
#endif
#endif

enum library_register_enum { LIBRARY_REG_FAILURE = 0, LIBRARY_REG_SUCCESS = 1, LIBRARY_REG_DUPLICATE = -1 };
enum library_call_enum { LIBRARY_SAMPLE, LIBRARY_REGISTER, LIBRARY_DEREGISTER };
typedef enum library_register_enum library_register_result;
typedef enum library_call_enum library_call_type;
#if __cplusplus >= 201103L
void library_callback_dummy(library_call_type, int, void**) LIBRARY_NOEXCEPT;
using library_callback = decltype(&library_callback_dummy);
#else
typedef void (*library_callback)(library_call_type, int, void**);
#endif

void library_init(void) LIBRARY_NOEXCEPT;
library_register_result library_register_callback(library_callback cb, void *cb_param) LIBRARY_NOEXCEPT;
void library_deregister_callback(library_callback cb, void *cb_param) LIBRARY_NOEXCEPT;
void library_deregister_any_callback(library_callback cb) LIBRARY_NOEXCEPT;
void library_deregister_all_callbacks(void) LIBRARY_NOEXCEPT;
void library_deinit(void) LIBRARY_NOEXCEPT;

void library_sample(void) LIBRARY_NOEXCEPT;

#ifdef __cplusplus
#pragma GCC diagnostic pop
}
#endif

请注意,私有数据和函数(即不属于 API 的部分)被声明为 static

library.c - 实现

#include "library.h"
#include <stdlib.h>

typedef struct callback_s {
   struct callback_s *next;
   library_callback function;
   void *parameter;
} callback;

static callback *cb_head;

void library_init(void) { /* some other code */
}
void library_deinit(void) { library_deregister_all_callbacks(); }

library_register_result library_register_callback(library_callback cb, void *cb_param) {
   callback *el = cb_head;
   while (el) {
      if (el->function == cb && el->parameter == cb_param) return LIBRARY_REG_DUPLICATE;
      el = el->next;
   }
   el = malloc(sizeof(callback));
   if (!el) return LIBRARY_REG_FAILURE;
   el->next = cb_head;
   el->function = cb;
   el->parameter = cb_param;
   cb_head = el;
   cb(LIBRARY_REGISTER, 0, &el->parameter);
   return LIBRARY_REG_SUCCESS;
}

static int match_callback(const callback *el, library_callback cb, void *cb_param) {
   return el && el->function == cb && el->parameter == cb_param;
}

static int match_any_callback(const callback *el, library_callback cb, void *cb_param) {
   return el && el->function == cb;
}

static int match_all_callbacks(const callback *el, library_callback cb, void *cb_param) {
   return !!el;
}

typedef int (*matcher)(const callback *, library_callback, void *);

static void deregister_callback(matcher match, library_callback cb, void *cb_param) {
   callback **p = &cb_head;
   while (*p) {
      callback *el = *p;
      if (match(el, cb, cb_param)) {
         *p = el->next;
         el->function(LIBRARY_DEREGISTER, 0, &el->parameter);
         free(el);
      } else
         p = &el->next;
   }
}

void library_deregister_callback(library_callback cb, void *cb_param) {
   deregister_callback(match_callback, cb, cb_param);
}

void library_deregister_any_callback(library_callback cb) {
   deregister_callback(match_any_callback, cb, NULL);
}

void library_deregister_all_callbacks(void) {
   deregister_callback(match_all_callbacks, NULL, NULL);
}

void library_sample(void) {
   int data = 42;
   // execute a bunch of code and then call the callback function
   callback *el = cb_head;
   while (el) {
      el->function(LIBRARY_SAMPLE, data, &el->parameter);
      el = el->next;
   }
}

那么,注册回调的用户可以向回调函数传递任意数据。库使用的代码应该按照以下方式在C++中实现:
// https://github.com/KubaO/stackoverflown/tree/master/questions/c-cpp-library-api-53643120
#include <iostream>
#include <memory>
#include <string>
#include "library.h"

struct Data {
   std::string payload;
   static int counter;
   void print(int value) {
      ++counter;
      std::cout << counter << ": " << value << ", " << payload << std::endl;
   }
};

int Data::counter;

extern "C" void callback1(library_call_type type, int value, void **param) noexcept {
   if (type == LIBRARY_SAMPLE) {
      auto *data = static_cast<Data *>(*param);
      data->print(value);
   }
}

using DataPrintFn = std::function<void(int)>;

extern "C" void callback2(library_call_type type, int value, void **param) noexcept {
   assert(param && *param);
   auto *fun = static_cast<DataPrintFn *>(*param);
   if (type == LIBRARY_SAMPLE)
      (*fun)(value);
   else if (type == LIBRARY_DEREGISTER) {
      delete fun;
      *param = nullptr;
   }
}

void register_callback(Data *data) {
   library_register_callback(&callback1, data);
}

template <typename F>
void register_callback(F &&fun) {
   auto f = std::make_unique<DataPrintFn>(std::forward<F>(fun));
   library_deregister_callback(callback2, f.get());
   library_register_callback(callback2, f.release());
   // the callback will retain the functor
}

int main() {
   Data data;
   data.payload = "payload";

   library_init();
   register_callback(&data);
   register_callback([&](int value) noexcept { data.print(value); });

   library_sample();
   library_sample();
   library_deinit();  // must happen before the 'data' is destructed
   assert(data.counter == 4);
}

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