From 508f2d04080f6fc84a063c4f5319dbbd7a87a40d Mon Sep 17 00:00:00 2001 From: tinysec Date: Thu, 9 Jul 2026 01:32:31 +0800 Subject: [PATCH] feat: TypeArchive type-add/update/rename/delete notifications Mirrors Python TypeArchiveNotification (typearchive.py:732) with four virtual methods (OnTypeAdded/OnTypeUpdated/OnTypeRenamed/OnTypeDeleted) plus TypeArchive.RegisterNotification/UnregisterNotification over the existing BNRegister/BNUnregisterTypeArchiveNotification P/Invokes. A TypeArchiveNotificationContext roots the four native wrapper delegates and the allocated BNTypeArchiveNotification struct for the registration lifetime (mirrors the Activity/AnalysisCompletionEvent delegate-rooting pattern), marshals each callback's native args (archive via BorrowHandle, id via PtrToStringAnsi, type via NewFromHandle, names via PtrToStructure+FromNative), and swallows exceptions so a throwing override cannot abort core dispatch. --- Delegate/TypeArchiveNotificationCallbacks.cs | 224 +++++++++++++++++++ Handle/BNTypeArchive.cs | 57 +++++ Struct/BNTypeArchiveNotification.cs | 60 ++++- 3 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 Delegate/TypeArchiveNotificationCallbacks.cs diff --git a/Delegate/TypeArchiveNotificationCallbacks.cs b/Delegate/TypeArchiveNotificationCallbacks.cs new file mode 100644 index 0000000..fb44a6a --- /dev/null +++ b/Delegate/TypeArchiveNotificationCallbacks.cs @@ -0,0 +1,224 @@ +using System; +using System.Runtime.InteropServices; + +namespace BinaryNinja +{ + internal static partial class NativeDelegates + { + // void (*typeAdded)(void* ctxt, BNTypeArchive* archive, const char* id, BNType* definition) + public delegate void TypeArchiveTypeAddedDelegate( + IntPtr context, IntPtr archive, IntPtr id, IntPtr definition); + + // void (*typeUpdated)(void* ctxt, BNTypeArchive* archive, const char* id, BNType* oldDefinition, BNType* newDefinition) + public delegate void TypeArchiveTypeUpdatedDelegate( + IntPtr context, IntPtr archive, IntPtr id, IntPtr oldDefinition, IntPtr newDefinition); + + // void (*typeRenamed)(void* ctxt, BNTypeArchive* archive, const char* id, BNQualifiedName* oldName, BNQualifiedName* newName) + public delegate void TypeArchiveTypeRenamedDelegate( + IntPtr context, IntPtr archive, IntPtr id, IntPtr oldName, IntPtr newName); + + // void (*typeDeleted)(void* ctxt, BNTypeArchive* archive, const char* id, BNType* definition) + public delegate void TypeArchiveTypeDeletedDelegate( + IntPtr context, IntPtr archive, IntPtr id, IntPtr definition); + } + + internal static partial class UnsafeUtils + { + // Adapts a TypeArchiveNotification into the four native TypeArchive callback shapes. Each + // method marshals the native arguments into managed objects and forwards to the user's + // virtual override. The core invokes the function pointers stored on the + // BNTypeArchiveNotification struct; this context is rooted by those delegates (see + // TypeArchiveNotificationContext.BuildNative), so it -- and the notification it holds -- + // stays alive for the registration lifetime. + internal sealed class TypeArchiveNotificationContext + { + internal readonly TypeArchiveNotification Notification; + + // The allocated BNTypeArchiveNotification struct (IntPtr to AllocHGlobal memory), kept + // alive until FreeNative so the core-held registration never sees freed memory. + internal IntPtr NativeStruct = IntPtr.Zero; + + // Rooting the four wrapper delegates on this context prevents the GC from reclaiming + // their function pointers while the core holds the registration. + private NativeDelegates.TypeArchiveTypeAddedDelegate? m_added; + private NativeDelegates.TypeArchiveTypeUpdatedDelegate? m_updated; + private NativeDelegates.TypeArchiveTypeRenamedDelegate? m_renamed; + private NativeDelegates.TypeArchiveTypeDeletedDelegate? m_deleted; + + // The GCHandle stored as the struct's context field; freed on unregistration. The + // delegates already root this context, but the handle makes the rooting explicit and + // gives the core a stable context value to pass back. + private GCHandle m_selfHandle; + + internal TypeArchiveNotificationContext(TypeArchiveNotification notification) + { + this.Notification = notification; + } + + // Marshals the native arguments and forwards to the user override. A throwing override + // must not abort the core's notification dispatch, so exceptions are swallowed (Python + // logs and continues). + internal void OnTypeAdded(IntPtr context, IntPtr archive, IntPtr id, IntPtr definition) + { + try + { + TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive); + if (null == archiveObject) + { + return; + } + + string idString = PtrToAnsiString(id); + Type? definitionType = Type.NewFromHandle(definition); + + this.Notification.OnTypeAdded(archiveObject, idString, definitionType); + } + catch (Exception) + { + // Swallowed: a failing callback must not crash the core dispatch. + } + } + + internal void OnTypeUpdated( + IntPtr context, IntPtr archive, IntPtr id, IntPtr oldDefinition, IntPtr newDefinition) + { + try + { + TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive); + if (null == archiveObject) + { + return; + } + + string idString = PtrToAnsiString(id); + Type? oldType = Type.NewFromHandle(oldDefinition); + Type? newType = Type.NewFromHandle(newDefinition); + + this.Notification.OnTypeUpdated(archiveObject, idString, oldType, newType); + } + catch (Exception) + { + // Swallowed: a failing callback must not crash the core dispatch. + } + } + + internal void OnTypeRenamed( + IntPtr context, IntPtr archive, IntPtr id, IntPtr oldName, IntPtr newName) + { + try + { + TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive); + if (null == archiveObject) + { + return; + } + + string idString = PtrToAnsiString(id); + QualifiedName oldNameValue = QualifiedNameFromPtr(oldName); + QualifiedName newNameValue = QualifiedNameFromPtr(newName); + + this.Notification.OnTypeRenamed(archiveObject, idString, oldNameValue, newNameValue); + } + catch (Exception) + { + // Swallowed: a failing callback must not crash the core dispatch. + } + } + + internal void OnTypeDeleted(IntPtr context, IntPtr archive, IntPtr id, IntPtr definition) + { + try + { + TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive); + if (null == archiveObject) + { + return; + } + + string idString = PtrToAnsiString(id); + Type? definitionType = Type.NewFromHandle(definition); + + this.Notification.OnTypeDeleted(archiveObject, idString, definitionType); + } + catch (Exception) + { + // Swallowed: a failing callback must not crash the core dispatch. + } + } + + // Allocates the BNTypeArchiveNotification struct, sets its context and the four function + // pointers (taken from the rooted wrapper delegates), and returns the struct pointer. + // Idempotent: a second call returns the existing struct. + internal IntPtr BuildNative() + { + if (IntPtr.Zero != this.NativeStruct) + { + return this.NativeStruct; + } + + // 1. Create the four wrapper delegates bound to this context and root them as fields. + this.m_added = new NativeDelegates.TypeArchiveTypeAddedDelegate(this.OnTypeAdded); + this.m_updated = new NativeDelegates.TypeArchiveTypeUpdatedDelegate(this.OnTypeUpdated); + this.m_renamed = new NativeDelegates.TypeArchiveTypeRenamedDelegate(this.OnTypeRenamed); + this.m_deleted = new NativeDelegates.TypeArchiveTypeDeletedDelegate(this.OnTypeDeleted); + + // 2. Allocate a stable context handle so the core has a value to pass back, and so + // this context is explicitly rooted. + this.m_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal); + + // 3. Build the struct: context + four function pointers. + BNTypeArchiveNotification native = new BNTypeArchiveNotification(); + native.context = GCHandle.ToIntPtr(this.m_selfHandle); + native.typeAdded = Marshal.GetFunctionPointerForDelegate(this.m_added); + native.typeUpdated = Marshal.GetFunctionPointerForDelegate(this.m_updated); + native.typeRenamed = Marshal.GetFunctionPointerForDelegate(this.m_renamed); + native.typeDeleted = Marshal.GetFunctionPointerForDelegate(this.m_deleted); + + this.NativeStruct = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(native, this.NativeStruct, false); + + return this.NativeStruct; + } + + // Frees the struct and the context handle. Called after the core has unregistered. + internal void FreeNative() + { + if (IntPtr.Zero != this.NativeStruct) + { + Marshal.DestroyStructure(this.NativeStruct); + Marshal.FreeHGlobal(this.NativeStruct); + this.NativeStruct = IntPtr.Zero; + } + + if (this.m_selfHandle.IsAllocated) + { + this.m_selfHandle.Free(); + } + } + + private static string PtrToAnsiString(IntPtr pointer) + { + if (IntPtr.Zero == pointer) + { + return string.Empty; + } + + return Marshal.PtrToStringAnsi(pointer) ?? string.Empty; + } + + // Reads a BNQualifiedName out of a pointer the core passes (the archive owns the name + // storage, so it is read without freeing, matching Python's _from_core_struct). + private static QualifiedName QualifiedNameFromPtr(IntPtr pointer) + { + if (IntPtr.Zero == pointer) + { + return new QualifiedName(string.Empty); + } + + BNQualifiedName native = Marshal.PtrToStructure(pointer); + + return QualifiedName.FromNative(native); + } + } + } +} diff --git a/Handle/BNTypeArchive.cs b/Handle/BNTypeArchive.cs index 7ac6132..e55d102 100644 --- a/Handle/BNTypeArchive.cs +++ b/Handle/BNTypeArchive.cs @@ -12,6 +12,10 @@ namespace BinaryNinja /// public sealed class TypeArchive : AbstractSafeHandle { + // Live notification registrations, keyed by the user's TypeArchiveNotification instance. Each + // entry roots its native callback delegates and struct for the registration lifetime. + private readonly Dictionary + m_notifications = new Dictionary(); /// /// Initializes a new TypeArchive wrapper around an existing native handle. /// @@ -890,5 +894,58 @@ public unsafe QualifiedNameTypeAndId[] GetTypes(string snapshot = "") NativeMethods.BNFreeQualifiedNameTypeAndId ); } + + /// + /// Registers a notification object to receive type-add/update/rename/delete events from this + /// archive, mirroring Python TypeArchive.register_notification (typearchive.py:711). + /// The notification must be unregistered (or this archive released) to stop delivery. + /// + /// The notification whose virtual methods receive the events. + public void RegisterNotification(TypeArchiveNotification notification) + { + if (null == notification) + { + throw new ArgumentNullException(nameof(notification)); + } + + // A notification already registered on this archive is a no-op (Python tolerates this). + if (this.m_notifications.ContainsKey(notification)) + { + return; + } + + // 1. Build the native callback struct (roots the four delegates + the context). + UnsafeUtils.TypeArchiveNotificationContext context = + new UnsafeUtils.TypeArchiveNotificationContext(notification); + IntPtr nativePointer = context.BuildNative(); + + // 2. Register with the core, then keep the context rooted so the delegates outlive the + // registration. + NativeMethods.BNRegisterTypeArchiveNotification(this.handle, nativePointer); + this.m_notifications[notification] = context; + } + + /// + /// Unregisters a previously-registered notification, mirroring Python + /// TypeArchive.unregister_notification (typearchive.py:721). Frees the native callback + /// struct and releases the rooted delegates. + /// + /// The notification to unregister. + public void UnregisterNotification(TypeArchiveNotification notification) + { + if (null == notification) + { + throw new ArgumentNullException(nameof(notification)); + } + + if (!this.m_notifications.TryGetValue(notification, out UnsafeUtils.TypeArchiveNotificationContext? context)) + { + return; + } + + NativeMethods.BNUnregisterTypeArchiveNotification(this.handle, context.NativeStruct); + context.FreeNative(); + this.m_notifications.Remove(notification); + } } } diff --git a/Struct/BNTypeArchiveNotification.cs b/Struct/BNTypeArchiveNotification.cs index 4ab0dc1..fb51a55 100644 --- a/Struct/BNTypeArchiveNotification.cs +++ b/Struct/BNTypeArchiveNotification.cs @@ -34,12 +34,60 @@ internal unsafe struct BNTypeArchiveNotification public IntPtr typeDeleted; } - public class TypeArchiveNotification + /// + /// Base class for receiving event notifications from a , mirroring + /// Python TypeArchiveNotification (typearchive.py:732). Subclass and override the + /// virtual methods for the events of interest, then register an instance via + /// . + /// + public abstract class TypeArchiveNotification { - - public TypeArchiveNotification() - { - - } + /// + /// Called when a type is added to the archive, mirroring Python type_added + /// (typearchive.py:741). + /// + /// The archive the type was added to. + /// The id of the added type. + /// The definition of the added type. + public virtual void OnTypeAdded(TypeArchive archive, string id, Type? definition) + { + } + + /// + /// Called when a type in the archive is updated to a new definition, mirroring Python + /// type_updated (typearchive.py:748). + /// + /// The archive the type belongs to. + /// The id of the updated type. + /// The previous definition. + /// The current definition. + public virtual void OnTypeUpdated( + TypeArchive archive, string id, Type? oldDefinition, Type? newDefinition) + { + } + + /// + /// Called when a type in the archive is renamed, mirroring Python type_renamed + /// (typearchive.py:755). + /// + /// The archive the type belongs to. + /// The id of the renamed type. + /// The previous name. + /// The current name. + public virtual void OnTypeRenamed( + TypeArchive archive, string id, QualifiedName oldName, QualifiedName newName) + { + } + + /// + /// Called when a type is deleted from the archive, mirroring Python type_deleted + /// (typearchive.py:762). + /// + /// The archive the type was deleted from. + /// The id of the deleted type. + /// The definition of the deleted type. + public virtual void OnTypeDeleted(TypeArchive archive, string id, Type? definition) + { + } } } \ No newline at end of file