0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

CoreBluetoothForUnityAdvent Calendar 2023

Day 23

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

Last updated at Posted at 2023-12-22

本記事について

この記事は CoreBluetoothForUnity Advent Calendar 2023 の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

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

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

CBMutableCharacteristicTests.cs

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

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

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

String(describing:) によって取得できます。

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

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

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

リンク

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 を実装する方法です。

CB4UMutableCharacteristic.swift

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

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

CB4UCentralManager.swift

    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

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

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# 側で定義するのも良いと思いますが、本記事の手法で楽をしてもあまり困る人はいないかなと予想しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?