web-dev-qa-db-ja.com

C ++関数ポインターをCで公開するにはどうすればよいですか?

私のC++には、次のような2種類の関数ポインターが定義されています。

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++の間で関数ポインタをやり取りすることが問題でした。
1つの方法は、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++。
代わりに私は第2の方法を考え出しました。 C++は同じままですが、CリンケージおよびC APIを介して他の言語と対話するために、自分で変換を行います。
つまり、Cスタイルを使用しているため、実装部分でこれをC++に変換します。これをさらに単純化するために、C++パーツにもデフォルトをいくつか設計しました。つまり、より良い例がないため、インスタンスは何が起こってもログに記録するためのコールバック関数を必要とします。ユーザーから提供されなかった場合に備えて、コールバック関数を定義し、C API用の2つの関数を作成します。

//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ラッパーを書くのは本当に簡単です)で行き詰まりましたその後、適切なタイプに再キャストします。

このシナリオについても知りたいのですが、これが良い方法なのか、それとも悪い方法なのかを教えてください。

質問2:

また、たとえばCCallbackFnおよびCallbackFnからの変換が可能かどうかを知りたいです。
CCalbackFn形式の関数(上記のC関数など)があるとしたら、最終的にはCallbackFn形式にしたい(変更して、 CallbackFn)を受け入れる基礎となるC++?これは可能ですか?

4
Rika

私は最終的に自分自身で "Delegating Callbacks"アプローチと呼んでいる独自のソリューションを思いつきました。ここでの考え方は、Cコールバックを直接使用する代わりに、宛先変更を作成し、2つの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);

では、最初の方法から2番目の方法、またはその逆にどうやって行くのでしょうか?タイプCallbackFnのC++クラスに新しいコールバックを作成し、その中でCコールバックを実行します。したがって、間接呼び出しを使用すると、C APIと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));
    }
}

そして、2つの公開された関数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);
                                                         }));

//...
}

あなたはこれで空想を得て、好きなようにスレッドなどを組み込むことができます。

0
Rika

C++関数をCに公開するには、C++呼び出しをC関数でプレーンなC++ライブラリにラップする必要があります。そして、C関数のみをエクスポートします。ライブラリ内外のC関数宣言に共通のヘッダーを使用します。これらの関数は、任意のC環境から呼び出すことができます。すべてのC++型はクラスにラップされ、C++環境へのハンドルとして、関数ラッパー間でそのクラスへのポインターを渡します。クラスへのポインタは、void *または単なる長さでなければなりません。そして、C++側でのみ、それを独自の環境クラスに再解釈します。
更新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互換の戻り値と引数の型のみを使用します。これは、関数ポインタのテーブルへのポインタを持つ構造を作成することを意味します。 C++ではインターフェイスですが、Cでは構造体です。この手法は、WindowsのCOM/OLE2で使用されます。 https://www.codeproject.com/Articles/13601/COM-in-plain-C このようなテクニックを使用するには、C++クラスをC構造体と互換性のあるものにする方法をよく理解する必要があります。

ここで、コードプロジェクトからいくつかのコードを少し説明を付けてコピー/貼り付けます。 CとC++の間でインターフェースを渡すときの経験則では、関数の引数および戻りの型として、Cと互換性のある型のみを使用します。インターフェイスの最初の4バイトは、仮想テーブルと呼ばれる関数の配列へのポインターです。

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/GetStrinf関数と仮想テーブル構造は次のとおりです。

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用に1つ、C++用に1つのインターフェースを作成します。 C++部分ではCインターフェースを非表示にし、CではC++インターフェースを非表示にします。ポインタのみを渡します。

0
armagedescu