Last active
June 20, 2025 00:42
-
-
Save pbk20191/9e4df4b5db4e71f794098a02b356925c to your computer and use it in GitHub Desktop.
Post Notification using Apple NSNotificationCenter with Pure c# and CoreFoundation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#nullable enable | |
using System; | |
using System.Runtime.InteropServices; | |
using System.Text; | |
public static class AppleMessagesend | |
{ | |
#if UNITY_IOS || UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_TVOS | |
private const uint kCFStringEncodingUTF8 = 0x08000100; | |
[DllImport("__Internal", CharSet = CharSet.Ansi)] | |
private static extern IntPtr CFStringCreateWithCString( | |
IntPtr alloc, | |
[MarshalAs(UnmanagedType.LPUTF8Str)] string cStr, | |
uint encoding | |
); | |
[DllImport("__Internal")] | |
private static extern void CFRelease(IntPtr cf); | |
[DllImport("__Internal")] | |
private static extern void CFRetain(IntPtr cf); | |
[DllImport("__Internal")] | |
private static extern IntPtr CFDataCreate(IntPtr allocator, byte[] bytes, nint length); | |
[DllImport("__Internal")] | |
private static extern void CFShow(IntPtr cfRef); | |
[DllImport("__Internal")] | |
private static extern IntPtr CFPropertyListCreateWithData( | |
IntPtr allocator, | |
IntPtr data, | |
ulong options, | |
IntPtr format, // nullable | |
out IntPtr error // nullable | |
); | |
[DllImport("__Internal")] | |
private static extern IntPtr CFNotificationCenterGetLocalCenter(); | |
[DllImport("__Internal")] | |
private static extern void CFNotificationCenterPostNotification(IntPtr center, IntPtr name, IntPtr obj, IntPtr userInfo, bool deliverImmediately); | |
[DllImport("__Internal")] | |
private static extern ulong CFDictionaryGetTypeID(); | |
[DllImport("__Internal")] | |
private static extern ulong CFGetTypeID(IntPtr cf); | |
[DllImport("__Internal")] | |
private static extern IntPtr CFCopyDescription(IntPtr cf); | |
[DllImport("__Internal", CallingConvention = CallingConvention.Cdecl)] | |
private static extern bool CFStringGetCString( | |
IntPtr cfString, | |
StringBuilder buffer, | |
nint maxSize, | |
uint encoding | |
); | |
[DllImport("__Internal")] | |
private static extern nint CFStringGetLength(IntPtr cfString); | |
[DllImport("__Internal", EntryPoint = "objc_getClass")] | |
private static extern IntPtr objc_getClass(string name); | |
[DllImport("__Internal", EntryPoint = "sel_registerName")] | |
private static extern IntPtr sel_registerName(string name); | |
[DllImport("__Internal", EntryPoint = "objc_msgSend")] | |
private static extern IntPtr sendDataToJsonMsg(IntPtr NSJSONSerializationClazz, IntPtr selector, IntPtr CFDataRef, nuint JSONReadingOption, out IntPtr error); | |
private sealed class CFObject : IDisposable | |
{ | |
public IntPtr Handle { get; private set; } | |
private CFObject(IntPtr handle) => Handle = handle; | |
public static CFObject? Create(IntPtr handle) | |
=> handle == IntPtr.Zero ? null : new CFObject(handle); | |
public void Dispose() | |
{ | |
if (Handle != IntPtr.Zero) | |
{ | |
CFRelease(Handle); | |
Handle = IntPtr.Zero; | |
} | |
} | |
public static implicit operator IntPtr(CFObject? obj) => obj?.Handle ?? IntPtr.Zero; | |
} | |
private ref struct AutoreleasePool | |
{ | |
[DllImport("__Internal")] | |
private static extern IntPtr objc_autoreleasePoolPush(); | |
[DllImport("__Internal")] | |
private static extern void objc_autoreleasePoolPop(IntPtr pool); | |
private IntPtr Handle; | |
private AutoreleasePool(IntPtr handle) | |
{ | |
Handle = handle; | |
} | |
public void Dispose() | |
{ | |
if (Handle != IntPtr.Zero) | |
{ | |
objc_autoreleasePoolPop(Handle); | |
Handle = IntPtr.Zero; | |
} | |
} | |
public static AutoreleasePool Create() | |
{ | |
IntPtr handle = objc_autoreleasePoolPush(); | |
if (handle == IntPtr.Zero) | |
throw new InvalidOperationException("Failed to create autorelease pool."); | |
return new AutoreleasePool(handle); | |
} | |
} | |
private readonly ref struct NSAutoreleasePoolRef | |
{ | |
[DllImport("__Internal", EntryPoint = "objc_msgSend")] | |
private static extern void msgSendVoid(IntPtr receiver, IntPtr selector); | |
[DllImport("__Internal", EntryPoint = "objc_msgSend")] | |
private static extern IntPtr objc_msgSendId(IntPtr receiver, IntPtr selector); | |
private readonly IntPtr Handle; | |
private NSAutoreleasePoolRef(IntPtr handle) | |
{ | |
Handle = handle; | |
} | |
public void Dispose() | |
{ | |
if (Handle != IntPtr.Zero) | |
{ | |
IntPtr selector = sel_registerName("drain"); | |
msgSendVoid(Handle, selector); | |
} | |
} | |
public bool IsValid => Handle != IntPtr.Zero; | |
public static NSAutoreleasePoolRef Create() | |
{ | |
IntPtr NSAutoreleasePool = objc_getClass("NSAutoreleasePool"); | |
if (NSAutoreleasePool == IntPtr.Zero) | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] NSAutoreleasePool class not found."); | |
return new NSAutoreleasePoolRef(IntPtr.Zero); | |
} | |
IntPtr selector = sel_registerName("new"); | |
IntPtr handle = objc_msgSendId(NSAutoreleasePool, selector); | |
if (handle == IntPtr.Zero) | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] Failed to dispatch msg to NSAutoreleasePool."); | |
return new NSAutoreleasePoolRef(IntPtr.Zero); | |
} | |
return new NSAutoreleasePoolRef(handle); | |
} | |
} | |
private static string CFStringToString(IntPtr cfString) | |
{ | |
if (cfString == IntPtr.Zero) | |
return string.Empty; | |
// Get character length (UTF-16 characters) | |
nint length = CFStringGetLength(cfString); | |
if (length == 0) | |
return string.Empty; | |
// Worst-case buffer size: 4 bytes per UTF-16 char (very conservative) | |
int bufferSize = (int)(length * 4) + 1; | |
StringBuilder sb = new(bufferSize); | |
if (CFStringGetCString(cfString, sb, sb.Capacity, kCFStringEncodingUTF8)) | |
{ | |
return sb.ToString(); | |
} | |
return string.Empty; | |
} | |
// Recommended to use with https://github.com/animetrics/PlistCS | |
public static bool DispatchMessage(string @namespace, byte[] plistBytes) | |
{ | |
if (string.IsNullOrEmpty(@namespace) || plistBytes.Length == 0) | |
return false; | |
using var pool = AutoreleasePool.Create(); | |
using var cfName = CFObject.Create( | |
CFStringCreateWithCString(IntPtr.Zero, @namespace, kCFStringEncodingUTF8) | |
); | |
using var cfData = CFObject.Create( | |
CFDataCreate(IntPtr.Zero, plistBytes, plistBytes.Length) | |
); | |
if (cfName == null || cfData == null) | |
return false; | |
using var plist = CFObject.Create( | |
CFPropertyListCreateWithData( | |
IntPtr.Zero, | |
cfData, | |
0, // options | |
IntPtr.Zero, // format | |
out IntPtr errorRef // error | |
) | |
); | |
using var error = CFObject.Create(errorRef); | |
if (plist == null) | |
{ | |
UnityEngine.Debug.LogWarning("[AppleMessagesend] Failed to parse plist."); | |
if (error != null) | |
{ | |
using var cfString = CFObject.Create(CFCopyDescription(error))!; | |
var description = CFStringToString(cfString); | |
if (!string.IsNullOrEmpty(description)) | |
{ | |
UnityEngine.Debug.LogError(description); | |
} | |
else | |
{ | |
CFShow(error); | |
} | |
} | |
return false; | |
} | |
else if (CFGetTypeID(plist) != CFDictionaryGetTypeID()) | |
{ | |
using var cfString = CFObject.Create(CFCopyDescription(plist))!; | |
var description = CFStringToString(cfString); | |
if (!string.IsNullOrEmpty(description)) | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] plist is not a dictionary. => " + description); | |
} | |
else | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] plist is not a dictionary. with unknown description."); | |
CFShow(plist); | |
} | |
return false; | |
} | |
else | |
{ | |
using var cfString = CFObject.Create(CFCopyDescription(plist))!; | |
var description = CFStringToString(cfString); | |
UnityEngine.Debug.Log("[AppleMessagesend] sending " + description); | |
IntPtr center = CFNotificationCenterGetLocalCenter(); | |
CFNotificationCenterPostNotification(center, cfName, IntPtr.Zero, plist, true); | |
return true; | |
} | |
} | |
public static bool DispatchJSON(string @namespace, byte[] jsonBytes) | |
{ | |
if (string.IsNullOrEmpty(@namespace) || jsonBytes.Length == 0) | |
return false; | |
using var pool = NSAutoreleasePoolRef.Create(); | |
if (!pool.IsValid) | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] Failed to create NSAutoreleasePool."); | |
return false; | |
} | |
using var cfName = CFObject.Create( | |
CFStringCreateWithCString(IntPtr.Zero, @namespace, kCFStringEncodingUTF8) | |
); | |
using var cfData = CFObject.Create( | |
CFDataCreate(IntPtr.Zero, jsonBytes, jsonBytes.Length) | |
); | |
if (cfName == null || cfData == null) | |
return false; | |
IntPtr NSJSONSerializationClazz = objc_getClass("NSJSONSerialization"); | |
IntPtr selector = sel_registerName("JSONObjectWithData:options:error:"); | |
IntPtr errorRef = IntPtr.Zero; | |
var json = CFObject.Create( | |
sendDataToJsonMsg(NSJSONSerializationClazz, selector, cfData, 0, out errorRef) | |
); | |
using var error = CFObject.Create(errorRef); | |
if (json == null) | |
{ | |
if (error != null) | |
{ | |
using var cfString = CFObject.Create(CFCopyDescription(error))!; | |
var description = CFStringToString(cfString); | |
if (!string.IsNullOrEmpty(description)) | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] Failed to parse JSON. error : => " + description); | |
} | |
else | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] Failed to parse JSON. with unknown error."); | |
CFShow(error); | |
} | |
} | |
return false; | |
} | |
else if (CFGetTypeID(json) != CFDictionaryGetTypeID()) | |
{ | |
using var cfString = CFObject.Create(CFCopyDescription(json))!; | |
var description = CFStringToString(cfString); | |
if (!string.IsNullOrEmpty(description)) | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] JSON is not a dictionary. => " + description); | |
} | |
else | |
{ | |
UnityEngine.Debug.LogError("[AppleMessagesend] JSON is not a dictionary. with unknown description."); | |
CFShow(json); | |
} | |
return false; | |
} | |
else | |
{ | |
using var cfString = CFObject.Create(CFCopyDescription(json))!; | |
var description = CFStringToString(cfString); | |
UnityEngine.Debug.Log("[AppleMessagesend] sending " + description); | |
IntPtr center = CFNotificationCenterGetLocalCenter(); | |
CFNotificationCenterPostNotification(center, cfName, IntPtr.Zero, json, true); | |
return true; | |
} | |
} | |
#else | |
public static bool DispatchMessage(string @namespace, byte[] plistBytes) | |
{ | |
Debug.LogError("[AppleMessagesend] DispatchMessage is not supported on this platform."); | |
return false; | |
} | |
public static bool DispatchJSON(string @namespace, byte[] jsonBytes) | |
{ | |
Debug.LogError("[AppleMessagesend] DispatchJSON is not supported on this platform."); | |
return false; | |
} | |
#endif | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment