JUCEのserialisationモジュールを触ってみました

本記事は[JUCE Advent Calendar 2023](https://qiita.com/advent-calendar/2023/juce) の12月15日向けに投稿した記事です。

# はじめに

JUCE (Jules' Utility Class Extensions)は、C++言語によるマルチメディア系アプリケーションの開発を支援するフレームワークです。
[公式サイト](https://juce.com/)

JUCE 7.0.8からデータシリアライズの機能を提供するserialisationモジュールが追加されました。本記事はそのserializationモジュールを実際に触ってみた感想を含む解説記事になります。

[クラスリファレンス：juce__core-serialisation](https://docs.juce.com/master/group__juce__core-serialisation.html)


# 対象とするバージョン

+ JUCE 7.0.9
+ Visual Studio 2022
+ Xcode 14.2
+ CMake 3.25以上
+ Git 2.3以上

# juce::varについて

serialisationモジュールについて解説をする前に、そのモジュールの土台となる `juce::var` クラスについて解説します。
`juce::var` クラスは、JUCE ライブラリ内の多目的なバリアントクラスであり、様々なプリミティブな値、文字列、または `juce::ReferenceCountedObject` から派生した任意のオブジェクトを保持できるように設計されています。

[クラスリファレンス：juce::var](https://docs.juce.com/master/classvar.html)
```
A variant class, that can be used to hold a range of primitive values.

A var object can hold a range of simple primitive values, strings, or any kind of ReferenceCountedObject. The var class is intended to act like the kind of values used in dynamic scripting languages.

You can save/load var objects either in a small, proprietary binary format using writeToStream()/readFromStream(), or as JSON by using the JSON class.
```

このクラスは、動的なスクリプト言語で使用される値のような柔軟性を提供することが意図されています。

+ 多様性： `juce::var` は整数、浮動小数点数、文字列、ブール値、配列、オブジェクトなど、さまざまなタイプのデータを保持できます。

+ 動的スクリプティング： 動的なスクリプト言語で使用される値の柔軟性を模倣するように設計されています。これは、`juce::var` インスタンスに異なるタイプの値を明示的に宣言せずに簡単に代入できることを意味します。

+ シリアライゼーション： `juce::var` は、異なる形式で保存および読み込むことができます。`juce::var::writeToStream()` や `juce::var::readFromStream()` のようなメソッドを提供して、小規模で独自のバイナリ形式で保存できます。さらに、JSON クラスを使用して `juce::var` オブジェクトを JSON 形式で保存および読み込むことができます。

独自のバイナリ形式で保存する例：

```cpp
juce::MemoryOutputStream binaryData;
myVarObject.writeToStream(binaryData);
```
JSON で保存する例：

```cpp
juce::var myVarObject = /* some data */;
juce::String jsonString = juce::JSON::toString(myVarObject);
```

+ 相互運用性：バイナリおよび JSON 形式の保存と読み込みができることは、これらの形式をサポートする他のシステムや言語との連携を容易にします。実際にこの仕組みを利用することで、プラットフォームやCPUアーキテクチャに依存せずにオーディオプラグインのプリセットデータを保存と読み込みをすることができます。具体的には、Windowsで保存したVSTプラグインのプリセットをmacOSで読み込むということを実現します。

使用例：

```cpp
juce::var myVar = 42; // 整数を代入
myVar = "Hello, JUCE!"; // 文字列を代入
myVar = juce::JSON::parse("{ \"key\": \"value\" }"); // JSON オブジェクトを代入
```

# JUCEのserialisationモジュールについて

serialisationモジュールはテンプレートクラス、テンプレート関数として提供されます。

特にシリアライザの実装は `juce::SerialisationTraits<T>` テンプレートを特殊化することで実装を定義することができます。

[クラスリファレンス：juce::SerialisationTraits](https://docs.juce.com/master/structSerialisationTraits.html)

## SerialisationTraitsについて

 `juce::SerialisationTraits<T>` は、特定の型に対してシリアライゼーション関数を関連付けるためのものであり、その型の宣言を変更せずに利用できます。

SerialisationTraits の特殊化には次の要素が含まれます

### marshallingVersion定数

`marshallingVersion` という名前の静的な `constexpr` データメンバが必要で、その値は `std::optional<int>` に変換可能である必要があります。
`marshallingVersion` が null オプションに変換される場合、型をシリアライズする際にはすべてのバージョニング情報が無視されます。それ以外の場合、このバージョニング情報は型をシリアライズする際に含まれます。

### serialise関数

読み込みと保存の処理が共通であるような通常の用途では `serialise` 関数を実装すれば十分です。

```cpp
template <typename Archive, typename Item>
static void serialise (Archive& archive, Item& item);
```

### load関数, save関数

読み込みと保存で異なる作業を行う必要がある型の場合、`load`, `save` という2つの関数を実装します。

```cpp
template <typename Archive>
static void load (Archive& archive, T& item);

template <typename Archive>
static void save (Archive& archive, const T& item);
```

シリアライズ対象としたい型がコピーコンストラクタをサポートしていない場合は、上記関数の実装内で値のコピー処理を記述することで、型の内部に変更を加えることなくシリアライズ可能とすることができます。

`serialise()` および `load()` 内で `archive.getVersion()` を呼び出すと、デシリアライズされているオブジェクトの検出されたバージョンが取得できます。`archive.getVersion()` は `std::optional<int>` を返し、その値が `nullopt` だった場合はバージョニング情報が検出されなかったことを示します。


# サンプルコード

次のサンプルコードでは、JUCEのserialisationモジュールを使用して以下の仕組みを実験しています。

+ シリアライズ対象のC++構造体を新規に定義する
+ C++構造体をシリアライズするテンプレート特殊化を実装する
+ シリアライズした値をJSON文字列に変換する
+ JSON文字列をメモリ領域に書き込むことで、スコープを越えて値の受け渡しをする
+ JSON文字列をデシリアライズして取得したC++構造体の値をコンソールに出力する


```Main.cpp
#include <JuceHeader.h>

// シリアライズ対象のC++構造体
struct SerializableData
{
  int ix = 1;
  float fx = 809.0f;
  double dx = 123131.0;
  juce::String sx = "text data";
  std::vector<juce::String> vs;

  juce::String print() const
  {
    juce::String text;
    text << juce::String(ix) << " - " 
         << juce::String(fx, 10) << " - " 
         << juce::String(dx, 10) << " - " 
         << juce::String(sx) << " - ";
    for (const auto& s : vs)
    {
      text << s << " , ";
    }

    return text;
  }
};

// SerializableData構造体をシリアライズするためのテンプレート特殊化
template <>
struct juce::SerialisationTraits<SerializableData>
{
  static constexpr auto marshallingVersion = 3;

  template <typename Archive, typename T>
  static void serialise(Archive& archive, T& t)
  {
    archive(
      named("x", t.ix),
      named("fx", t.fx),
      named("dx", t.dx),
      named("sx", t.sx),
      named("vs", t.vs)
    );
  }
};

//==============================================================================
int main(int argc, char* argv[])
{
    // シリアライズされたデータを受け渡しするためのメモリ領域
    juce::MemoryBlock memory_block;

    // シリアライズ処理
    // C++言語の構造体からjuce::var形で保持するデータ型に変換してJSON文字列としてメモリ領域に書き込む
    {
        SerializableData serializedata;
        serializedata.ix = 11111;
        serializedata.fx = 3178.0212f;
        serializedata.dx = 790179.0419;
        serializedata.sx = "this is custom text";
        serializedata.vs.push_back("eee");
        serializedata.vs.push_back("bbb");
        serializedata.vs.push_back("cccc");
        serializedata.vs.push_back("11111");

        // コンソールへの出力
        juce::Logger::outputDebugString(serializedata.print());

        // JUCE APIによるシリアライズ処理の実行
        const auto var_data = juce::ToVar::convert(serializedata);

        // nulloptチェック
        if (var_data.has_value())
        {
            const juce::var var_write = var_data.value();
            juce::MemoryOutputStream mos(memory_block, false);

            // JSON文字列としてメモリ領域に書き込む
            juce::JSON::writeToStream(mos, var_write);
        }
    }

    // デシリアライズ処理
    // メモリ領域に書き込まれたJSON文字列をパースしてjuce::var形で保持するデータ型に変換してC++言語の構造体に値を書き込む
    {
        juce::MemoryInputStream mis(memory_block, false);
        
        const auto str = juce::String::createStringFromData(memory_block.getData(), memory_block.getSize());

        juce::Logger::outputDebugString(str);

        // メモリ領域に書き込まれたJSON文字列をパースしてjuce::var型のデータを取得する
        juce::var var_read = juce::JSON::parse(str);

        // JUCE APIによるデシリアライズ処理の実行
        const auto deserialize = juce::FromVar::convert<SerializableData>(var_read);
        
        // nulloptチェック
        if (deserialize.has_value())
        {
            // コンソールへの出力
            juce::Logger::outputDebugString(deserialize.value().print());
        }
    }

    return 0;
}

```

# 使いどころ

JUCE 7.0.8にserialisationモジュールが追加されるまで、オーディオプラグイン開発者はプラグインのプリセットデータのシリアライズ処理などを、`juce::ValueTree` から生成できるXMLファイルや、`juce::var` 型を用いて操作可能なJSONファイル等で実装していましたが、これらはあくまでもテキストファイルフォーマットの上でデータシリアライズを実装するものであり、シリアライゼーション自体の詳細、特にシリアライズバージョンの定義等は、プラグイン開発者独自の実装次第となっていました。また、C++の標準ライブラリにはシリアライゼーションの実装は含まれておらず、JUCE以外のシリアライゼーションライブラリを使うことも珍しくない状況でした（筆者は詳しくは使用したことは無いですが、`boost::serialization`や`cereal`等がC++にはあります。）

JUCE 7.0.8からserialisationモジュールが追加されてC++構造体をJUCE APIでシリアライズできるようになることで、プラグインプリセットなどの状態変数の保存と読み込みのシリアライゼーションフォーマットの設計のハードルが下がるという恩恵が受けられると思います。

特に、オーディオプラグインの状態変数にバージョンを付与することで、DAWプロジェクトやプラグインプリセットに保存されたデータからバージョン番号を取得し、バージョン番号に適したシリアライズ、デシリアライズ処理を実装する仕組みを構築することができるので、プラグイン開発の悩みの一つである、プリセットの互換性についての悩みを助ける仕組みとなるでしょう。