More than 3 years have passed since last update.

VST3ホストを作ろう(10) 〜インターフェースとコンポーネントのクラス定義〜

Last updated at Posted at 2020-05-11



 今回は、 VST-MA においてインターフェースとコンポーネントがソースコード上でどのように定義されているかについて解説します。
 この定義に触れておくことで、次回の VST-MA を利用したプログラムを書くための作法についての解説記事や、今後の VST3 SDK の解説を読む際に、インターフェースとコンポーネントをより具体的な対象として見ることができるようになります。


FUnknown のクラス定義

 前回解説したように、 VST-MA のインターフェースは C++ の抽象基底クラスとして定義されます。そしてインターフェースは他のインターフェースを継承することもでき、その最も基底に存在するのが FUnknown インターフェースです。

 FUnknown は、ソースコード上では、以下のような抽象基底クラスとして定義されています。

FUnknown の定義 (funknown.h)
// FUnknown
/** The basic interface of all interfaces.
\ingroup pluginBase
- The FUnknown::queryInterface method is used to retrieve pointers to other
  interfaces of the object.
- FUnknown::addRef and FUnknown::release manage the lifetime of the object.
  If no more references exist, the object is destroyed in memory.
Interfaces are identified by 16 byte Globally Unique Identifiers.
The SDK provides a class called FUID for this purpose.
\ref howtoClass */
class FUnknown

    /** Query for a pointer to the specified interface.
    Returns kResultOk on success or kNoInterface if the object does not implement the interface.
    The object has to call addRef when returning an interface.
    \param _iid : (in) 16 Byte interface identifier (-> FUID)
    \param obj : (out) On return, *obj points to the requested interface */
    virtual tresult PLUGIN_API queryInterface (const TUID _iid, void** obj) = 0;

    /** Adds a reference and return the new reference count.
    \par Remarks:
        The initial reference count after creating an object is 1. */
    virtual uint32 PLUGIN_API addRef () = 0;

    /** Releases a reference and return the new reference count.
    If the reference count reaches zero, the object will be destroyed in memory. */
    virtual uint32 PLUGIN_API release () = 0;

    static const FUID iid;

DECLARE_CLASS_IID (FUnknown, 0x00000000, 0x00000000, 0xC0000000, 0x00000046)


 FUnknown のメンバ関数として定義されている queryInterface() 関数は、あるインターフェースを別のインターフェースにキャストするような処理を行う関数です。また、 addRef() 関数と release() 関数は、コンポーネントの寿命を参照カウントで管理するための関数です。これらの関数の使い方は、次回詳しく解説します。

 クラス定義の一番下にある iid static メンバ変数は、このインターフェースの IID を表していて、その値はクラス定義のすぐ下の DECLARE_CLASS_IID() マクロでセットされています。


 次に、FUnknown を継承したインターフェースの例として、バイナリデータの読み書きを行う機能を提供する IBStream インターフェースの定義を見ていきます。

 IBStream は以下のように定義されています。

IBStream の定義 (ibstream.h)
/** Base class for streams.
\ingroup pluginBase
- read/write binary data from/to stream
- get/set stream read-write position (read and write position is the same)

class IBStream: public FUnknown
    enum IStreamSeekMode
        kIBSeekSet = 0, ///< set absolute seek position
        kIBSeekCur,     ///< set seek position relative to current position
        kIBSeekEnd      ///< set seek position relative to stream end

    /** Reads binary data from stream.
    \param buffer : destination buffer
    \param numBytes : amount of bytes to be read
    \param numBytesRead : result - how many bytes have been read from stream (set to 0 if this is of no interest) */
    virtual tresult PLUGIN_API read (void* buffer, int32 numBytes, int32* numBytesRead = 0) = 0;

    /** Writes binary data to stream.
    \param buffer : source buffer
    \param numBytes : amount of bytes to write
    \param numBytesWritten : result - how many bytes have been written to stream (set to 0 if this is of no interest) */
    virtual tresult PLUGIN_API write (void* buffer, int32 numBytes, int32* numBytesWritten = 0) = 0;

    /** Sets stream read-write position. 
    \param pos : new stream position (dependent on mode)
    \param mode : value of enum IStreamSeekMode
    \param result : new seek position (set to 0 if this is of no interest) */
    virtual tresult PLUGIN_API seek (int64 pos, int32 mode, int64* result = 0) = 0;

    /** Gets current stream read-write position. 
    \param pos : is assigned the current position if function succeeds */
    virtual tresult PLUGIN_API tell (int64* pos) = 0;
    static const FUID iid;

