【Unity】ネイティブプラグインにおける Dispose のタイミングについて

## 本記事について

この記事は [CoreBluetoothForUnity Advent Calendar 2023](https://qiita.com/advent-calendar/2023/core-bluetooth-for-unity) の24日目の記事です。

ネイティブプラグインにおいて、アンマネージドなオブジェクトを作成したらそれは必ず明示的に破棄する必要があります。

https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/unmanaged

その上で、「誰」が「いつ」破棄するのかについて開発する上でいくつかパターンがあったためそれをまとめます。

## 環境

- CoreBluetoothForUnity 0.4.7

## パターン1 使い終わったら破棄する

基本のパターンです。

[SampleLightControl_Central.cs](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Assets/CoreBluetooth/Samples/01_LightControl/SampleLightControl_Central.cs) では `CBCentralManager` のインスタンスを `Start` で作って `OnDestroy` で破棄しています。

```SampleLightControl_Central.cs
    public class SampleLightControl_Central : MonoBehaviour, ICBCentralManagerDelegate, ICBPeripheralDelegate
    {
...
        CBCentralManager _centralManager;
...
        void Start()
        {
            var initOptions = new CBCentralManagerInitOptions() { ShowPowerAlert = true };
            _centralManager = new CBCentralManager(this, initOptions);
...
        }
...
        void OnDestroy()
        {
            if (_centralManager != null)
            {
                _centralManager.Dispose();
                _centralManager = null;
            }
        }
    }
```

https://github.com/teach310/CoreBluetoothForUnity/tree/0.4.7#how-to-release

CoreBluetoothForUnity の README にも記載しています。

自分で `IDisposable` を実装したクラスをインスタンス化する場合にはこれになるかなと思います。

## パターン2 大元のオブジェクトが破棄されたら破棄する

`CBCentralManager` が持つ、`CBPeripheral` は `CBCentralManager` が破棄されたら自動的に破棄されます。

ライブラリがインスタンスするものはライブラリが破棄する役割を持つというパターンです。
以下で引数で渡る peripheral です。

[リンク](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Assets/CoreBluetooth/Samples/01_LightControl/SampleLightControl_Central.cs#L51)

```SampleLightControl_Central.cs
void ICBCentralManagerDelegate.DidDiscoverPeripheral(CBCentralManager central, CBPeripheral peripheral, int rssi)
{
    _header.SetStateText("Connecting...");
    _peripheral = peripheral;
    peripheral.Delegate = this;
    central.StopScan();
    central.Connect(peripheral);
}
```

[CBCentralManager.cs](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Packages/com.teach310.core-bluetooth-for-unity/Runtime/CBCentralManager.cs#L196-L208)

```CBCentralManager.cs
public void Dispose()
{
    if (_disposed) return;

...
    foreach (var peripheral in _peripherals.Values)
    {
        peripheral.Dispose();
    }

    _disposed = true;
}
```

作ったらキャッシュして全部破棄しています。

## パターン3 閾値を設けて破棄する

CoreBluetooth では通信のたびに、[CBATTRequest](https://developer.apple.com/documentation/corebluetooth/cbattrequest)というクラスを使ってデータをやり取りします。

このクラスは通信ごとにインスタンス化されるため数が多くなってしまいますが、C# においては参照カウントが0になったら破棄するという実装をすることはできないという問題がありました。

そのため、閾値を設けて破棄するという実装をしています。

[CBPeripheralManager.cs](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Packages/com.teach310.core-bluetooth-for-unity/Runtime/CBPeripheralManager.cs#L67)

```CBPeripheralManager.cs
    public class CBPeripheralManager : CBManager, IPeripheralManagerData, INativePeripheralManagerDelegate, IDisposable
    {
...
        static readonly int s_maxATTRequests = 10;
        Queue<IDisposable> _attRequestDisposables = new Queue<IDisposable>();
...
        void AddATTRequestDisposable(IDisposable disposable)
        {
            _attRequestDisposables.Enqueue(disposable);
            while (_attRequestDisposables.Count > s_maxATTRequests)
            {
                _attRequestDisposables.Dequeue().Dispose();
            }
        }
...

        void INativePeripheralManagerDelegate.DidReceiveReadRequest(SafeNativeATTRequestHandle requestHandle)
        {
            CallbackContext.Post(_ =>
            {
                if (_disposed) return;
                var request = new CBATTRequest(requestHandle, new NativeATTRequestProxy(requestHandle, this));
                _delegate?.DidReceiveReadRequest(this, request);
                AddATTRequestDisposable(request);
            }, null);
        }
...
        public void Dispose()
        {
            if (_disposed) return;

...
            while (_attRequestDisposables.Count > 0)
            {
                _attRequestDisposables.Dequeue().Dispose();
            }

            _disposed = true;
        }
    }
```

Queue を使って閾値を超えたら古いものから破棄という実装です。
さらにパターン2のように大元のオブジェクトが破棄されたら同時に全部破棄します。

## IDisposable をあえて実装しないという手

パターン2 と パターン3 ではライブラリ側が破棄する役割を持っています。
であるならば `Dispose` メソッドを公開する必要はないため、そもそも `IDisposable` を実装しないという手もありかなと思いました。

それによって、ユーザーがこのオブジェクトは破棄すべきなんだろうか..?と悩む必要がなくなるかもしれません。(需要があるかはわかりませんが..)

## おわりに

本記事ではネイティブプラグインにおける Dispose の呼び出し方の種類についてまとめました。
本記事の内容は CoreBluetoothForUnity の開発で得た知見ですが、他のライブラリがどうやっているか等は正直わからない部分も多いです。他のパターンもご存知の方いたらコメントいただけると嬉しいです！