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: 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 + } +}