3

We have a bit of an issue here. We have upgraded from Delphi 2006 to Delphi XE2 and are in the process of converting our code.

The problem is, we use the value -693594 through our application and database records to represent no date (zero date). In Delphi 2006 the FormatDateTime function would correctly format this as 00/00/0000 (given a date format of dd/mm/yyyy).

However in Delphi XE2 they have added a call to ValidateTimeStampDate in the DateTImeToTimeStamp function in System.SysUtils which raises the error "invalid floating point operation". passing anything greater than -693594, such as -693593, works fine.

Has anyone else had this issue and/or does anyone know a work around?

There is no spoon
  • 1,775
  • 2
  • 22
  • 53
  • I may be wrong, but didn't mean "passing anything less than -693594"? also, this works in XE, how about copy-paste the d2006 function into the project "utils" and always add that as the last unit in "uses" clause, in this way the compiler will use THAT in stead of the default one. –  Feb 13 '12 at 05:06
  • 4
    Write your own function that wraps the rtl function and detect the sentinel value in that wrapper – David Heffernan Feb 13 '12 at 07:11
  • @David Heffernan: +1! No spoon-fed code – menjaraz Feb 13 '12 at 11:18
  • Hi David. That sounds like a good idea. But how do I do that? sorry but I have no experience in that area. – There is no spoon Feb 13 '12 at 19:40
  • Sorry david I thought you mean to somehow hook the rtl function and call it from within a wrapper function that detects and handles that value. We have written an alternative function which we are using but FormatDateTime is also used throughout the VCL so I'm concerned that this problem will happend inside VCL controls when passing -693594 to them. Is there a way to intercept FormatDateTime and provide an alternative? Or how can I modify FormatDateTime and rebuild the RTL? – There is no spoon Feb 13 '12 at 20:07
  • Ouch. I would certainly not hook such a function. I would stop using such sentinel values, or else, always use the wrapper function. – Warren P Mar 07 '12 at 16:06

1 Answers1

3

If you are really desperate to patch back to the previous behaviour you could use something like this:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
  OldProtect: DWORD;
begin
  if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
  begin
    Move(NewCode, Address^, Size);
    FlushInstructionCache(GetCurrentProcess, Address, Size);
    VirtualProtect(Address, Size, OldProtect, @OldProtect);
  end;
end;

type
  PInstruction = ^TInstruction;
  TInstruction = packed record
    Opcode: Byte;
    Offset: Integer;
  end;

procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
  NewCode: TInstruction;
begin
  NewCode.Opcode := $E9;//jump relative
  NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
  PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;

function DateTimeToTimeStamp(DateTime: TDateTime): TTimeStamp;
const
  FMSecsPerDay: Single = MSecsPerDay;
  IMSecsPerDay: Integer = MSecsPerDay;
var
  LTemp, LTemp2: Int64;
begin
  LTemp := Round(DateTime * FMSecsPerDay);
  LTemp2 := (LTemp div IMSecsPerDay);
  Result.Date := DateDelta + LTemp2;
  Result.Time := Abs(LTemp) mod IMSecsPerDay;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage(FormatDateTime('dd/mm/yyyy', -693594));
end;

initialization
  RedirectProcedure(@System.SysUtils.DateTimeToTimeStamp, @DateTimeToTimeStamp);

end.

This will work for 32 bit code. It will also work for 64 bit code provided that both the old and new functions reside in the same executable module. Otherwise the jump distance may exceed the range of a 32 bit integer. It will also not work if your RTL resides in a runtime package. Both of these limitations can be readily remedied.

What this code does is re-route all calls to SysUtils.DateTimeToTimeStamp to the version implemented in this unit. The code in this unit is just the PUREPASCAL version from the XE2 source.

The only other approach that meets the needs outlined in your comments is to modify and re-compile the SysUtils unit itself, but I personally avoid that sort of solution.

enter image description here

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks for that. I found an open source project uallCollection that has a method called HookCode which does the same as what you have described but I don't know if it works under 64bit. The project is dead now from what I heard so I had to modify it to get it to compile under Delphi XE2. Anyway thanks for the code. – There is no spoon Feb 14 '12 at 00:01
  • Would you mind explaining how the code works as I would really like to know. The RedirectMethod calculates a offset, the offset for what are we actually finding here? Would this work for most RTL functions or just FormatDateTime? – There is no spoon Feb 14 '12 at 00:15
  • yeah, there's nothing original here! – David Heffernan Feb 14 '12 at 00:16
  • How it works? It replaces the code at the beginning of the function at `OldAddress` with an unconditional jump (`JMP`) to the code at `NewAddress`. The code at `NewAddress` must have the exact same set of parameters and calling convention as that at `OldAddress`. This approach will work for any function and can be made to work for instance methods too. You can hook anything with this. All that said it's a very primitive and a good hooking library will offer much more functionality. – David Heffernan Feb 14 '12 at 00:21
  • Thanks for that, I understand it now. We are simply replacing the first 5 bytes of the old function with a relative jump instruction which will jump the distance between the end byte of TInstruction in the old function and the first byte of the new function resulting in the new function being called. – There is no spoon Feb 14 '12 at 09:11
  • Sorry I have one more question. In the case where the old function is overloaded, how do specify which one to replace with the new opcode? – There is no spoon Feb 14 '12 at 09:27
  • You can declare a variable to hold a typed function pointer. Make that function pointer have the same signature as the target function. Then assign the target function to that function pointer variable and the compiler will pick the one that matches. – David Heffernan Feb 14 '12 at 09:44