From c6ea5aeb7a90a44b5d3a2904a70f76655e00b67f Mon Sep 17 00:00:00 2001 From: teach310 Date: Thu, 26 Oct 2023 00:28:59 +0900 Subject: [PATCH 1/2] add NSString MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## NSStringクラスを追加した理由 汎用的な文字列取得実装のため。 MicrosoftのドキュメントでStringBuilderは非推奨と書かれていたためにStringBuilderを使い続けることに抵抗があった。 https://learn.microsoft.com/ja-jp/dotnet/standard/native-interop/best-practices そして、NSDictionaryに文字列を登録するため ## NSString.HandleToStringについて 流れに関してはXamarinの実装を参考にしている。 https://github.com/xamarin/xamarin-macios/blob/main/src/CoreFoundation/CFString.cs#L190-L224 ここではFromHandleという関数名だが、NSString.FromHandleという名前であればNSStringを返した方が自然だと思うためHandleToStringという名前にした。 引数のreleaseHandleに関してはSafeNSStringHandleをusingで破棄する方が書き心地よいと感じるため不要と判断した。 ## NSStringを使用している理由 swiftのStringは値型のためUnmanaged.passRetainedの引数にすることができない。 したがって、csharp側にポインタを渡すためにはCFStringまたはNSString等のクラスを用いる必要があった。 そして、CFStringよりもNSStringのほうが扱いやすかったためNSStringを使用している。 NSStringを参照型のクラスとして使用することはswiftの公式ドキュメントにも書かれている。 > They provide reference semantics instead of value semantics, which is a useful tool to have in the toolbox. https://github.com/apple/swift-corelibs-foundation ## NSStringのcstringからのコピーをcsharp側で行っている理由 char配列を渡してそこにswiftで文字列を入れたところ文字化けしたから。 StringBuilderの場合は文字化けしないが前述の理由により避けた。 byte配列を渡せば文字化けしないかもしれないが、ArrayPoolが遅いかもしれず、最適な方法がわからなかったため 中身のコード見た感じ問題なさそうで一番楽なMarshal.PtrToStringUTF8を使用した。 https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.cs,80 ## LengthOfBytesUtf8について 使ってないけど、テストで活用できるため残している。 --- .../Runtime/Foundation/NSString.cs | 61 +++++++++++++++++++ .../Runtime/Foundation/NSString.cs.meta | 11 ++++ .../Runtime/Foundation/NativeMethods.cs | 9 +++ .../Runtime/Foundation/SafeNSStringHandle.cs | 10 +++ .../Foundation/SafeNSStringHandle.cs.meta | 11 ++++ .../Tests/Runtime/Foundation/NSStringTests.cs | 54 ++++++++++++++++ .../Runtime/Foundation/NSStringTests.cs.meta | 11 ++++ 7 files changed, 167 insertions(+) create mode 100644 Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs create mode 100644 Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs.meta create mode 100644 Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs create mode 100644 Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs.meta create mode 100644 Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs create mode 100644 Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs.meta diff --git a/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs new file mode 100644 index 0000000..9d1f463 --- /dev/null +++ b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.InteropServices; + +namespace CoreBluetooth.Foundation +{ + 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); + } + + internal NSString(SafeNSStringHandle handle) + { + Handle = handle; + } + + internal NSString(IntPtr handle) + { + Handle = new SafeNSStringHandle(handle); + } + + public int LengthOfBytesUtf8() + { + ExceptionUtils.ThrowObjectDisposedExceptionIf(Handle.IsInvalid, this); + return NativeMethods.ns_string_length_of_bytes_utf8(Handle); + } + + public override string ToString() + { + ExceptionUtils.ThrowObjectDisposedExceptionIf(Handle.IsInvalid, this); + return HandleToString(Handle); + } + + public void Dispose() + { + if (Handle != null && !Handle.IsInvalid) + Handle.Dispose(); + } + + 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); + } + } +} diff --git a/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs.meta b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs.meta new file mode 100644 index 0000000..ee517aa --- /dev/null +++ b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NSString.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63bfe215835164c11b748c3372df3b75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NativeMethods.cs b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NativeMethods.cs index 56338ac..4f4e901 100644 --- a/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NativeMethods.cs +++ b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/NativeMethods.cs @@ -25,5 +25,14 @@ internal static class NativeMethods [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)] internal static extern int ns_number_int_value(SafeNSNumberHandle handle); + + [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)] + internal static extern SafeNSStringHandle ns_string_new([MarshalAs(UnmanagedType.LPStr)] string str); + + [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)] + internal static extern int ns_string_length_of_bytes_utf8(SafeNSStringHandle handle); + + [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)] + internal static extern void ns_string_get_cstring_and_length(SafeNSStringHandle handle, out IntPtr ptr, out int length); } } diff --git a/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs new file mode 100644 index 0000000..932519c --- /dev/null +++ b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs @@ -0,0 +1,10 @@ +using System; + +namespace CoreBluetooth.Foundation +{ + internal class SafeNSStringHandle : SafeNSObjectHandle + { + public SafeNSStringHandle() : base() { } + public SafeNSStringHandle(IntPtr handle) : base(handle) { } + } +} diff --git a/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs.meta b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs.meta new file mode 100644 index 0000000..a782128 --- /dev/null +++ b/Packages/com.teach310.core-bluetooth-for-unity/Runtime/Foundation/SafeNSStringHandle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66452e05814934a688bb31bf681ce898 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs b/Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs new file mode 100644 index 0000000..e1b9068 --- /dev/null +++ b/Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs @@ -0,0 +1,54 @@ +using CoreBluetooth.Foundation; +using NUnit.Framework; + +namespace CoreBluetoothTests.Foundation +{ + public class NSStringTests + { + [Test] + public void New() + { + using var nsString = new NSString("dummy"); + Assert.That(nsString.Handle, Is.Not.Null); + Assert.That(nsString.Handle.IsInvalid, Is.False); + } + + [Test] + public void LengthOfBytesUtf8() + { + using (var nsString = new NSString("a")) + { + Assert.That(nsString.LengthOfBytesUtf8(), Is.EqualTo(1)); + } + + using (var nsString = new NSString("あ")) + { + Assert.That(nsString.LengthOfBytesUtf8(), Is.EqualTo(3)); + } + + using (var nsString = new NSString("𠮷")) + { + Assert.That(nsString.LengthOfBytesUtf8(), Is.EqualTo(4)); + } + } + + [Test] + public void HandleToString() + { + using (var nsString = new NSString("dummy")) + { + Assert.That(NSString.HandleToString(nsString.Handle), Is.EqualTo("dummy")); + } + + using (var nsString = new NSString("あいうえお")) + { + Assert.That(NSString.HandleToString(nsString.Handle), Is.EqualTo("あいうえお")); + } + + using (var nsString = new NSString(string.Empty)) + { + Assert.That(NSString.HandleToString(nsString.Handle), Is.EqualTo(string.Empty)); + } + } + } +} diff --git a/Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs.meta b/Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs.meta new file mode 100644 index 0000000..6487f38 --- /dev/null +++ b/Packages/com.teach310.core-bluetooth-for-unity/Tests/Runtime/Foundation/NSStringTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bab5ec5e9e0894e8ea6a61fe9486284d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From d84e1b54aa501675edb917898d13e0a78cd6295d Mon Sep 17 00:00:00 2001 From: teach310 Date: Thu, 26 Oct 2023 00:31:32 +0900 Subject: [PATCH 2/2] =?UTF-8?q?##=20nsstring.utf8String=E3=81=AE=E3=83=9D?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=BF=E3=81=AF=E8=A7=A3=E6=94=BE=E3=81=97?= =?UTF-8?q?=E3=81=AA=E3=81=8F=E3=81=A6=E8=89=AF=E3=81=84=E3=81=8B=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 良い。NSStringを解放したらこれも解放される(内部だから)。 > This C string is a pointer to a structure inside the string object, which may have a lifetime shorter than the string object and will certainly not have a longer lifetime. https://developer.apple.com/documentation/foundation/nsstring/1411189-utf8string ## utf8StringはNSStringの中身だがこれをコピーしなくてよいのか? NSStringはヒープなため別の場所にコピーするメリットはlifetimeをNSStringと分離することだと思う。 同じでいいためしなくて良いと判断 ## cstringの文字列の長さの測り方 ここにstrlenを使った例がある https://developer.apple.com/documentation/swift/string/utf8cstring --- .../FoundationForUnity.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/FoundationForUnity/FoundationForUnity.swift b/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/FoundationForUnity/FoundationForUnity.swift index d249030..11dc0ad 100644 --- a/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/FoundationForUnity/FoundationForUnity.swift +++ b/Plugins/CoreBluetoothForUnity/Sources/CoreBluetoothForUnity/FoundationForUnity/FoundationForUnity.swift @@ -28,3 +28,27 @@ public func ns_number_int_value(_ handle: UnsafeRawPointer) -> Int32 { let instance = Unmanaged.fromOpaque(handle).takeUnretainedValue() return instance.int32Value } + +@_cdecl("ns_string_new") +public func ns_string_new(_ str: UnsafePointer) -> UnsafeMutableRawPointer { + let nsstring = NSString(utf8String: str)! + return Unmanaged.passRetained(nsstring).toOpaque() +} + +@_cdecl("ns_string_length_of_bytes_utf8") +public func ns_string_length_of_bytes_utf8(_ handle: UnsafeRawPointer) -> Int32 { + let nsstring = Unmanaged.fromOpaque(handle).takeUnretainedValue() + return Int32(nsstring.lengthOfBytes(using: String.Encoding.utf8.rawValue)) +} + +@_cdecl("ns_string_get_cstring_and_length") +public func ns_string_get_cstring_and_length(_ handle: UnsafeRawPointer, _ ptr: UnsafeMutablePointer?>, _ length: UnsafeMutablePointer) { + let nsstring = Unmanaged.fromOpaque(handle).takeUnretainedValue() + if let cstring = nsstring.utf8String { + ptr.pointee = UnsafePointer(cstring) + length.pointee = Int32(strlen(cstring)) + } else { + ptr.pointee = nil + length.pointee = 0 + } +}