如何在 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 包装器真的很容易然后将其重铸为正确的类型。
我也想知道这种情况,以及这是一种好的做法还是一种不好的做法。
问题二:
另外我想知道是否可以从 CCallbackFn
和 CallbackFn
进行转换?
假设我有一个 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:
您应该将 C 和 C++ 分开。这意味着不要在 C 和 C++ 之间进行转换。将 XX_log_callback 函数的 C 版本和 C++ 版本分开。例如,您的 C++ 函数使用 std::string、py::array_t&。你无法使用它是 C。没有可用的转换,也没有办法在 C 中利用它。你只能在 C++ 中利用 C++,所以为 C++ 制作一个单独的版本,一个供 C 开发人员使用.
顺便说一句。有一种将 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);
}));
//...
}
你可以喜欢这个并根据需要合并线程等。
我在我的 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 包装器真的很容易然后将其重铸为正确的类型。
我也想知道这种情况,以及这是一种好的做法还是一种不好的做法。
问题二:
另外我想知道是否可以从 CCallbackFn
和 CallbackFn
进行转换?
假设我有一个 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:
您应该将 C 和 C++ 分开。这意味着不要在 C 和 C++ 之间进行转换。将 XX_log_callback 函数的 C 版本和 C++ 版本分开。例如,您的 C++ 函数使用 std::string、py::array_t&。你无法使用它是 C。没有可用的转换,也没有办法在 C 中利用它。你只能在 C++ 中利用 C++,所以为 C++ 制作一个单独的版本,一个供 C 开发人员使用.
顺便说一句。有一种将 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);
}));
//...
}
你可以喜欢这个并根据需要合并线程等。