Skip to content

Commit 4d3d121

Browse files
authored
feat: TypeArchive type-add/update/rename/delete notifications (#39)
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<BNQualifiedName>+FromNative), and swallows exceptions so a throwing override cannot abort core dispatch.
1 parent 634af83 commit 4d3d121

3 files changed

Lines changed: 335 additions & 6 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace BinaryNinja
5+
{
6+
internal static partial class NativeDelegates
7+
{
8+
// void (*typeAdded)(void* ctxt, BNTypeArchive* archive, const char* id, BNType* definition)
9+
public delegate void TypeArchiveTypeAddedDelegate(
10+
IntPtr context, IntPtr archive, IntPtr id, IntPtr definition);
11+
12+
// void (*typeUpdated)(void* ctxt, BNTypeArchive* archive, const char* id, BNType* oldDefinition, BNType* newDefinition)
13+
public delegate void TypeArchiveTypeUpdatedDelegate(
14+
IntPtr context, IntPtr archive, IntPtr id, IntPtr oldDefinition, IntPtr newDefinition);
15+
16+
// void (*typeRenamed)(void* ctxt, BNTypeArchive* archive, const char* id, BNQualifiedName* oldName, BNQualifiedName* newName)
17+
public delegate void TypeArchiveTypeRenamedDelegate(
18+
IntPtr context, IntPtr archive, IntPtr id, IntPtr oldName, IntPtr newName);
19+
20+
// void (*typeDeleted)(void* ctxt, BNTypeArchive* archive, const char* id, BNType* definition)
21+
public delegate void TypeArchiveTypeDeletedDelegate(
22+
IntPtr context, IntPtr archive, IntPtr id, IntPtr definition);
23+
}
24+
25+
internal static partial class UnsafeUtils
26+
{
27+
// Adapts a TypeArchiveNotification into the four native TypeArchive callback shapes. Each
28+
// method marshals the native arguments into managed objects and forwards to the user's
29+
// virtual override. The core invokes the function pointers stored on the
30+
// BNTypeArchiveNotification struct; this context is rooted by those delegates (see
31+
// TypeArchiveNotificationContext.BuildNative), so it -- and the notification it holds --
32+
// stays alive for the registration lifetime.
33+
internal sealed class TypeArchiveNotificationContext
34+
{
35+
internal readonly TypeArchiveNotification Notification;
36+
37+
// The allocated BNTypeArchiveNotification struct (IntPtr to AllocHGlobal memory), kept
38+
// alive until FreeNative so the core-held registration never sees freed memory.
39+
internal IntPtr NativeStruct = IntPtr.Zero;
40+
41+
// Rooting the four wrapper delegates on this context prevents the GC from reclaiming
42+
// their function pointers while the core holds the registration.
43+
private NativeDelegates.TypeArchiveTypeAddedDelegate? m_added;
44+
private NativeDelegates.TypeArchiveTypeUpdatedDelegate? m_updated;
45+
private NativeDelegates.TypeArchiveTypeRenamedDelegate? m_renamed;
46+
private NativeDelegates.TypeArchiveTypeDeletedDelegate? m_deleted;
47+
48+
// The GCHandle stored as the struct's context field; freed on unregistration. The
49+
// delegates already root this context, but the handle makes the rooting explicit and
50+
// gives the core a stable context value to pass back.
51+
private GCHandle m_selfHandle;
52+
53+
internal TypeArchiveNotificationContext(TypeArchiveNotification notification)
54+
{
55+
this.Notification = notification;
56+
}
57+
58+
// Marshals the native arguments and forwards to the user override. A throwing override
59+
// must not abort the core's notification dispatch, so exceptions are swallowed (Python
60+
// logs and continues).
61+
internal void OnTypeAdded(IntPtr context, IntPtr archive, IntPtr id, IntPtr definition)
62+
{
63+
try
64+
{
65+
TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive);
66+
if (null == archiveObject)
67+
{
68+
return;
69+
}
70+
71+
string idString = PtrToAnsiString(id);
72+
Type? definitionType = Type.NewFromHandle(definition);
73+
74+
this.Notification.OnTypeAdded(archiveObject, idString, definitionType);
75+
}
76+
catch (Exception)
77+
{
78+
// Swallowed: a failing callback must not crash the core dispatch.
79+
}
80+
}
81+
82+
internal void OnTypeUpdated(
83+
IntPtr context, IntPtr archive, IntPtr id, IntPtr oldDefinition, IntPtr newDefinition)
84+
{
85+
try
86+
{
87+
TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive);
88+
if (null == archiveObject)
89+
{
90+
return;
91+
}
92+
93+
string idString = PtrToAnsiString(id);
94+
Type? oldType = Type.NewFromHandle(oldDefinition);
95+
Type? newType = Type.NewFromHandle(newDefinition);
96+
97+
this.Notification.OnTypeUpdated(archiveObject, idString, oldType, newType);
98+
}
99+
catch (Exception)
100+
{
101+
// Swallowed: a failing callback must not crash the core dispatch.
102+
}
103+
}
104+
105+
internal void OnTypeRenamed(
106+
IntPtr context, IntPtr archive, IntPtr id, IntPtr oldName, IntPtr newName)
107+
{
108+
try
109+
{
110+
TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive);
111+
if (null == archiveObject)
112+
{
113+
return;
114+
}
115+
116+
string idString = PtrToAnsiString(id);
117+
QualifiedName oldNameValue = QualifiedNameFromPtr(oldName);
118+
QualifiedName newNameValue = QualifiedNameFromPtr(newName);
119+
120+
this.Notification.OnTypeRenamed(archiveObject, idString, oldNameValue, newNameValue);
121+
}
122+
catch (Exception)
123+
{
124+
// Swallowed: a failing callback must not crash the core dispatch.
125+
}
126+
}
127+
128+
internal void OnTypeDeleted(IntPtr context, IntPtr archive, IntPtr id, IntPtr definition)
129+
{
130+
try
131+
{
132+
TypeArchive? archiveObject = TypeArchive.BorrowHandle(archive);
133+
if (null == archiveObject)
134+
{
135+
return;
136+
}
137+
138+
string idString = PtrToAnsiString(id);
139+
Type? definitionType = Type.NewFromHandle(definition);
140+
141+
this.Notification.OnTypeDeleted(archiveObject, idString, definitionType);
142+
}
143+
catch (Exception)
144+
{
145+
// Swallowed: a failing callback must not crash the core dispatch.
146+
}
147+
}
148+
149+
// Allocates the BNTypeArchiveNotification struct, sets its context and the four function
150+
// pointers (taken from the rooted wrapper delegates), and returns the struct pointer.
151+
// Idempotent: a second call returns the existing struct.
152+
internal IntPtr BuildNative()
153+
{
154+
if (IntPtr.Zero != this.NativeStruct)
155+
{
156+
return this.NativeStruct;
157+
}
158+
159+
// 1. Create the four wrapper delegates bound to this context and root them as fields.
160+
this.m_added = new NativeDelegates.TypeArchiveTypeAddedDelegate(this.OnTypeAdded);
161+
this.m_updated = new NativeDelegates.TypeArchiveTypeUpdatedDelegate(this.OnTypeUpdated);
162+
this.m_renamed = new NativeDelegates.TypeArchiveTypeRenamedDelegate(this.OnTypeRenamed);
163+
this.m_deleted = new NativeDelegates.TypeArchiveTypeDeletedDelegate(this.OnTypeDeleted);
164+
165+
// 2. Allocate a stable context handle so the core has a value to pass back, and so
166+
// this context is explicitly rooted.
167+
this.m_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);
168+
169+
// 3. Build the struct: context + four function pointers.
170+
BNTypeArchiveNotification native = new BNTypeArchiveNotification();
171+
native.context = GCHandle.ToIntPtr(this.m_selfHandle);
172+
native.typeAdded = Marshal.GetFunctionPointerForDelegate(this.m_added);
173+
native.typeUpdated = Marshal.GetFunctionPointerForDelegate(this.m_updated);
174+
native.typeRenamed = Marshal.GetFunctionPointerForDelegate(this.m_renamed);
175+
native.typeDeleted = Marshal.GetFunctionPointerForDelegate(this.m_deleted);
176+
177+
this.NativeStruct = Marshal.AllocHGlobal(Marshal.SizeOf<BNTypeArchiveNotification>());
178+
Marshal.StructureToPtr<BNTypeArchiveNotification>(native, this.NativeStruct, false);
179+
180+
return this.NativeStruct;
181+
}
182+
183+
// Frees the struct and the context handle. Called after the core has unregistered.
184+
internal void FreeNative()
185+
{
186+
if (IntPtr.Zero != this.NativeStruct)
187+
{
188+
Marshal.DestroyStructure<BNTypeArchiveNotification>(this.NativeStruct);
189+
Marshal.FreeHGlobal(this.NativeStruct);
190+
this.NativeStruct = IntPtr.Zero;
191+
}
192+
193+
if (this.m_selfHandle.IsAllocated)
194+
{
195+
this.m_selfHandle.Free();
196+
}
197+
}
198+
199+
private static string PtrToAnsiString(IntPtr pointer)
200+
{
201+
if (IntPtr.Zero == pointer)
202+
{
203+
return string.Empty;
204+
}
205+
206+
return Marshal.PtrToStringAnsi(pointer) ?? string.Empty;
207+
}
208+
209+
// Reads a BNQualifiedName out of a pointer the core passes (the archive owns the name
210+
// storage, so it is read without freeing, matching Python's _from_core_struct).
211+
private static QualifiedName QualifiedNameFromPtr(IntPtr pointer)
212+
{
213+
if (IntPtr.Zero == pointer)
214+
{
215+
return new QualifiedName(string.Empty);
216+
}
217+
218+
BNQualifiedName native = Marshal.PtrToStructure<BNQualifiedName>(pointer);
219+
220+
return QualifiedName.FromNative(native);
221+
}
222+
}
223+
}
224+
}

