In a recent comment on this blog Patrick van Logchem suggested a way of extending an existing object instance with custom data at run-time (originally the idea of Thorsten Engler). The main idea is to be able to “assign” to an arbitrary object (whose sources you cannot change) some other object at run-time. This may prove to be useful in different scenarios when you need some additional data to be carried by an object.
The suggested idea was to use the monitor field which all TObject instances have. If you are not familiar with monitors in Delphi, let me explain: All TObject instances carry a new “hidden” field which is a pointer to a TMonitor (see System unit) structure. This referenced TMonitor structure is actually a kind of synchronization object that lets you solve some common threading tasks easier. You can read more about this in other posts, since this post is about using that field to store data.
- The solution must not be too simplistic. Simply rewriting the pointer in TObject instances is not allowed.
- Normal monitor routines must function as they did before. This means that we must store a real monitor there alongside with the data.
- No custom functions to simulate monitor support, and no class helpers. See previous item.
- The attached data must be disposed when the object is disposed so to avoid memory and resource leaks.
- Possibility to extend an object with another object: ExtendObject(Object, Extension);
- Possibility to query an object for an extension: GetObjectExtension(Object): Extension;
- Possibility to remove an extension from an object: RemoveObjectExtension(Object): Extension;
- Object is any type of object in Delphi! No restrictions, no common ancestor; just plain TObject!
- Extension is also simply a TObject value. It’s user-defined in it’s implementation and purpose.
The first implementation I came up with is non-intrusive. I wanted to avoid patching the System exposed functions at run-time. The following list enumerates the design and restrictions of the first implementation:
- The unit must me USED directly after the inclusion of SysUtils in the main source file. This is an inconvenience of course, but it is a required one. Note that the unit must be included AFTER and not BEFORE SysUtils.
- The unit overrides the values in the System.MonitorSupport variable and inserts it’s own custom routines used to obtain and release synchronization objects.
- The unit uses the old System.MonitorSupport routines to do the real job. These routines are normally provided by the SysUtils unit — thus the dependency on SysUtils.
- For each synchronization object requested, my custom routines return a fake handle which is actually a pointer to a structure containing a real handle and a TObject value.
- This method does not use the monitor field per se; rather, it uses a field in the monitor itself.
- Class helpers are used in implementation section to obtain access to internal method in the TMonitor structure.
- While this method is non-intrusive at assembly level, it is surely more complex and uses more CPU cycles.
The second implementation is intrusive! It patches some functions in System unit so that my handler are executed in certain scenarios. The following list enumerates what’s going on in this one:
- The unit should be USED after or before SysUtils. This restriction comes from the fact that monitors initialized before this unit is included have a different format. So there may be (or maybe not) problems.
- Two functions are patched in System unit: TMonitor.Destroy(TObject) and TMonitor.Create(). First one is executed when a monitor is destroyed – normally at object death; and the second one is called when a monitor value is needed for the first time.
- The two injected functions do basically the same thing as the System versions, with a slight turn – a TMonitor + Pointer value is created/destroyed. This bonus Pointer value hold the extension object.
- Class helpers are used internally to gain access to some monitor support routines.
- This method does not incur any overhead on normal monitor operations, so it is the preferred one.
And now to some code:
uses SysUtils, ObjectExtensions_Intrusive; type TStrExtenstion = class end; TIntExtension = class end; procedure DoStuff(const A: TObject); var Extension: TObject; begin if A = nil then Exit; Extension := GetObjectExtension(A); if Extension = nil then Exit; WriteLn(Extension.ClassName); end; var a, b, c: TObject; begin a := TObject.Create; b := TObject.Create; c := TObject.Create; ExtendObject(a, TStrExtenstion.Create); ExtendObject(b, TIntExtension.Create); DoStuff(a); DoStuff(b); DoStuff(c); ReadLn; end.
It’s not hard to imagine that this method can be used is different circumstances – the ideas are all yours!
MEGA WARNING: The attached code is barely tested, possibly unstable and even worse – maybe destructive. This is just a fun and proof of concept code and not something that can be used in applications.
The code can be found in this archive: [download#41]
Mentioned the authors of the idea.