【Unity】Swift のネイティブプラグインとの配列のやり取りの仕方

## 本記事について

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

Swift のネイティブプラグイン開発における文字列、数値、参照型の配列の渡し方、受け取る時のやり方について説明します。

例として CoreBluetoothForUnity の関数を用いますが、特に CoreBluetooth 関連の概念を知っている必要はありません。

## 環境

- CoreBluetoothForUnity 0.4.5

## 文字列の配列を引数として渡す

文字列の配列は非 Blittable です。したがってマーシャリングが必要です。

### C#

[リンク](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.5/Packages/com.teach310.core-bluetooth-for-unity/Runtime/NativeMethods.cs#L57-L62)

```NativeMethods.cs
[DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
internal static extern void cb4u_central_manager_scan_for_peripherals(
    SafeNativeCentralManagerHandle handle,
    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 2)] string[] serviceUUIDs,
    int serviceUUIDsCount
);
```

引数の serviceUUIDs で文字列の配列を渡しています。

以下のドキュメントに文字列の配列を渡すサンプルがあります。

[Default Marshalling for Arrays](https://learn.microsoft.com/en-us/dotnet/framework/interop/default-marshalling-for-arrays#managed-signature-1)

また、[UnmanagedType Enum](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedtype?view=net-7.0) に LPArray に関する説明があります。

> A pointer to the first element of a C-style array

### Swift

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

```CoreBluetoothForUnity.swift
@_cdecl("cb4u_central_manager_scan_for_peripherals")
public func cb4u_central_manager_scan_for_peripherals(
    _ centralPtr: UnsafeRawPointer,
    _ serviceUUIDs: UnsafePointer<UnsafePointer<CChar>?>,
    _ serviceUUIDsCount: Int32
) {
    let instance = Unmanaged<CB4UCentralManager>.fromOpaque(centralPtr).takeUnretainedValue()
    
    let serviceUUIDsArray = (0..<Int(serviceUUIDsCount)).map { index -> CBUUID in
        let uuidString = String(cString: serviceUUIDs[index]!)
        return CBUUID(string: uuidString)
    }
    
    instance.scanForPeripherals(withServices: serviceUUIDsArray)
}
```

`_ serviceUUIDs: UnsafePointer<UnsafePointer<CChar>?>`で受け取っています。

配列の中の要素へのアクセスは `serviceUUIDs[index]!` で行っています。

ここでは serviceUUIDs、その中の要素 が null であることは想定していません。(C# 側で null を渡さないようにしています)

## 数値の配列を引数として渡す

ここでの数値は[ここ](https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types)に記載されている Int32 (= int) 等を指します。

上記に記載のある通り、数値は Blittable です。したがってマーシャリングは不要です。
そのまま渡せます。

例

```.cs
[DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
internal static extern void print_numbers(
    int[] numbers,
    int numbersCount
);
```

配列の長さは渡す必要があります。

## 参照型の配列を引数として渡す

IntPtr の配列を渡すのが楽でした。

例としてアンマネージドなオブジェクトの配列を渡す場合の例を示します。

### C#

[リンク](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.5/Packages/com.teach310.core-bluetooth-for-unity/Runtime/NativeMethods.cs#L242)

```NativeMethods.cs
[DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
internal static extern int cb4u_mutable_service_set_characteristics(SafeNativeMutableServiceHandle handle, IntPtr[] characteristics, int characteristicsCount);
```

characteristics はアンマネージドなオブジェクトのポインタの配列です。

[SetCharacteristics](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.5/Packages/com.teach310.core-bluetooth-for-unity/Runtime/NativeMutableServiceProxy.cs#L14)

```NativeMutableServiceProxy.cs
internal void SetCharacteristics(CBCharacteristic[] characteristics)
{
    IntPtr[] mutableCharacteristics = null;
    if (characteristics != null)
    {
        mutableCharacteristics = new IntPtr[characteristics.Length];
        for (int i = 0; i < characteristics.Length; i++)
        {
            mutableCharacteristics[i] = ConvertToCBMutableCharacteristic(characteristics[i]).Handle.DangerousGetHandle();
        }
    }
    NativeMethods.cb4u_mutable_service_set_characteristics(_handle, mutableCharacteristics, mutableCharacteristics?.Length ?? 0);
}
```

SafeHandle から DangerousGetHandle() でポインタを取得しています。

### Swift

[リンク](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.5/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/CoreBluetoothForUnity.swift#L440-L453)

```CoreBluetoothForUnity.swift
@_cdecl("cb4u_mutable_service_set_characteristics")
public func cb4u_mutable_service_set_characteristics(_ servicePtr: UnsafeRawPointer, _ characteristicsPtr: UnsafePointer<UnsafeRawPointer>?, _ characteristicsCount: Int32) {
    let service = Unmanaged<CB4UMutableService>.fromOpaque(servicePtr).takeUnretainedValue()
    
    if let characteristicsPtr = characteristicsPtr {
        let characteristics = (0..<Int(characteristicsCount)).map { index -> CB4UMutableCharacteristic in
            let characteristicPtr = characteristicsPtr[index]
            return Unmanaged<CB4UMutableCharacteristic>.fromOpaque(characteristicPtr).takeUnretainedValue()
        }
        service.setCharacteristics(characteristics)
    } else {
        service.setCharacteristics(nil)
    }
}
```

characteristicsPtr `UnsafePointer<UnsafeRawPointer>?` に IntPtr の配列が渡されています。

あとはループ回して1つずつポインタからオブジェクトを取得しています。

## 数値型の配列を受け取る

固定長であれば C# 側で配列を用意して、それをネイティブプラグイン側に渡して値詰めてもらうのが楽だと思います。

byte 配列の場合とほぼ同じなため例は省略します！
TODO: 前回の記事貼る

## 汎用的に配列を受け取る

NSArray を使うと便利です。

具体的な手順は以下です。

1. Swift から NSArray のポインタをもらう
2. NSArray の長さを取得する
3. 長さ分ループを回して、各要素のポインタを取得する
4. 各要素のポインタをよしなに利用する。

文字列の場合の例を示します。

CoreBluetoothForUnity では NSArray に `StringsFromHandle` という関数を用意しています。

```NSArray.cs
public static string[] StringsFromHandle(SafeNSArrayHandle handle)
{
    var ptrs = ArrayFromHandle<string>(handle);
    var array = new string[ptrs.Length];
    for (var i = 0; i < ptrs.Length; i++)
    {
        using var nsstring = new NSString(ptrs[i]);
        array[i] = nsstring.ToString();
    }
    return array;
}
```

[リンク](https://github.com/teach310/CoreBluetoothForUnity/blob/0.4.5/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSArray.cs#L61-L71)

以下のように NSArray のポインタ(ハンドル)を渡して、文字列の配列を取得できます。

```NSArrayTests.cs
[Test]
public void StringsFromHandle()
{
    using var nsArray = NSArray.FromStrings(new[] { "hoge", "fuga" });
    var strings = NSArray.StringsFromHandle(nsArray.Handle);
    Assert.That(strings, Is.EqualTo(new[] { "hoge", "fuga" }));
}
```

## 文字列を受け取る時のその他のやり方

もし文字列にカンマが含まれていないのであれば、カンマ区切りの文字列を渡して、C# 側で Split するということも可能でした。

## 参考

Xamarin の NSArray

https://github.com/xamarin/xamarin-macios/blob/xamarin-ios-16.4.0.23/src/Foundation/NSArray.cs

## おわりに

Swift のネイティブプラグイン開発における文字列、数値、参照型の配列の渡し方、受け取る時のやり方について説明しました。
NSArray を学ぶ前はなるべく配列を避けたいという思いからカンマ区切りの文字列を使ったりと工夫で乗り切ろうとしましたが結局配列からは逃れられませんでした。
NSArray を学んだ後はもう全部これでいいんじゃないかなぐらいに思っています。
本記事の扱い方はやり方の一つであり、目的に応じたより良いやり方はあると思います！