VST3ホストを作ろう（１０） 〜インターフェースとコンポーネントのクラス定義〜

[目次](https://qiita.com/hotwatermorning/items/5ab2a687a4cbc73a0f56)

## はじめに

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

　また、この記事の後半では、インターフェースがバイナリ境界を超えられる仕組みについて解説します。

## FUnknown のクラス定義

　[前回](https://qiita.com/hotwatermorning/items/efe9be3934c6863b2a3b)解説したように、 VST-MA のインターフェースは C++ の抽象基底クラスとして定義されます。そしてインターフェースは他のインターフェースを継承することもでき、その最も基底に存在するのが `FUnknown` インターフェースです。

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

```cpp:FUnknown&nbsp;の定義&nbsp;(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
{
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)
```
*https://github.com/steinbergmedia/vst3_pluginterfaces/blob/227ea1d8f749c6a66dd591582061d58682b4aa9f/base/funknown.h#L337*

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

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

## インターフェースのクラス定義

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

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

```cpp:IBStream&nbsp;の定義&nbsp;(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
{
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)
```
*https://github.com/steinbergmedia/vst3_pluginterfaces/blob/227ea1d8f749c6a66dd591582061d58682b4aa9f/base/ibstream.h#L31*

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

[^derive-from-funknown]: 前回の記事で触れた `IEditController` インターフェースのように、他のインターフェースを介して間接的に `FUnknown` を継承しているケースもあります。

## コンポーネントのクラス定義

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

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

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

```cpp:MemoryStream&nbsp;のクラス定義&nbsp;(memorystream.h)
//------------------------------------------------------------------------
/** 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
};
```
*https://github.com/steinbergmedia/vst3_public_sdk/blob/c04e9bd690f50893742e2a082abab47af2c3efc8/source/common/memorystream.h#L48*

```cpp:&nbsp;MemoryStream&nbsp;のメンバ関数定義（一部を抜粋）&nbsp;(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)
{ 
	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;
}
```
*https://github.com/steinbergmedia/vst3_public_sdk/blob/c04e9bd690f50893742e2a082abab47af2c3efc8/source/common/memorystream.cpp*

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

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

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

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

　このようにコンポーネントは、インターフェースを継承した具象クラスとして定義され、インターフェースが定義している純粋仮想関数をオーバーライドして、インターフェースが提供する機能を実装します。

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

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

## インターフェースがバイナリ境界を超えられる仕組み

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

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

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

```cpp
//============================================================
// ホストアプリケーションと 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) 型のみを受け取る、例外を使わない、演算子オーバーロードを行わないなどのいくつかのルールを満たすような抽象基底クラスとして定義されます。
　このようにして定義されたクラスは、コンパイラの種類やビルド設定によらず、同じ構造の仮想関数テーブル[^about-vtable]を持つ性質があるため、このインターフェースをバイナリ境界を超えて受け渡しても、正しく仮想関数を呼び出せます。[^com-portability] [^calling_convention]

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

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

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

#### 参考情報

* [Component Object Model(COM)とは何か まとめる](https://garicchi.com/?p=19259)
* [Fabian Renn Giles - Under the hood of VST2, VST3, AU, AUv3 and AAX](https://youtu.be/swVqdbhfkkE?t=2336)

## おわりに

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