如何在 C 中公开 C++ 函数指针?

How can I expose C++ function pointers in C?

我在我的 C++ 中定义了两种类型的函数指针,如下所示:

typedef void(*CallbackFn)(bool, std::string, py::array_t<uint8_t>&);
typedef std::function<void(std::string)> LogFunction;
Class Core{
...
void myfunc1(LogFunction lg1, CallbackFn callback, int x, std::string y);
};

并且我希望能够在 C 中公开它们,但我似乎找不到这样做的方法。我的第一个尝试是将它们转换为 void* 然后将它们重新转换回它们的实际类型。但这似乎是个坏主意。所以我对如何进行这种转换一无所知。
此外,我需要提出的解决方案至少应该可以使用 C++11 来实现。

更新:

非常感谢您的回答。但是我需要添加更多解释作为我所追求的。我知道 extern "C",事实上 C++ 函数已经在我的 DLL 中公开了。然而,我遇到的问题是在 C 和 C++ 之间来回传递 函数指针
一种方法是以 C 可以直接使用的方式定义函数指针。这是我需要更改的,例如:

typedef void(*CallbackFn)(bool, std::string, py::array_t<uint8_t>&);
typedef std::function<void(std::string)> LogFunction;

到它的 C 兼容版本:

typedef void(*CCallbackFn)(bool, char*, int, unsigned char, int length);
typedef void(*CLogFunction)(char* string, int length);

并改用这些。然而,这样做的缺点是,DLL 也被 C++ 客户端使用,这将阻碍将 C++ 的所有内容更改为与 C 兼容,这样做会失去 C++ 的优势。
相反,我想到了第二种方法。 C++保持不变,但对于C链接并通过C与其他语言交互API,我自己进行转换。
那就是他们使用 C 风格,然后我在实现部分将其转换回 C++。为了进一步简化这一点,所以我也在 C++ 部分设计了一些默认值。意思是,假设由于缺乏更好的例子,实例需要一个回调函数来记录发生的任何事情。我定义了一个回调函数,以防用户没有给出它,并为 C API 创建了两个函数,具体来说与此大致相似:

//in core.cpp for example
include "Core.h"
...