‎Handle/BNTypeArchive.cs‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ namespace BinaryNinja
1212
/// </summary>
1313
public sealed class TypeArchive : AbstractSafeHandle<TypeArchive>
1414
{
15+
// Live notification registrations, keyed by the user's TypeArchiveNotification instance. Each
16+
// entry roots its native callback delegates and struct for the registration lifetime.
17+
private readonly Dictionary<TypeArchiveNotification, UnsafeUtils.TypeArchiveNotificationContext>
18+
m_notifications = new Dictionary<TypeArchiveNotification, UnsafeUtils.TypeArchiveNotificationContext>();
1519
/// <summary>
1620
/// Initializes a new TypeArchive wrapper around an existing native handle.
1721
/// </summary>
@@ -890,5 +894,58 @@ public unsafe QualifiedNameTypeAndId[] GetTypes(string snapshot = "")
890894
NativeMethods.BNFreeQualifiedNameTypeAndId
891895
);
892896
}
897+
898+
/// <summary>
899+
/// Registers a notification object to receive type-add/update/rename/delete events from this
900+
/// archive, mirroring Python <c>TypeArchive.register_notification</c> (typearchive.py:711).
901+
/// The notification must be unregistered (or this archive released) to stop delivery.
902+
/// </summary>
903+
/// <param name="notification">The notification whose virtual methods receive the events.</param>
904+
public void RegisterNotification(TypeArchiveNotification notification)
905+
{
906+
if (null == notification)
907+
{
908+
throw new ArgumentNullException(nameof(notification));
909+
}
910+
911+
// A notification already registered on this archive is a no-op (Python tolerates this).
912+
if (this.m_notifications.ContainsKey(notification))
913+
{
914+
return;
915+
}
916+
917+
// 1. Build the native callback struct (roots the four delegates + the context).
918+
UnsafeUtils.TypeArchiveNotificationContext context =
919+
new UnsafeUtils.TypeArchiveNotificationContext(notification);
920+
IntPtr nativePointer = context.BuildNative();
921+
922+
// 2. Register with the core, then keep the context rooted so the delegates outlive the
923+
// registration.
924+
NativeMethods.BNRegisterTypeArchiveNotification(this.handle, nativePointer);
925+
this.m_notifications[notification] = context;
926+
}
927+
928+
/// <summary>
929+
/// Unregisters a previously-registered notification, mirroring Python
930+
/// <c>TypeArchive.unregister_notification</c> (typearchive.py:721). Frees the native callback
931+
/// struct and releases the rooted delegates.
932+
/// </summary>
933+
/// <param name="notification">The notification to unregister.</param>
934+
public void UnregisterNotification(TypeArchiveNotification notification)
935+
{
936+
if (null == notification)
937+
{
938+
throw new ArgumentNullException(nameof(notification));
939+
}
940+
941+
if (!this.m_notifications.TryGetValue(notification, out UnsafeUtils.TypeArchiveNotificationContext? context))
942+
{
943+
return;
944+
}
945+
946+
NativeMethods.BNUnregisterTypeArchiveNotification(this.handle, context.NativeStruct);
947+
context.FreeNative();
948+
this.m_notifications.Remove(notification);
949+
}
893950
}
894951
}

