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

5

我在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",实际上我的DLL已经使用它公开了C++函数。然而,我遇到的问题是在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 API进行交互的C链接,我自己进行转换。
这表示它们使用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 函数并使用其返回结果来 set_log_call_back。基本思想是能够使用已定义的 C++ 资产。

我卡在了这个转换过程中,如何将这样的回调指针转换为 C 兼容类型(就像我展示的那样,将指针轻松地强制转换为 void* ,然后编写一个接受 void* 的 C 封装器,然后重新转换为适当的类型)。

我也想了解这种情况,无论是好的实践还是不好的实践。

第二个问题:

此外,我想知道是否有可能从 CCallbackFn 和 CallbackFn 进行转换?
假设我有一个以 CCalbackFn 形式表示的函数(例如上面的我的 C 函数),但我最终想要将其形式改为 CallbackFn(更改并调用支持 CallbackFn 的底层 C++)?这有可能吗?


1
当将某些东西公开为 C API 时,我会失去 C++ 的优势 - 嗯,是的。当将某些东西公开为 C API 时,当然会失去 C++ 的优势。为什么会有其他期望呢?您仍然可以在函数内部使用 C++,但在任何公开的内容中都受到 C 的限制。为什么这很神秘或令人惊讶呢? - Jesper Juhl
我指的是从底层开始更改基础的C++签名以与C兼容,而不仅仅是暴露的C函数。 - Hossein
@Rika 你应该将C和C++分开。这意味着不要进行这些转换。保留XX_log_callback函数的C版本和C++版本。例如,你的C++函数使用std::string、py::array_t<uint8_t>&。在C中无法使用它,没有可用的转换方式,也没有利用的方法。你只能在C++中利用C++,所以要为C++单独创建一个版本。 - armagedescu
1
顺便说一下,有一种将C++接口传递给C并返回到C++的技术。但要注意,它仅使用与C兼容的返回和参数类型。这意味着创建一个带有指向函数指针表的指针的结构体。在C++中,它是一个接口,但在C中,它是一个结构体。这种技术在Windows的COM/OLE2中使用。 https://www.codeproject.com/Articles/13601/COM-in-plain-C 要使用这种技术,您应该非常了解如何使C++类与C结构体兼容。 - armagedescu
1
@armagedescu 非常感谢,真的很感激。我看到了,我想你在这里的评论更好地回答了我的问题。如果您也可以将这些作为答案发布,我会非常感激。 - Hossein
4个回答

7

C语言不能处理C++的名称重载(name mangling),也不能处理与C类型不完全相同的C++类型。在任何暴露给C的内容中,您不能使用非POD类型(以及涉及不可用于C的类型的纯函数指针)。并且您需要在暴露的内容中使用extern "C",以禁用名称重载(或者更确切地说,使用当前平台C编译器使用的任何命名约定/命名规则)。

简而言之:对于必须从C中调用的任何内容,请使用extern "C",并确保以这种方式公开的任何内容仅使用您可以编写/使用的C类型。


1
回复:“禁用名称修饰” - 实际上是鼓励C风格的名称修饰。大多数C编译器在函数名前加下划线,尽管我曾经使用过一个在函数名后添加下划线的编译器。无论如何,“extern C”告诉编译器使用一些针对相同系统的C编译器所使用的调用约定。尽管如此,+1。 - Pete Becker
@PeteBecker 好观点。回答已更新。感谢您的支持。 - Jesper Juhl
@JesperJuhl 非常感谢,我更新了答案并更详细地解释了我的意思。 - Hossein
@JesperJuhl 哦,我的错:)) 是的,问题,谢谢:) - Hossein

4

您可以通过声明extern "C"将函数暴露给C。

但是,该函数只能接受在C中有效的参数类型。

从上面代码的外观来看,您需要用更接近C的术语表达回调函数。


2
例如,不要传递 std::string,而是传递 const char*;不要传递 py::array 引用,而是传递 unsigned char 指针和长度;等等。 - Peter - Reinstate Monica
@Richard Hodges 非常感谢,我已经更新了答案并更详细地解释了我的意思。 - Hossein
@Peter-ReinstateMonica 也谢谢您的评论。非常感激。 - Hossein

0
为了将任何C++函数暴露给C,您应该在一个纯C++库中用C函数包装C++调用,并仅从其中导出C函数。在库内外使用通用头文件声明C函数。这些函数将可以从任何C环境中调用。将所有的C++类型封装在一个类中,并通过函数包装器传递指向该类的指针,作为对C++环境的句柄。类指针应该是void*或者只是long。只有在C++端,您才会重新解释它以拥有自己的环境类。
更新1:
  1. 你应该将C和C++分开。这意味着不要在C和C++之间进行转换。保持XX_log_callback函数的C版本和C++版本分离。例如,你的C++函数使用std::string、py::array_t&。在C中无法使用它,没有可用的转换方式,也无法利用它。你只能在C++中利用C++,因此为C++制作一个单独的版本,并为C开发人员提供一个版本。

  2. 顺便说一下,有一种技术可以将C++接口传递给C,然后再传回C++。但是要注意,它只使用与C兼容的返回值和参数类型。这意味着创建一个带有指向函数指针表的指针的结构体。在C++中,它是一个接口,但在C中,它是一个结构体。这种技术在Windows的COM/OLE2中使用。https://www.codeproject.com/Articles/13601/COM-in-plain-C 要使用这种技术,你应该非常了解如何使C++类与C结构体兼容。

现在我只需要从CodeProject上复制/粘贴一些带有少量解释的代码片段。当在C和C++之间传递接口时,基本原则是仅使用与C兼容的类型作为函数参数和返回类型。接口中的前四个字节是指向函数数组的指针,称为虚拟表:

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

在这里,您将在虚拟表中添加函数指针。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 语言。 在上面的示例中,您明确引用了虚拟表成员,并且将其作为函数的第一个参数显式传递。调用 GetString/SetString 的 C++ 等效代码如下:
example->SetString("Some text");
example->GetString(buffer, sizeof(buffer));

这里是SetString/GetString函数和虚拟表结构:

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++调用成员函数类,因此您将能够在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++中不使用C声明,反之亦然。这是通过COM方法解决问题的。它可能无法在不同的编译器中移植,但请记住,在旧的CORBA中也采用了类似的方法。只需记住,您需要为C和C++创建一个接口。在C++部分隐藏C接口,在C上隐藏C++接口。只传递指针。


2
虽然这是正确的,但你必须在声明中加上extern "C",这样编译器才会生成使用C编译器命名和调用约定的代码,而不是通常使用的约定。 - Pete Becker
@PeteBecker 这是正确的。我只是不想重复其他答案中已经指定的内容。 - armagedescu
1
非常感谢,我已经更新了答案并更详细地解释了我的意思。 - Hossein

0

最终,我自己想出了一种解决方案,我自己称之为“委托回调”方法!这里的想法是,不直接使用C回调,而是创建一个转移,创建一个中间回调,作为两个API之间的翻译。

例如,假设我的C++类有一个仅接受此签名回调的方法:

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++ 类中创建一个类型为 CallbackFn 的新回调函数,在其中执行 C 回调。因此,使用间接调用,我们可以轻松地解耦 C 和 C++ API 之间的签名,并使用最适合每个 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));
    }
}

然后,您可以使用两个公开的函数Add和Remove来添加和删除任何回调函数,以此方式更新C回调列表:

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++类中,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);
}

只需将回调添加/删除到回调列表中即可。就这样。 现在,当我们实例化我们的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);
                                                         }));

//...
}

你可以根据自己的需求,使用线程等高级技术来实现更加复杂的功能。


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