はじめに
今回は、 VST-MA においてインターフェースとコンポーネントがソースコード上でどのように定義されているかについて解説します。
この定義に触れておくことで、次回の VST-MA を利用したプログラムを書くための作法についての解説記事や、今後の VST3 SDK の解説を読む際に、インターフェースとコンポーネントをより具体的な対象として見ることができるようになります。
また、この記事の後半では、インターフェースがバイナリ境界を超えられる仕組みについて解説します。
FUnknown のクラス定義
前回解説したように、 VST-MA のインターフェースは C++ の抽象基底クラスとして定義されます。そしてインターフェースは他のインターフェースを継承することもでき、その最も基底に存在するのが FUnknown
インターフェースです。
FUnknown
は、ソースコード上では、以下のような抽象基底クラスとして定義されています。
//------------------------------------------------------------------------
// 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
{
public:
//------------------------------------------------------------------------
/** 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
は以下のように定義されています。
/** 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
{
public:
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
は以下のように定義されています。
//------------------------------------------------------------------------
/** Memory based Stream for IBStream implementation (using malloc).
\ingroup sdkBase
*/
//------------------------------------------------------------------------
class MemoryStream : public IBStream
{
public:
//------------------------------------------------------------------------
MemoryStream ();
MemoryStream (void* memory, TSize memorySize); ///< reuse a given memory without getting ownership
virtual ~MemoryStream ();
//---IBStream---------------------------------------
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
//------------------------------------------------------------------------
DECLARE_FUNKNOWN_METHODS
protected:
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
};
//-----------------------------------------------------------------------------
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)
{
FUNKNOWN_CTOR
}
//-----------------------------------------------------------------------------
MemoryStream::MemoryStream ()
: memory (nullptr)
, memorySize (0)
, size (0)
, cursor (0)
, ownMemory (true)
, allocationError (false)
{
FUNKNOWN_CTOR
}
//-----------------------------------------------------------------------------
MemoryStream::~MemoryStream ()
{
if (ownMemory && memory)
::free (memory);
FUNKNOWN_DTOR
}
//-----------------------------------------------------------------------------
tresult PLUGIN_API MemoryStream::read (void* data, int32 numBytes, int32* numBytesRead)
{
if (memory == nullptr)
{
if (allocationError)
return kOutOfMemory;
numBytes = 0;
}
else
{
// 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;
}
else
numBytes = maxBytes;
}
if (numBytes)
{
memcpy (data, &memory[cursor], numBytes);
cursor += numBytes;
}
}
if (numBytesRead)
*numBytesRead = numBytes;
return kResultTrue;
}
MemoryStream
は IBStream
を継承し、 IBStream
に定義された read()
関数や write()
関数などをオーバーライドしています。2
FUnknown
で定義されていた queryInterface()
/addRef()
/release()
関数は、ヘッダーファイル内の DECLARE_FUNKNOWN_METHODS
マクロと、ソースファイル内の IMPLEMENT_FUNKNOWN_METHODS
マクロによってオーバーライドしています。
コンポーネントのコンストラクタ/デストラクタそれぞれ使用されている FUNKNOWN_CTOR
/FUNKNOWN_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
参考情報
- Component Object Model(COM)とは何か まとめる
- Fabian Renn Giles - Under the hood of VST2, VST3, AU, AUv3 and AAX
おわりに
今回は VST-MA のインターフェースとコンポーネントがどのように定義されているかと、インターフェースがバイナリ境界を超えられる仕組みについて解説しました。次回はインターフェースとコンポーネントをどのように利用するかという VST-MA の作法を見ていきます。
-
前回の記事で触れた
IEditController
インターフェースのように、他のインターフェースを介して間接的にFUnknown
を継承しているケースもあります。 ↩ -
ここで使われている
SMTG_OVERRIDE
というマクロは、コンパイル時の C++ のバージョンに合わせてoverride
指定子に置き換わります。 ↩ -
これらのマクロは、実際には大した処理を行っていません。
FUNKNOWN_CTOR
は参照カウントの値を初期値として 1 にセットするだけであり、FUNKNOWN_DTOR
は何も処理を行ないません。 ↩ -
コンポーネントが複数のインターフェースを継承している場合は、
queryInterface()
関数を単純に実装すると、FUnknown
インターフェースを取得するための処理で型変換の解釈が曖昧になってコンパイルエラーが発生することがあります (擬似コード)。 VST-MA はこの問題に対してFObject
というヘルパークラスを用意しています。詳細は細かくなるため割愛しますが、複数のインターフェースを継承しているコンポーネントはFUnknown
インターフェースのポインタを取得する処理をFObject
クラスに任せることで、この問題を回避できるようになっています。 ↩ -
C++ のクラスが仮想関数を呼び出せるようにするために、コンパイラが用意する関数ポインタのテーブルで、vtable とも呼ばれます。仮想関数テーブルの仕組みはソースコードからは隠蔽されていて、プログラマがこれを直接操作することはできません。 ↩
-
C++の規格では、仮想関数を呼び出す仕組みをどのように実装するかは定義されていません。そのため、コンパイラの実装によっては、 COM が期待する仮想関数テーブルの構造(つまり MSVC コンパイラが生成する仮想関数テーブルの構造)と異なる構造や仕組みによって仮想関数の呼び出しをサポートしている可能性があり、その場合はインターフェースを受け渡しても正しくそれを利用できないことになります。ともあれ、現在の主要なC++コンパイラは、 VST-MA や COM が定める形式によって定義されたインターフェースに対して同じ構造の仮想関数テーブルを生成するので、問題なくインターフェースを受け渡して利用できます。 ↩
-
バイナリ境界をまたいで安全に関数を呼び出すためには、さらに呼び出し規約というものを合わせておく必要があります。筆者はこれについてあまり詳しくないので、詳細な解説はできませんが、 VST-MA の中では関数宣言や関数定義に含まれる PLUGIN_API というマクロが呼び出し規約を指定する役割を持っています。(ただし Windows 環境でのみ有効な値がセットされて、それ以外の環境では単に無視されます) ↩