I'm working on a project in RAD Studio 2007, using VCL classes in c++.
TDBLookupControl is part of VCL & has some undesirable behaviour, which is caused by use of an internal variable SearchTickCount
var
SearchTickCount: Integer = 0; //file scope in DBCtrls.pas
procedure TDBLookupControl.ProcessSearchKey(Key: Char);
var
TickCount: Integer;
S: string;
begin
//some code removed for brevity
TickCount := GetTickCount;
if TickCount - SearchTickCount > 2000 then SearchText := '';
SearchTickCount := TickCount;
//some code removed for brevity
end;
However, SearchTickCount has never been PACKAGEd inside the VCL, as in example below.
extern PACKAGE int SearchTickCount;
I'd like to set SearchTickCount to zero (on demand) in my c++ code.
Externing it in my code makes the c++ compile. However, the linker (obviously) cannot find the variable.
namespace Dbctrls
{
extern int SearchTickCount;
}
// later on, inside a function
Dbctrls::SearchTickCount = 0;
Is there any way/workaround to link to this variable?
EDIT: Unfortunately, we're also using some custom controls that derive from TDBLookupControl so I was tring to avoid creating more custom controls.
SearchTickCount is a global (unit level) variable declared in the implementation section of the unit, it's not supposed to be accessed outside that unit. You'd have the same problem if you were working with Delphi, not C++ Builder.
TDBLookupControl, override ProcessSearchKey() and make sure it uses your own SearchTickCount, one that's easily accessible. Happily ProcessSearchKey() is virtual, in theory this should work, but in practice the code depends on FListField, that's a private field so we'll be back to square 1.TDBLookupControl to your own TMyDBLookupControl and make sure you can access the SearchTickCount. This would definitively work.Ofcourse, hacks are a lot more fun. The CPU has no problem finding the SearchTickCount because the address is coded into the ASM instructions that make up ProcessSearchKey's code. What the CPU can read, we can read.
Evaluating the code for the ProcessSearchKey method, it only uses one global variable (SearchTickCount) and it uses it in two places. First in this test:
if TickCount - SearchTickCount > 2000 then
then in this instruction:
SearchTickCount := TickCount;
If you look into the disassembly listing of that routine, global variable access is easily spotted, because it gives the address of the variable in square brackets, with no other qualifier. For the if to work the compiler does something like this:
SUB EAX, [$000000]
For the assignment, the compiler does something like this:
MOV [$000000], EAX // or ESI on Delphi 7 with debug enabled
If you look at the left of the assembler instruction, you can easily see the actual opcode, in HEX notation. For example the SUB EAX, [$000000] looks like this:
2B0500000000
My hacky solution exploits this. I get the address of the actual procedure (TDBLookupControl.ProcessSearchKey), scan the code looking for the opcode (2B 05) and grab the address. That's it, and it works.
Of course, this has potential problems. It depends on the code being compiled with those exact registers (EAX in my example). The compiler is free to choose different registers. I tested with both Delphi7 and Delphi 2010, with code compiled for Debug and compiled without Debug. In all 4 cases the compiler chose to use EAX for the SUB instruction, and in 3/4 cases chose to use ESI as the register for the MOV instruction. Because of that my code only looks for the SUB instruction.
On the other hand if the code works once, the code works every time. Code doesn't change once released, so if you can properly test on the development machine, you will not get nasty AV's at client's machine. But use at your own risk, this is, after all, a hack!
Here's the code:
unit Unit2;
interface
uses DbCtrls;
function GetSearchTickCountPointer: PInteger;
implementation
type
THackDbLookupControl = class(TDBLookupControl); // Hack to get address of protected member
TInstructionHack = packed record
OpCodePrefix: Word;
OpCodeAddress: PInteger;
end;
PInstructionHack = ^TInstructionHack;
function GetSearchTickCountPointer: PInteger;
var P: PInstructionHack;
N: Integer;
begin
P := @THackDbLookupControl.ProcessSearchKey;
N := 0; // Sentinel counter, so we don't look for the opcode for ever
while N < 2000 do
begin
if P.OpCodePrefix = $052B then // Looking for SUB EAX, [SearchTickCount]
begin
Result := P.OpCodeAddress;
Exit;
end;
Inc(N);
P := PInstructionHack(Cardinal(P)+1); // Move pointer 1 byte
end;
Result := nil;
end;
end.
You use the hacky version like this:
var P: PInteger;
begin
P := GetSearchTickCountPointer;
if Assigned(P) then
P^ := 1; // change SearchTickCount value!
end;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With