Skip to content

Instantly share code, notes, and snippets.

@pbk20191
Last active June 20, 2025 00:42
Show Gist options
  • Save pbk20191/9e4df4b5db4e71f794098a02b356925c to your computer and use it in GitHub Desktop.
Save pbk20191/9e4df4b5db4e71f794098a02b356925c to your computer and use it in GitHub Desktop.
Post Notification using Apple NSNotificationCenter with Pure c# and CoreFoundation
#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