DECLARE_CLASS_IID (IBStream, 0xC3BF6EA2, 0x30994752, 0x9B6BF990, 0x1EE33E9B)


 このように、FUnknown 以外のインターフェースは、全て FUnknown を継承します。1 そしてインターフェースが提供する機能を、純粋仮想関数の形で追加します。また FUnknown 同様に、自身の IID を iid static メンバ変数として定義しています。


 IBStream はインターフェースとしてバイナリデータの読み書き機能を持っていると定義しているだけで、これだけでは実際の処理が実装されていないために動作しません。

 次は、インターフェースを継承し、インターフェースが定義している機能を実装したコンポーネントの例として、MemoryStream コンポーネントを見ていきます。 MemoryStream はメモリ上のバッファにバイナリデータを読み書きする機能を実装しています。

 MemoryStream は以下のように定義されています。

MemoryStream のクラス定義 (memorystream.h)
/** Memory based Stream for IBStream implementation (using malloc).
\ingroup sdkBase
class MemoryStream : public IBStream
    MemoryStream ();
    MemoryStream (void* memory, TSize memorySize);  ///< reuse a given memory without getting ownership
    virtual ~MemoryStream ();

    tresult PLUGIN_API read  (void* buffer, int32 numBytes, int32* numBytesRead) SMTG_OVERRIDE;
    tresult PLUGIN_API write (void* buffer, int32 numBytes, int32* numBytesWritten) SMTG_OVERRIDE;
    tresult PLUGIN_API seek  (int64 pos, int32 mode, int64* result) SMTG_OVERRIDE;
    tresult PLUGIN_API tell  (int64* pos) SMTG_OVERRIDE;

    TSize getSize ();       ///< returns the current memory size
    void setSize (TSize size);  ///< set the memory size, a realloc will occur if memory already used
    char* getData ();       ///< returns the memory pointer
    char* detachData ();    ///< returns the memory pointer and give up ownership
    bool truncate ();       ///< realloc to the current use memory size if needed
    bool truncateToCursor ();   ///< truncate memory at current cursor position

    char* memory;               // memory block
    TSize memorySize;           // size of the memory block
    TSize size;                 // size of the stream
    int64 cursor;               // stream pointer
    bool ownMemory;             // stream has allocated memory itself
    bool allocationError;       // stream invalid


 MemoryStream のメンバ関数定義(一部を抜粋) (memorystream.cpp)
IMPLEMENT_FUNKNOWN_METHODS (MemoryStream, IBStream, IBStream::iid)
static const TSize kMemGrowAmount = 4096;

MemoryStream::MemoryStream (void* data, TSize length)
: memory ((char*)data)
, memorySize (length)
, size (length)
, cursor (0)
, ownMemory (false)
, allocationError (false)

MemoryStream::MemoryStream ()
: memory (nullptr)
, memorySize (0)
, size (0)
, cursor (0)
, ownMemory (true)
, allocationError (false)

MemoryStream::~MemoryStream () 
    if (ownMemory && memory)
        ::free (memory);


tresult PLUGIN_API MemoryStream::read (void* data, int32 numBytes, int32* numBytesRead)
    if (memory == nullptr)
        if (allocationError)
            return kOutOfMemory;
        numBytes = 0;
        // Does read exceed size ?
        if (cursor + numBytes > size)
            int32 maxBytes = int32 (size - cursor);

            // Has length become zero or negative ?
            if (maxBytes <= 0) 
                cursor = size;
                numBytes = 0;
                numBytes = maxBytes;

        if (numBytes)
            memcpy (data, &memory[cursor], numBytes);
            cursor += numBytes;

    if (numBytesRead)
        *numBytesRead = numBytes;

    return kResultTrue;


 MemoryStreamIBStream を継承し、 IBStream に定義された read() 関数や write() 関数などをオーバーライドしています。2
 FUnknown で定義されていた queryInterface()addRef()release() 関数は、ヘッダーファイル内の DECLARE_FUNKNOWN_METHODS マクロと、ソースファイル内の IMPLEMENT_FUNKNOWN_METHODS マクロによってオーバーライドしています。

 コンポーネントのコンストラクタ/デストラクタそれぞれ使用されている FUNKNOWN_CTORFUNKNOWN_DTOR マクロは、コンポーネントの寿命を参照カウントで管理できるようにするためのものです。参照カウントによる寿命の管理は、次回詳しく解説します。3


 VST-MA には他にもいろいろなインターフェースとコンポーネントが実装されています。中には、複数のインターフェースを継承しているもう少し複雑な構造のコンポーネントも存在しますが、基本の仕組みはここで紹介したものと変わりありません。4


 VST-MA の仕様に従って作られたインターフェースは、バイナリ境界を超えて利用できます。

 通常の C++ のプログラムでは、 DLL とそれをロードするホストアプリケーションから同じヘッダーファイルを参照して、そのヘッダーの中に含まれるクラスを利用しようとしても、お互いが想定しているオブジェクトのメモリ上でのレイアウトが異なっているために、アクセス違反や予期せぬエラーが発生する可能性があります。

 例えば、以下のようにホストアプリケーションと DLL の両方から参照されるヘッダーファイル MyAwesomeClass.h を用意して、その中に含まれるクラス定義を使用するケースを考えます。

// ホストアプリケーションと DLL の両方から参照されるヘッダーファイル
// (MyAwesomeClass.h)
#include <vector>
#include <string>
#include <iostream>

class MyAwesomeClass
  std::string name_;

  void setName(std::string const &name) { name_ = name; }

  void doSomething() {
    std::cout << "My Name is : " << name_ << std::endl;

// DLL のソースコード
// (dll_main.cpp)

#include <MyAwesomeClass.h>

extern "C" {
  __declspec(dllexport) MyAwesomeClass * CreateAwesomeClass()
    return new MyAwesomeClass();

// ホストアプリケーションのソースコード
// (main.cpp)

#include <MyAwesomeClass.h>

int main()
  using FunctionType = MyAwesomeClass * (*)();
  HMODULE module = LoadModule("<DLLのパス>");
  FunctionType create_func =
    static_cast<FunctionType>(GetProcAddress(module, "CreateAwesomeClass"));

  MyAwesomeClass *c = create_func();

  c->doSomething(); // 危険!
                    // このポインタの指すオブジェクトは、アプリケーション側が想定しているのとは
                    // 違うレイアウトでメモリ上に配置されているかもしれない。

 このとき、ホストアプリケーションと DLL が同じ種類のコンパイラの同じバージョン、さらに同じビルド設定でビルドされていなければ、それぞれで想定する MyAwesomeClass のメモリ上のレイアウトが異なって、アクセス違反や予期せぬエラーが発生する可能性があります。

 VST-MA や COM のインターフェースは、純粋仮想関数のみを持つ、デストラクタを定義しない、引数には POD (Plain Old Data) 型のみを受け取る、例外を使わない、演算子オーバーロードを行わないなどのいくつかのルールを満たすような抽象基底クラスとして定義されます。
 このようにして定義されたクラスは、コンパイラの種類やビルド設定によらず、同じ構造の仮想関数テーブル5を持つ性質があるため、このインターフェースをバイナリ境界を超えて受け渡しても、正しく仮想関数を呼び出せます。6 7



 今回は VST-MA のインターフェースとコンポーネントがどのように定義されているかと、インターフェースがバイナリ境界を超えられる仕組みについて解説しました。次回はインターフェースとコンポーネントをどのように利用するかという VST-MA の作法を見ていきます。

  1. 前回の記事で触れた IEditController インターフェースのように、他のインターフェースを介して間接的に FUnknown を継承しているケースもあります。 

  2. ここで使われている SMTG_OVERRIDE というマクロは、コンパイル時の C++ のバージョンに合わせて override 指定子に置き換わります。 

  3. これらのマクロは、実際には大した処理を行っていません。 FUNKNOWN_CTOR は参照カウントの値を初期値として 1 にセットするだけであり、 FUNKNOWN_DTOR は何も処理を行ないません。 

  4. コンポーネントが複数のインターフェースを継承している場合は、 queryInterface() 関数を単純に実装すると、 FUnknown インターフェースを取得するための処理で型変換の解釈が曖昧になってコンパイルエラーが発生することがあります (擬似コード)。 VST-MA はこの問題に対して FObject というヘルパークラスを用意しています。詳細は細かくなるため割愛しますが、複数のインターフェースを継承しているコンポーネントは FUnknown インターフェースのポインタを取得する処理を FObject クラスに任せることで、この問題を回避できるようになっています。 

  5. C++ のクラスが仮想関数を呼び出せるようにするために、コンパイラが用意する関数ポインタのテーブルで、vtable とも呼ばれます。仮想関数テーブルの仕組みはソースコードからは隠蔽されていて、プログラマがこれを直接操作することはできません。 

  6. C++の規格では、仮想関数を呼び出す仕組みをどのように実装するかは定義されていません。そのため、コンパイラの実装によっては、 COM が期待する仮想関数テーブルの構造(つまり MSVC コンパイラが生成する仮想関数テーブルの構造)と異なる構造や仕組みによって仮想関数の呼び出しをサポートしている可能性があり、その場合はインターフェースを受け渡しても正しくそれを利用できないことになります。ともあれ、現在の主要なC++コンパイラは、 VST-MA や COM が定める形式によって定義されたインターフェースに対して同じ構造の仮想関数テーブルを生成するので、問題なくインターフェースを受け渡して利用できます。 

  7. バイナリ境界をまたいで安全に関数を呼び出すためには、さらに呼び出し規約というものを合わせておく必要があります。筆者はこれについてあまり詳しくないので、詳細な解説はできませんが、 VST-MA の中では関数宣言や関数定義に含まれる PLUGIN_API というマクロが呼び出し規約を指定する役割を持っています。(ただし Windows 環境でのみ有効な値がセットされて、それ以外の環境では単に無視されます) 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up