‎Struct/BNTypeArchiveNotification.cs‎

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,60 @@ internal unsafe struct BNTypeArchiveNotification
3434
public IntPtr typeDeleted;
3535
}
3636

37-
public class TypeArchiveNotification
37+
/// <summary>
38+
/// Base class for receiving event notifications from a <see cref="TypeArchive"/>, mirroring
39+
/// Python <c>TypeArchiveNotification</c> (typearchive.py:732). Subclass and override the
40+
/// virtual methods for the events of interest, then register an instance via
41+
/// <see cref="TypeArchive.RegisterNotification"/>.
42+
/// </summary>
43+
public abstract class TypeArchiveNotification
3844
{
39-
40-
public TypeArchiveNotification()
41-
{
42-
43-
}
45+
/// <summary>
46+
/// Called when a type is added to the archive, mirroring Python <c>type_added</c>
47+
/// (typearchive.py:741).
48+
/// </summary>
49+
/// <param name="archive">The archive the type was added to.</param>
50+
/// <param name="id">The id of the added type.</param>
51+
/// <param name="definition">The definition of the added type.</param>
52+
public virtual void OnTypeAdded(TypeArchive archive, string id, Type? definition)
53+
{
54+
}
55+
56+
/// <summary>
57+
/// Called when a type in the archive is updated to a new definition, mirroring Python
58+
/// <c>type_updated</c> (typearchive.py:748).
59+
/// </summary>
60+
/// <param name="archive">The archive the type belongs to.</param>
61+
/// <param name="id">The id of the updated type.</param>
62+
/// <param name="oldDefinition">The previous definition.</param>
63+
/// <param name="newDefinition">The current definition.</param>
64+
public virtual void OnTypeUpdated(
65+
TypeArchive archive, string id, Type? oldDefinition, Type? newDefinition)
66+
{
67+
}
68+
69+
/// <summary>
70+
/// Called when a type in the archive is renamed, mirroring Python <c>type_renamed</c>
71+
/// (typearchive.py:755).
72+
/// </summary>
73+
/// <param name="archive">The archive the type belongs to.</param>
74+
/// <param name="id">The id of the renamed type.</param>
75+
/// <param name="oldName">The previous name.</param>
76+
/// <param name="newName">The current name.</param>
77+
public virtual void OnTypeRenamed(
78+
TypeArchive archive, string id, QualifiedName oldName, QualifiedName newName)
79+
{
80+
}
81+
82+
/// <summary>
83+
/// Called when a type is deleted from the archive, mirroring Python <c>type_deleted</c>
84+
/// (typearchive.py:762).
85+
/// </summary>
86+
/// <param name="archive">The archive the type was deleted from.</param>
87+
/// <param name="id">The id of the deleted type.</param>
88+
/// <param name="definition">The definition of the deleted type.</param>
89+
public virtual void OnTypeDeleted(TypeArchive archive, string id, Type? definition)
90+
{
91+
}
4492
}
4593
}

0 commit comments

Comments
 (0)