本記事について
この記事は CoreBluetoothForUnity Advent Calendar 2023 の15日目の記事です。
Swift のネイティブプラグイン開発における文字列の渡し方、受け取る時のやり方について説明します。
文字列の配列に関しては本記事では扱いません。
例として CoreBluetoothForUnity の関数を用いますが、特に CoreBluetooth 関連の概念を知っている必要はありません。
環境
- CoreBluetoothForUnity 0.4.4
前提
文字列を引数として渡す
C#
[DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
internal static extern int cb4u_peripheral_read_characteristic_value(
SafeNativePeripheralHandle handle,
[MarshalAs(UnmanagedType.LPStr), In] string serviceUUID,
[MarshalAs(UnmanagedType.LPStr), In] string characteristicUUID
);
serviceUUID と characteristicUUID は文字列を引数として渡しています。
Swift
@_cdecl("cb4u_peripheral_read_characteristic_value")
public func cb4u_peripheral_read_characteristic_value(
_ peripheralPtr: UnsafeRawPointer,
_ serviceUUID: UnsafePointer<CChar>,
_ characteristicUUID: UnsafePointer<CChar>
) -> Int32 {
let instance = Unmanaged<CB4UPeripheral>.fromOpaque(peripheralPtr).takeUnretainedValue()
return instance.readCharacteristicValue(CBUUID(string: String(cString: serviceUUID)), CBUUID(string: String(cString: characteristicUUID)))
}
文字列は UnsafePointer<CChar>
として渡され、String(cString: serviceUUID)
で Swift の String に変換しています。
C# 側のポイント
文字列は Blittable ではないため、ネイティブプラグインに渡すためにはマーシャリングが必要です。
そのためMarshalAs
Attribute を使用しています。
UnmanagedType.LPStr は ANSI 文字列
=> Unix 系の OS では UTF-8 エンコードの文字列として扱われます。
MarshalAs
では default で UnmanagedType.LPStr
が使用されますが、
これを書くことで、UTF-8 であることを間接的に明示しています。
(間接的に明示って言葉はおかしいかもしれませんが、ニュアンスは通じるかと!)
なぜ LPStr を使用しているかは後述します。
In/Out 属性は keijiro さんの MEMO を参考につけるようにしています。
Swift 側のポイント
Swift では UTF-8 の文字列は UnsafePointer<CChar>
として扱われます。
これは Swift において CChar は Int8 の typealias であるためです。
(UTF-8 は 8bit で表現される)
あとは nullTerminatedUTF8 を引数に String を生成する init(cString: UnsafePointer) を使うことで完了です。
nullTerminatedUTF8 を cString と表現してるのもポイントです。
他のやり方
-
MarshalAs(UnmanagedType.LPWStr)
を使う - NSString のポインタとして渡す
- 長さ情報を追加して渡す
NSString を使うタイミングはあるかもしれませんが、基本的には上記の方法で十分だと思います!
文字列を受け取る
NSString の ToString()
で文字列が取得できるようにしています。
public class NSString : IDisposable
{
...
public override string ToString()
{
...
return HandleToString(Handle);
}
...
internal static string HandleToString(SafeNSStringHandle handle)
{
if (handle.IsInvalid)
return null;
NativeMethods.ns_string_get_cstring_and_length(handle, out IntPtr ptr, out int length);
if (ptr == IntPtr.Zero)
return null;
if (length == 0)
return string.Empty;
return Marshal.PtrToStringUTF8(ptr, length);
}
}
ざっくりとした手順は以下です。
- NSString のポインタを Swift から受け取る
- IntPtr.zero なら文字列は null
- NSString の中の文字列のポインタと長さを取得する
- 文字列のポインタが IntPtr.zero なら文字列は null (↑で弾いてるのでここに入ることは想定していない)
- 文字列の長さが 0 なら空文字列
- 文字列のポインタと長さを使って C# の文字列を生成する
- NSString を破棄する。
以下のテストコードの流れを例に説明します。
using (var nsString = new NSString("あいうえお"))
{
Assert.That(NSString.HandleToString(nsString.Handle), Is.EqualTo("あいうえお"));
}
NSString のポインタを Swift から受け取る
var nsString = new NSString("あいうえお")
この部分です。
public class NSString : IDisposable
{
internal SafeNSStringHandle Handle { get; private set; }
public NSString(string str)
{
if (str is null)
throw new ArgumentNullException(nameof(str));
Handle = NativeMethods.ns_string_new(str);
}
[DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
internal static extern SafeNSStringHandle ns_string_new([MarshalAs(UnmanagedType.LPStr)] string str);
SafeNSStringHandle は NSString のポインタを保持するクラスです。
ここは大事ではないため以下を参照ください。
SafeNSStringHandle
SafeNSObjectHandle
@_cdecl("ns_string_new")
public func ns_string_new(_ str: UnsafePointer<CChar>) -> UnsafeMutableRawPointer {
let nsstring = NSString(utf8String: str)!
return Unmanaged.passRetained(nsstring).toOpaque()
}
Swift の String ではなく、NSString (クラス)のポインタを返すようにします。
文字列のポインタと長さを取得する
@_cdecl("ns_string_get_cstring_and_length")
public func ns_string_get_cstring_and_length(_ handle: UnsafeRawPointer, _ ptr: UnsafeMutablePointer<UnsafePointer<CChar>?>, _ length: UnsafeMutablePointer<Int32>) {
let nsstring = Unmanaged<NSString>.fromOpaque(handle).takeUnretainedValue()
if let cstring = nsstring.utf8String {
ptr.pointee = UnsafePointer(cstring)
length.pointee = Int32(strlen(cstring))
} else {
ptr.pointee = nil
length.pointee = 0
}
}
NSString から utf8String を用いて文字列のポインタを取得しています。
この utf8String は NSString 内部のUTF-8 文字列を返します。
そして、この文字列のポインタは NSString が破棄されるまで有効です。
と、utf8String
のドキュメントの Discussion に記載してあります。
つまり、NSString さえ管理しておけばいいので、新たにメモリ確保してコピーしたりする必要はないと考えています。
文字列のポインタと長さを使って C# の文字列を生成する
これが一番楽だから使っています!
Marshal.PtrToStringUTF8(ptr, length);
実装の中身はここから見れます。
他のやり方
- NSString を使わずに文字列のポインタをもらう
- => 手動でアロケーション・コピーが必要になるかと思います。 参考
- 文字列のポインタと長さではなく直接文字列をもらう
- => やったら文字化けしました。文字化け回避できるならこれでもいいと思います。
- Marshal.PtrToStringUTF8 を使わない
- => NativeArray を使ってより Unity に適した形にすることが可能そうです。ARKit Plugin で使われています。
- SafeHandle を使わない
- => IntPtr で管理するのも良いと思います。
- StringBuilder に詰めてもらう
- => これは楽にやりたいことができます。が、非推奨とされています。
- ArrayPool に詰めてもらう
- 文字列が短い場合にはむしろ遅そうという記事を見かけたため避けました。
おわりに
本記事では Swift のネイティブプラグインとの文字列のやり取りの仕方について説明しました。
情報はいろいろ探せばでるものの、ちゃんと動くところまでいくまでにはかなり時間を使いました。
最終的にはテストも書けたし、不明点もほぼ解消されたため満足のいくものができたと思っています。
ネイティブプラグイン開発で1, 2を争うくらい詰まった部分です。これからネイティブプラグイン開発をする方の参考になれば幸いです!