【Unity】Swift のネイティブプラグインにおける汎用 ToString

## 本記事について

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

Swift において、標準ライブラリで提供されているクラスのインスタンスを `print` で出力すると、インスタンスのプロパティーの値などを出力してくれます。

```例
<CBMutableCharacteristic: 0x2c54b6f00 UUID = EA521290-A651-4FA0-A958-0CE73F4DAE55, Value = (null), Properties = 0x1, Permissions = 0x1, Descriptors = (null), SubscribedCentrals = ()>
```

そして、C# では `ToString` メソッドをオーバーライドすることで、オブジェクトの文字列表現をカスタマイズすることができます。

```Example.cs
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return $"Name: {Name}, Age: {Age}";
    }
}
```

本記事では「Swift のクラスを C# に持ってきて扱う用のクラス」において、`ToString` で Swift で出力したような文字列を返すようにする方法を説明します。

## 環境

- CoreBluetoothForUnity 0.4.7

## 出力例

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

```CBMutableCharacteristic.cs
public override string ToString() => NSObject.ToString(this, Handle);
```

のように上書きすることで `Debug.Log` で以下のような出力結果が得られます。

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

```
CBMutableCharacteristic: UUID = EA521290-A651-4FA0-A958-0CE73F4DAE55, Value = (null), Properties = 0x1, Permissions = 0x1, Descriptors = (null), SubscribedCentrals = (\n)
```

## Swift において `print` で出力される文字列の取得

クラスを文字列化する方法です。

[String(describing:)](https://developer.apple.com/documentation/swift/string/init(describing:)-67ncf) によって取得できます。

もしくは description プロパティーを直接参照することでも取得できます。
ただし、description プロパティーに直接アクセスするのは非推奨です。

https://developer.apple.com/documentation/swift/customstringconvertible

> Accessing a type’s description property directly or using CustomStringConvertible as a generic constraint is discouraged.

それを踏まえて CoreBluetoothForUnity ではアンマネージドなオブジェクトを文字列化するために以下のメソッドを定義しています。

[リンク](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/FoundationForUnity/FoundationForUnity.swift#L8)

```FoundationForUnity.swift
@_cdecl("any_object_to_string")
public func any_object_to_string(_ handle: UnsafeRawPointer) -> UnsafeMutableRawPointer {
    let instance = Unmanaged<AnyObject>.fromOpaque(handle).takeUnretainedValue()
    let str = String(describing: instance)
    let nsstring = str as NSString
    return Unmanaged.passRetained(nsstring).toOpaque()
}
```

## String(describing:) で取得できる文字列をカスタマイズ

Swift 標準ライブラリのクラスをラップする場合には、`String(describing:)` で取得できる文字列をカスタマイズする必要があります。

CoreBluetoothForUnity では、2種類の方法を使っています。

一つ目は [CustomStringConvertible](https://developer.apple.com/documentation/swift/customstringconvertible) を実装する方法です。

[CB4UMutableCharacteristic.swift](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/CB4UMutableCharacteristic.swift#L42C1-L46C2)

```
extension CB4UMutableCharacteristic: CustomStringConvertible {
    public var description: String {
        return characteristic.description
    }
}
```

二つ目は description を override する方法です。

[CB4UCentralManager.swift](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/CB4UCentralManager.swift#L68-L70)

```
    override public var description: String {
        return centralManager.description
    }
```

NSObject を継承しているクラスであれば、この方法でカスタマイズできます。

`String(describing:)` にオプショナルな型を渡すと出力結果に Optional が付与されてしまうため、それを楽に省くために先ほど非推奨と書いていた description を使っています(楽さを優先)。

## NSObject を継承したクラスで出力されるフォーマット

NSObject を継承したクラスで、プロパティーがある場合に出力されるフォーマットは以下のようになっています。

```
<クラス名: メモリアドレス, プロパティー1 = 値1, プロパティー2 = 値2, ...>
```

上記のフォーマットのうち「<>」とメモリアドレスは C# ではできれば出力したくありません。
理由はこのメモリアドレスはアンマネージドなオブジェクトのメモリアドレスであって、C# のマネージドなオブジェクトのメモリアドレスではなく紛らわしいためです。

## AnyObject.ToString, NSObject.ToString

CoreBluetoothForUnity では上記の課題を解決するために ToString を AnyObject と NSObject に分けて実装しています。

[AnyObject.cs](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/AnyObject.cs#L15-L29)

```AnyObject.cs
        public static string ToString(IntPtr handle)
        {
            if (handle == IntPtr.Zero)
            {
                return string.Empty;
            }

            using var description = GetDescription(handle);
            return description.ToString();
        }

        internal static NSString GetDescription(IntPtr handle)
        {
            return new NSString(NativeMethods.any_object_to_string(handle));
        }
```

`AnyObject.ToString` ではネイティブから受け取った文字列をそのまま返しています。

[NSObject.cs](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.7/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSObject.cs#L10-L31)

```NSObject.cs
public static string ToString<T>(T obj, SafeHandle handle)
{
    return ToString(obj, handle.DangerousGetHandle());
}

public static string ToString<T>(T obj, IntPtr handle)
{
    if (handle == IntPtr.Zero)
    {
        return typeof(T).Name;
    }

    using var description = AnyObject.GetDescription(handle);
    string pattern = @"^<.*?:\s+0x[0-9a-f]+|>$";
    var content = Regex.Replace(description.ToString(), pattern, "");
    if (content == string.Empty)
    {
        return typeof(T).Name;
    }

    return $"{obj.GetType().Name}:{content}";
}
```

`NSObject.ToString` では正規表現を使うことでネイティブから受け取った文字列からメモリアドレスと `<>` を削除しています。

## クラスがネストしている場合

上記の正規表現だと、中身のクラスの出力結果ではメモリアドレスと `<>` が表示されます。

```
CBMutableService: Primary = YES, UUID = 068C47B7-FC04-4D47-975A-7952BE1A576F, Included Services = (null), Characteristics = (
    "<CBMutableCharacteristic: 0x2a199b5f0 UUID = E3737B3F-A08D-405B-B32D-35A8F6C64C5D, Value = (null), Properties = 0x16, Permissions = 0x3, Descriptors = (null), SubscribedCentrals = (\n)>"
```

ここの対応は手間なため、妥協しています。
もし不都合があれば ToString をよしなに上書きすれば問題ないと思っています。

## 他のライブラリの ToString の実装方法

xamarin ではネイティブ側の出力結果を使わず自前で定義しているように見えました。(動作未確認)
Unity の `Apple ARKit XR Plug-in` においてはネイティブプラグイン側の出力結果をそのまま使っていました。

## おわりに

本記事では Swift のネイティブプラグインからオブジェクトを文字列化した結果をとってくる方法を紹介しました。
丁寧にやるのであれば全て C# 側で定義するのも良いと思いますが、本記事の手法で楽をしてもあまり困る人はいないかなと予想しています。