extern "C"
{
 Core * core;
 ...

 Core_API void* get_default_log_callback()
 {
   return (void*) core->SomeDefaultCallback();  
 } 

 Core_API void* set_log_callback(void* fn)
 {
    // convert/cast that to the c++ callback type
    // CallbackFn, 
     core->SetCallback(fn_converted);  
 }

客户端可以使用 get_default_log_callback 并将其 return 用于 set_log_call_back。 基本上这里的想法是能够使用 C++ 已经定义的资产。 我被困在这个转换过程中,如何将此类回调指针转换为 C 兼容类型(就像我展示的那样,例如将指针转换为 void* 并编写一个接受 void* 的 C 包装器真的很容易然后将其重铸为正确的类型。

我也想知道这种情况,以及这是一种好的做法还是一种不好的做法。

问题二:

另外我想知道是否可以从 CCallbackFnCallbackFn 进行转换?
假设我有一个 CCalbackFn 形式的函数(例如上面的 C 函数),但我最终想以 CallbackFn 形式拥有它(更改它并调用接受 CallbackFn)?这可能吗 ?

您可以通过声明函数来向 C 公开函数 extern "C"

但是,该函数必须只接受在 C 语言中有效的参数类型。

从上面的代码来看,您将不得不用更类似于 C 的术语来表达您的回调。

C 不处理/不能处理 C++ 名称修改(也不能处理与 C 类型不同的 C++ 类型)。您不能在任何暴露给 C 的东西中使用非 POD 类型(以及涉及在 C 中不可用的类型的普通函数指针)。并且您需要对暴露的东西使用 extern "C" 来禁用名称重整(或者更确切地说,使用任何命名 convention/mangling 您当前的 C 编译器使用的平台)。

简而言之:将 extern "C" 用于必须可从 C 调用的任何内容 确保以这种方式公开的任何内容仅使用您可以 write/use 的类型在 C.

为了向 C 公开任何 C++ 函数,您应该将 C++ 调用包装在普通 C++ 库中的 C 函数中。并仅从中导出 C 函数。对库内外的 C 函数声明使用通用的 header。这些函数可以从任何 C 环境调用。所有 C++ 类型都包装在 class 中,并通过函数包装器传递指向该 class 的指针,作为 C++ 环境的句柄。指向 class 的指针应该是 void* 或者只是 long。并且只有在 C++ 端,您才会将其重新解释为自己的环境 class.
更新 1:

  1. 您应该将 C 和 C++ 分开。这意味着不要在 C 和 C++ 之间进行转换。将 XX_log_callback 函数的 C 版本和 C++ 版本分开。例如,您的 C++ 函数使用 std::string、py::array_t&。你无法使用它是 C。没有可用的转换,也没有办法在 C 中利用它。你只能在 C++ 中利用 C++,所以为 C++ 制作一个单独的版本,一个供 C 开发人员使用.

  2. 顺便说一句。有一种将 C++ 接口传递给 C 并返回给 C++ 的技术。但请注意,它仅使用与 C 兼容的 return 和参数类型。这意味着创建一个结构,该结构具有指向 table 函数指针的指针。在 C++ 中它是一个接口,但在 C 中它是一个结构。 Windows中的COM/OLE2使用了这个技巧。 https://www.codeproject.com/Articles/13601/COM-in-plain-C 要使用这种技术,您应该非常了解如何使 C++ class 与 C 结构兼容。

现在我将 copy/paste codeproject 中的一些代码片段,几乎没有解释。 在 C 和 C++ 之间传递接口时的经验法则,仅使用与 C 兼容的类型作为函数参数和 return 类型。接口中的前四个字节是指向函数数组的指针,称为 Virtual Table:

typedef struct
{
   IExampleVtbl * lpVtbl;//<-- here is the pointer to virtual table
   DWORD          count;//<-- here the current class data starts
   char           buffer[80];
} IExample;

在此处添加指向虚拟 table 中的函数的指针。 IExampleVtbl是一个用指针填充的结构体,二进制它相当于一个连续的指针数组

static const IExampleVtbl IExample_Vtbl = {SetString, GetString};
IExample * example;

// Allocate the class
example = (IExample *)malloc(sizeof(IExample));

example->lpVtbl = &IExample_Vtbl;//<-- here you pass the pointer to virtual functions
example->count = 1; //<-- initialize class members
example->buffer[0] = 0;

现在调用方法是这样的:

char buffer[80];
example->lpVtbl->SetString(example, "Some text");
example->lpVtbl->GetString(example, buffer, sizeof(buffer));

记住,以上都是C。 在上面的示例中,您显式引用了虚拟 table 成员,并且还显式地将其作为函数中的第一个参数传递。调用 GetString/SetString 的 C++ 等价物是:

example->SetString("Some text");
example->GetString(buffer, sizeof(buffer));

这是 SetString/GetStrinf 函数和虚拟 table 结构:

HRESULT STDMETHODCALLTYPE SetString(IExample *this, char * str)
{
   memcpy(this->buffer, str, length);//be attentive, it is almost pseudocode
   return(0);
}
HRESULT STDMETHODCALLTYPE GetString(IExample *this, char *buffer, int buffer_len)
{
   memcpy(str, this->buffer, length);//be attentive, it is almost pseudocode
   return(0);
}
typedef struct {
   SetStringPtr       *SetString;
   GetStringPtr       *GetString;
} IExampleVtbl;

STDMETHODCALLTYPE是为了兼容C++调用成员函数classes,这样你就可以在C和C++之间传递IExample。我相信这对C程序员来说真的是一场噩梦,但对C++同行来说却不是一件容易的事。
要在从 C 传递接口时访问它,您可以这样声明接口:

class IExample
{
public:
   virtual HRESULT  SetString(char * str) = 0;//<-- see first parameter gone away in both functions
   virtual HRESULT GetString(char *buffer, int buffer_len) = 0;
};

如果您在 C++ 中实现以传入与上述代码等效的 C,则为:

class IExample
{
    int count = 1; //<-- initialize class members
    char buffer[80] = "";
public:
   virtual HRESULT  SetString(char * str)
   {
      memcpy(this->buffer, str, length);//be attentive, it is almost pseudocode
      return(0);
   }
   virtual HRESULT GetString(char *buffer, int buffer_len)
   {
      memcpy(str, this->buffer, length);//be attentive, it is almost pseudocode
      return(0);
   }
};

还有一件事。您不使用 C++ 和 vice-versa 中的 C 声明。这是通过 COM 方法来解决问题的。它可能不适用于不同的编译器,但请记住,旧的 CORBA 中采用了类似的方法。只有你应该记住。您为 C 创建一个接口,为 C++ 创建一个接口。在 C++ 部分隐藏 C 接口,在 C 部分隐藏 C++ 接口。只传递指针。

我最终想出了自己的解决方案,我自己称之为 "Delegating Callbacks" 方法!这里的想法是,您创建一个转换,而不是直接使用 C 回调,而是创建一个充当两个 API 之间的翻译器的中间回调。 例如,假设我的 C++ class 有一个方法只接受具有此签名的回调:

typedef void(*CallbackFn)(bool, std::string, py::array_t<uint8_t>&);

现在我们想将其公开给 C。这是我们的 C 回调签名:

typedef void(*CCallbackFn)(bool, const char*, unsigned char*, int rows, int cols);

现在我们如何从第一个转到第二个,反之亦然?我们在 C++ class 中创建一个类型为 CallbackFn 的新回调,并在其中执行 C 回调。因此,使用间接调用,我们可以轻松地解耦 C 和 C++ API 之间的签名,并使用最适合它们的签名。

为了使它更具体,我们需要这样的东西:

CORE_API void Core::DelegateCCallback(bool status, std::string id, py::array_t<uint8_t>& img)
{
    //here is used a std::map to store my c-callbacks you can use
    //vector or anything else that you like
    for (auto item: this->callbackMap_c)
    {
        //item.first is our callback, so use it like a function 
        item.first(status, id.c_str(), img.mutable_data(), img.shape(0), img.shape(1));
    }
}

然后您像这样更新您的 C 回调列表,使用两个公开的函数 Add 和 Remove 分别添加和删除任何回调:

extern "C"
{
//Core is our C++ class for example
Core* core = nullptr;
...
    CORE_API void AddCallback(CCallbackFn callback)
    {
        core->AddCallback_C(callback);
    }

    CORE_API void RemoveCallback(CCallbackFn callback)
    {
        core->RemoveCallback_C(callback);
    }
}

回到我们的 C++ class,AddCallback_C 方法定义如下:

CORE_API void Core::AddCallback_C(CCallbackFn callback)
{
    auto x = this->callbackMap_c.emplace(callback, typeid(callback).name());
}

CORE_API void Core::RemoveCallback_C(CCallbackFn callback)
{
    this->callbackMap_c.erase(callback);
}

只需adding/removing回调到回调列表。就这样。 现在当我们实例化我们的 C++ 代码时,我们需要将 DelegateCCallback 添加到回调列表中,所以当所有 C++ 回调都执行时,这个回调也会执行,它将遍历所有 C 回调并执行它们一个一个。

例如,在我的例子中,回调需要在 Python 模块中 运行,所以在我的构造函数中我必须做这样的事情:


CORE_API Core::Core(LogFunction logInfo)
{
    //....
    // add our 'Callback delegate' to the list of callbacks
    // that would run.  
    callbackPyList.attr("append")(py::cpp_function([this](bool status, std::string id, py::array_t<uint8_t>& img)
                                                         {
                                                            this->DelegateCCallback(status, id, img);
                                                         }));

//...
}

你可以喜欢这个并根据需要合并线程等。