Updated 28 May 2014: Removed extraneous unit reference from AttributeValidation.pas. Sorry…
What problem was I trying to solve?
Recently, while using the Indy Internet components in Delphi XE2, I was struggling to track the thread contexts in which certain code paths ran, to ensure that resource contention and deadlocks were correctly catered for.
Indy components are reasonably robust, but use a multithreaded model which it turns out is difficult to get 100% correct. Component callbacks can occur on many different threads:
- The thread that constructed the component
- The VCL thread
- The server listener thread
- The connection’s thread
- Some, e.g. exceptions, can occur on any thread
Disentangling this, especially when in conjunction with third party solutions that are based on Indy and may add several layers of indirection, quickly becomes an unenjoyable task.
I started adding thread validation assertions to each function to ensure that I was (a) understanding which thread context the function was actually running in, and (b) to ensure that I didn’t call the function in the wrong context myself. However, when browsing the code, it was still very difficult to get a big picture view of thread usage.
Introducing attributes
Enter attributes. Delphi 2010 introduced support for attributes in Win32, and a nice API to query them with extended Run Time Type Information (RTTI). This is nice, except for one thing: it’s difficult at runtime to find the RTTI associated with the current method.
In this unit, I have tried to tick a number of boxes:
- Create a simple framework for extending runtime testing of classes with attributes
- Use attributes to annotate methods, in this case about thread safety, to optimise self-documentation
- Keep a single, consistent function call in each member method, to test any attributes associated with that method.
- Sensible preprocessor use to enable and disable both the testing and full RTTI in one place.
One gotcha is that by default, RTTI for Delphi methods is only available for public and published member methods. This can be changed with the $RTTI
compiler directive but you have to remember to do it in each unit! I have used a unit-based $I
include in order to push the correct RTTI settings consistently.
I’ve made use of Delphi’s class helper model to give direct access to any object at compile time. This is a clean way of injecting this support into all classes which are touched by the RTTI, but does create larger executables. I believe this to be a worthwhile tradeoff.
Example code
The code sample below demonstrates how to use the attribute tests in a multi-threaded context. In this example, an assertion will be raised soon after cmdDoSomeHardWorkClick
is called. Why is this? It happens because the HardWorkCallback
function on the main thread is annotated with [MainThread]
attribute, but it will be called from TSomeThread
‘s thread context, not the main thread.
In order for the program run without an assertion, you could change the annotation of HardWorkCallback
to [NotMainThread]
. Making this serves as an immediate prompt that you should not be accessing VCL properties, because you are no longer running on the main thread. In fact, unless you can prove that the lifetime of the form will exceed that of TSomeThread
, you shouldn’t even be referring to the form. The HardWorkCallback
function here violates these principles by referring to the Handle
property of TForm
. However, because we can show that the form is destroyed after the thread exits, it’s safe to make the callback to the TAttrValForm
object itself.
You can download the full source for this project from the link at the bottom of this post in order to compile it and run it yourself.
Exercise: How could you restructure this to make HardWorkCallback
thread-safe? There’s more than one way to skin this cat.
unit AttrValSample; interface uses System.Classes, System.SyncObjs, System.SysUtils, System.Variants, Vcl.Controls, Vcl.Dialogs, Vcl.Forms, Vcl.Graphics, Vcl.StdCtrls, Winapi.Messages, Winapi.Windows, {$I AttributeValidation.inc}; type TSomeThread = class; TAttrValForm = class(TForm) cmdStartThread: TButton; cmdDoSomeHardWork: TButton; cmdStopThread: TButton; procedure cmdStartThreadClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure cmdStopThreadClick(Sender: TObject); procedure cmdDoSomeHardWorkClick(Sender: TObject); private FThread: TSomeThread; public [MainThread] procedure HardWorkCallback; end; TSomeThread = class(TThread) private FOwner: TAttrValForm; FEvent: TEvent; [NotMainThread] procedure HardWork; protected [NotMainThread] procedure Execute; override; public [MainThread] constructor Create(AOwner: TAttrValForm); [MainThread] destructor Destroy; override; [MainThread] procedure DoSomeHardWork; end; var AttrValForm: TAttrValForm; implementation {$R *.dfm} procedure TAttrValForm.cmdStartThreadClick(Sender: TObject); begin FThread := TSomeThread.Create(Self); cmdDoSomeHardWork.Enabled := True; cmdStopThread.Enabled := True; cmdStartThread.Enabled := False; end; procedure TAttrValForm.cmdDoSomeHardWorkClick(Sender: TObject); begin FThread.DoSomeHardWork; end; procedure TAttrValForm.cmdStopThreadClick(Sender: TObject); begin FreeAndNil(FThread); cmdDoSomeHardWork.Enabled := False; cmdStopThread.Enabled := False; cmdStartThread.Enabled := True; end; procedure TAttrValForm.FormDestroy(Sender: TObject); begin FreeAndNil(FThread); end; procedure TAttrValForm.HardWorkCallback; begin ValidateAttributes; SetWindowText(Handle, 'Hard work done'); end; { TSomeThread } constructor TSomeThread.Create(AOwner: TAttrValForm); begin ValidateAttributes; FEvent := TEvent.Create(nil, False, False, ''); FOwner := AOwner; inherited Create(False); end; destructor TSomeThread.Destroy; begin ValidateAttributes; if not Terminated then begin Terminate; FEvent.SetEvent; WaitFor; end; FreeAndNil(FEvent); inherited Destroy; end; procedure TSomeThread.DoSomeHardWork; begin ValidateAttributes; FEvent.SetEvent; end; procedure TSomeThread.Execute; begin ValidateAttributes; while not Terminated do begin if FEvent.WaitFor = wrSignaled then if not Terminated then HardWork; end; end; procedure TSomeThread.HardWork; begin ValidateAttributes; FOwner.HardWorkCallback; end; end.
The AttributeValidation.inc file referenced in the uses clause above controls RTTI and debug settings, in one line. This pattern makes it easy to use the unit without forgetting to set the appropriate RTTI flags in one unit.
// Disable the following $DEFINE to remove all validation from the project // You may want to do this with {$IFDEF DEBUG} ... {$ENDIF} {$DEFINE ATTRIBUTE_DEBUG} // Shouldn't need to touch anything below here {$IFDEF ATTRIBUTE_DEBUG} {$RTTI EXPLICIT METHODS([vcPrivate,vcProtected,vcPublic,vcPublished])} {$ENDIF} // This .inc file is also included from AttributeValidation.pas, so // don't use it again in that context. {$IFNDEF ATTRIBUTE_DEBUG_UNIT} AttributeValidation {$ENDIF}
Finally, the AttributeValidation.pas file itself contains the assembly stub to capture the return address for the caller, and the search through the RTTI for the appropriate method to test in each case. This will have a performance cost so should really only be present in Debug builds.
unit AttributeValidation; interface {$DEFINE ATTRIBUTE_DEBUG_UNIT} {$I AttributeValidation.inc} uses System.Rtti; type // Base class for all validation attributes ValidationAttribute = class(TCustomAttribute) function Execute(Method: TRTTIMethod): Boolean; virtual; end; // Will log to the debug console whenever a deprecated // function is called DeprecatedAttribute = class(ValidationAttribute) function Execute(Method: TRTTIMethod): Boolean; override; end; // Base class for all thread-related attributes ThreadAttribute = class(ValidationAttribute); // This indicates that the procedure can be called from // any thread. No test to pass, just a bare attribute ThreadSafeAttribute = class(ThreadAttribute); // This indicates that the procedure must only be called // in the context of the main thread MainThreadAttribute = class(ThreadAttribute) function Execute(Method: TRTTIMethod): Boolean; override; end; // This indicates that the procedure must only be called // in another thread context. NotMainThreadAttribute = class(ThreadAttribute) function Execute(Method: TRTTIMethod): Boolean; override; end; TAttributeValidation = class helper for TObject {$IFDEF ATTRIBUTE_DEBUG} private procedure IntValidateAttributes(FReturnAddress: UIntPtr); {$ENDIF} protected procedure ValidateAttributes; end; implementation uses Winapi.Windows, classes; { TAttributeValidation } { Function: TAttributeValidation.ValidateAttributes Description: Save the return address to an accessible variable on the stack. We could do this with pure Delphi and some pointer jiggery-pokery, but this is cleaner. } {$IFNDEF ATTRIBUTE_DEBUG} procedure TAttributeValidation.ValidateAttributes; begin end; {$ELSE} {$IFDEF CPUX64} procedure TAttributeValidation.ValidateAttributes; asm push rbp sub rsp, $20 mov rbp, rsp // rcx = param 1; will already be pointing to Self. mov rdx, [rbp+$28] // rdx = param 2; rbp+$28 is return address on stack call TAttributeValidation.IntValidateAttributes; lea rsp, [rbp+$20] pop rbp end; {$ELSE} procedure TAttributeValidation.ValidateAttributes; asm // eax = Self mov edx, dword ptr [esp] // edx = parameter 1 call TAttributeValidation.IntValidateAttributes end; {$ENDIF} { Function: TAttributeValidation.IntValidateAttributes Description: Find the closest function to the return address, and test the attributes in that function. Assumes that the closest function is the correct one, so if RTTI is missing then you'll be in a spot of bother. } procedure TAttributeValidation.IntValidateAttributes(FReturnAddress: UIntPtr); var FRttiType: TRttiType; FClosestRttiMethod, FRttiMethod: TRTTIMethod; FAttribute: TCustomAttribute; begin with TRttiContext.Create do try FRttiType := GetType(ClassType); if not Assigned(FRttiType) then Exit; FClosestRttiMethod := nil; // Find nearest function for the return address for FRttiMethod in FRttiType.GetMethods do begin if (UIntPtr(FRttiMethod.CodeAddress) <= FReturnAddress) then begin if not Assigned(FClosestRttiMethod) or (UIntPtr(FRttiMethod.CodeAddress) > UIntPtr(FClosestRttiMethod.CodeAddress)) then FClosestRttiMethod := FRttiMethod; end; end; // Check attributes for the function if Assigned(FClosestRttiMethod) then begin for FAttribute in FClosestRttiMethod.GetAttributes do begin if FAttribute is ValidationAttribute then begin if not (FAttribute as ValidationAttribute).Execute(FClosestRttiMethod) then begin Assert(False, 'Attribute '+FAttribute.ClassName+' did not validate on '+FClosestRttiMethod.Name); end; end; end; end; finally Free; end; end; {$ENDIF} { ValidationAttribute } function ValidationAttribute.Execute(Method: TRTTIMethod): Boolean; begin Result := True; end; { MainThreadAttribute } function MainThreadAttribute.Execute(Method: TRTTIMethod): Boolean; begin Result := GetCurrentThreadID = MainThreadID; end; { NotMainThreadAttribute } function NotMainThreadAttribute.Execute(Method: TRTTIMethod): Boolean; begin Result := GetCurrentThreadID <> MainThreadID; end; { DeprecatedAttribute } function DeprecatedAttribute.Execute(Method: TRTTIMethod): Boolean; begin OutputDebugString(PChar(Method.Name + ' was called.'#13#10)); Result := True; end; end.
There you have it — a “real” use case for attributes in Delphi. The key advantages I see to this approach, as opposed to, say function-level assertions, is that a birds-eye view of your class will help you to understand the preconditions for each member function, and these preconditions can be consistently and simply tested.
Using a class helper makes it easy to inject the additional functionality into every class that is touched by attribute validation, without polluting the class hierarchy. This means that attribute tests can be seamlessly added to existing infrastructure and Delphi child classes such as TForm
.
Full source: AttrVal.zip. License: MPL 2.0. YMMV and use at your own risk.