How to make a very small windows service executable

by Iztok Kacin in Coding

I a recent stack overflow question, a user asked how to make a Delphi generated NT Service executable smaller. There was some debate, if Delphi was appropriate to do this and if the executable size it generates was to big. Let me answer those two question as I see them:

  1. The size is not to big. In today desktop environment a megabyte, or few of them don’t matter. The difference between 50K and 1MB executable just doesn’t matter. There is enough RAM and hard drive space, that very few situations need a smaller executable.
  2. And in cases when you need that, you can get to that in Delphi. You don’t need C/C++. Sure you can make the same result there, but why? Even if you only use API calls, you can still reuse some of the low level code you have written in Delphi. And that is a good enough reason to do it in your known IDE and language. You already know your tool and can save a lot of time not writing support code you already have written.

Ok, so I dusted off a very old example I made years back and recompiled it with the Delphi 2006 and Delphi XE compiler. (I have those currently at hand). The result are:

  • Delphi 2006: 526 KB
  • Delphi XE: 1018 KB

These are the result of opening a new service application and just clicking build. No settings were adjusted. Then I cleaned that old example a little removed the unneeded code and made sure it works (I tested the service). The results were the following:

  • Delphi 2006: 22 KB
  • Delphi XE: 32 KB

Here I striped XE version of RTTI as suggested on stack overflow (actually as there is only procedural code the RTTI has no effect whatsoever on the code as noticed by Cris). I left the Delphi 2006 intact. The code I used is posted bellow. It is basically a service skeleton made of two parts. One is the part that installs and controls the service. And the other is the service code itself that spawns a new “service control manager” attached process and then just waits until it is told to stop. It also reports the status to SCM.

And now the challenge. How small can you make a static linked, single c/c++ executable service. It would be fun to know. I will play more with this latter trying different compiler settings and other tricks to see it I can squeeze a byte or two out of my current size :)

A word of advice. Do not just copy and paste the sample service skeleton. It is just a prototype to show what can be done. It is in no way complete and does not have good error coverage. If enough people finds it important I can finish the code and polish it, but otherwise it is not worth the time.

{
  NT Service  model based completely on API calls. Version 0.1
  Inspired by NT service skeleton from Aphex
  Adapted by Runner
}
 
program PureAPIService;
 
{$APPTYPE CONSOLE}
 
uses
  Windows,
  WinSvc;
 
const
  ServiceName     = 'PureAPIService';
  DisplayName     = 'Pure Windows API Service';
  NUM_OF_SERVICES = 2;
 
var
  ServiceStatus : TServiceStatus;
  StatusHandle  : SERVICE_STATUS_HANDLE;
  ServiceTable  : array [0..NUM_OF_SERVICES] of TServiceTableEntry;
  Stopped       : Boolean;
  Paused        : Boolean;
 
var
  ghSvcStopEvent: Cardinal;
 
procedure OnServiceCreate;
begin
  // do your stuff here;
end;
 
procedure AfterUninstall;
begin
  // do your stuff here;
end;
 
procedure ReportSvcStatus(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD);
begin
  // fill in the SERVICE_STATUS structure.
  ServiceStatus.dwCurrentState := dwCurrentState;
  ServiceStatus.dwWin32ExitCode := dwWin32ExitCode;
  ServiceStatus.dwWaitHint := dwWaitHint;
 
  case dwCurrentState of
    SERVICE_START_PENDING: ServiceStatus.dwControlsAccepted := 0;
    else
      ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP;
  end;
 
  case (dwCurrentState = SERVICE_RUNNING) or (dwCurrentState = SERVICE_STOPPED) of
    True: ServiceStatus.dwCheckPoint := 0;
    False: ServiceStatus.dwCheckPoint := 1;
  end;
 
  // Report the status of the service to the SCM.
  SetServiceStatus(StatusHandle, ServiceStatus);
end;
 
procedure MainProc;
begin
  // we have to do something or service will stop
  ghSvcStopEvent := CreateEvent(nil, True, False, nil);
 
  if ghSvcStopEvent = 0 then
  begin
    ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
    Exit;
  end;
 
  // Report running status when initialization is complete.
  ReportSvcStatus( SERVICE_RUNNING, NO_ERROR, 0 );
 
  // Perform work until service stops.
  while True do
  begin
    // Check whether to stop the service.
    WaitForSingleObject(ghSvcStopEvent, INFINITE);
    ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
    Exit;
  end;
end;
 
procedure ServiceCtrlHandler(Control: DWORD); stdcall;
begin
  case Control of
    SERVICE_CONTROL_STOP:
      begin
        Stopped := True;
        SetEvent(ghSvcStopEvent);
        ServiceStatus.dwCurrentState := SERVICE_STOP_PENDING;
        SetServiceStatus(StatusHandle, ServiceStatus);
      end;
    SERVICE_CONTROL_PAUSE:
      begin
        Paused := True;
        ServiceStatus.dwcurrentstate := SERVICE_PAUSED;
        SetServiceStatus(StatusHandle, ServiceStatus);
      end;
    SERVICE_CONTROL_CONTINUE:
      begin
        Paused := False;
        ServiceStatus.dwCurrentState := SERVICE_RUNNING;
        SetServiceStatus(StatusHandle, ServiceStatus);
      end;
    SERVICE_CONTROL_INTERROGATE: SetServiceStatus(StatusHandle, ServiceStatus);
    SERVICE_CONTROL_SHUTDOWN: Stopped := True;
  end;
end;
 
procedure RegisterService(dwArgc: DWORD; var lpszArgv: PChar); stdcall;
begin
  ServiceStatus.dwServiceType := SERVICE_WIN32_OWN_PROCESS;
  ServiceStatus.dwCurrentState := SERVICE_START_PENDING;
  ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP or SERVICE_ACCEPT_PAUSE_CONTINUE;
  ServiceStatus.dwServiceSpecificExitCode := 0;
  ServiceStatus.dwWin32ExitCode := 0;
  ServiceStatus.dwCheckPoint := 0;
  ServiceStatus.dwWaitHint := 0;
 
  StatusHandle := RegisterServiceCtrlHandler(ServiceName, @ServiceCtrlHandler);
 
  if StatusHandle <> 0 then
  begin
    ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
    try
      Stopped := False;
      Paused  := False;
      MainProc;
    finally
      ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
    end;
  end;
end;
 
procedure UninstallService(const ServiceName: PChar; const Silent: Boolean);
const
  cRemoveMsg = 'Your service was removed sucesfuly!';
var
  SCManager: SC_HANDLE;
  Service: SC_HANDLE;
begin
  SCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
  if SCManager = 0 then
    Exit;
  try
    Service := OpenService(SCManager, ServiceName, SERVICE_ALL_ACCESS);
    ControlService(Service, SERVICE_CONTROL_STOP, ServiceStatus);
    DeleteService(Service);
    CloseServiceHandle(Service);
    if not Silent then
      MessageBox(0, cRemoveMsg, ServiceName, MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  finally
    CloseServiceHandle(SCManager);
    AfterUninstall;
  end;
end;
 
procedure InstallService(const ServiceName, DisplayName, LoadOrder: PChar;
  const FileName: string; const Silent: Boolean);
const
  cInstallMsg = 'Your service was Installed sucesfuly!';
  cSCMError = 'Error trying to open SC Manager';
var
  SCMHandle  : SC_HANDLE;
  SvHandle   : SC_HANDLE;
begin
  SCMHandle := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
 
  if SCMHandle = 0 then
  begin
    MessageBox(0, cSCMError, ServiceName, MB_ICONERROR or MB_OK or MB_TASKMODAL or MB_TOPMOST);
    Exit;
  end;
 
  try
    SvHandle := CreateService(SCMHandle,
                              ServiceName,
                              DisplayName,
                              SERVICE_ALL_ACCESS,
                              SERVICE_WIN32_OWN_PROCESS,
                              SERVICE_AUTO_START,
                              SERVICE_ERROR_IGNORE,
                              pchar(FileName),
                              LoadOrder,
                              nil,
                              nil,
                              nil,
                              nil);
    CloseServiceHandle(SvHandle);
 
    if not Silent then
      MessageBox(0, cInstallMsg, ServiceName, MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  finally
    CloseServiceHandle(SCMHandle);
  end;
end;
 
procedure WriteHelpContent;
begin
  WriteLn('To install your service please type  /install');
  WriteLn('To uninstall your service please type  /remove');
  WriteLn('For help please type  /? or /h');
end;
 
begin
  if (ParamStr(1) = '/h') or (ParamStr(1) = '/?') then
    WriteHelpContent
  else if ParamStr(1) = '/install' then
    InstallService(ServiceName, DisplayName, 'System Reserved', ParamStr(0), ParamStr(2) = '/s')
  else if ParamStr(1) = '/remove' then
    UninstallService(ServiceName, ParamStr(2) = '/s')
  else if ParamCount = 0 then
  begin
    OnServiceCreate;
 
    ServiceTable[0].lpServiceName := ServiceName;
    ServiceTable[0].lpServiceProc := @RegisterService;
    ServiceTable[1].lpServiceName := nil;
    ServiceTable[1].lpServiceProc := nil;
 
    StartServiceCtrlDispatcher(ServiceTable[0]);
  end
  else
    WriteLn('Wrong argument!');
end.

Contact on the blog

by Iztok Kacin in Coding

If you tried to contact me via the contact page in the last few days, I was not reachable. The reason is, that I changed my gmail account password and forgot to change it in wordpress SMTP plugin. So if you did not receive a response, please try again and sorry for the inconvenience.

New version of SimpleStorage and other updates

by Iztok Kacin in Coding

Quite a lot of new code got piled up so without further ado here are the news.

(You can get all new releases from the download page)

SimpleStorage

This a new major release (1.7.3) of SimpleStorage. And sadly also a breaking one. At least if you were writing your own filters and adapters. There was no other way around if I wanted to write a good and clean code. All changes that were made revolve around filters and adapters. Here is the list of important changes

  • Filters were extended to the document level. This means you can now compress and encrypt XML nodes or whole document in a transparent manner.
  • Chaining was introduced to filters. This means you can now encrypt and compress a document in a single step or if you like, in a single line of code :)
  • Encryption and compression are now integral part of SimpleStorage and no third party components are needed anymore.
  • Adapters and Filter now use cleaner approach, so no more strings as identifiers, to specify which filter or adapter you want.

Let me give you some examples:

Compressing and the encrypting a whole document.

NormalXML.Filter(ZLIB).Filter(XTEA('MyStrongKey')).SaveToFile(FilteredXMLFileTwo);

The same but using a document fiter chain interface.

DocumentFilterChain := CreateDocumentFilterChain;
DocumentFilterChain.AddFilter(CompressedStorage);
DocumentFilterChain.AddFilter(EncryptedStorage('MyStrongKey'));
NormalXML := DocumentFilterChain.LoadFromFile(FilteredXMLFileOne);
NormalXML.SaveToFile(NormalXMLFileOne);

Compressing using the compressed storage interface.

CompressedStorage.SaveToFile(DecompressedXML, CompressedXMLFileOne);

Compressing using the build in ZLIB filter.

DecompressedXML.Filter(ZLIB).SaveToFile(CompressedXMLFileTwo);

Do you see how simple it is to compress and encrypt a XML document. The other change is in the adapter code. There are no more strings to specify which adapter to use. It looks like this:

  SS.Ensure('MemTable').Adapter(DataSet).Load(ClientDS);

and

  SS.Get('MemTable').Adapter(DataSet).Save(ClientDS);

There are other smaller changes, but this are the major ones. Just a warning here. If you were writing adapters or filters (your own) then this is a breaking release and you will have to update your code to the new specifications. The examples that come withe the library are clear enough that it should be a breeze. For normal user nothing changes and the code will stay the same. I also left the old version of SimpleStorage available for download if people don’t want to upgrade right away.

Also I would like to announce that I will begin writing basic documentation for SimpleStorage.

ISAPIServer

This is a new component in the library. It is a standalone ISAPI server capable of processing Delphi ISAPI modules. You don’t need Apache or IIS. And it is build in the way, that you can use your favorite HTTP library to use it. It comes with already made bindings for Indy, but it would be equally simple to do bindings for Synapse or ICS. This is possible because it is designed in a very modular way. If any of you are interested in that, you can contact me and I can help with it. I am posting the whole ISAPI request code for Indy as an example:

procedure TfMain.HTTPServerCommandGet({$IFDEF Indy9}
                                        AThread: TIdPeerThread;
                                      {$ELSE}
                                        AContext: TIdContext;
                                      {$ENDIF}
                                      ARequestInfo: TIdHTTPRequestInfo;
                                      AResponseInfo: TIdHTTPResponseInfo);
var
  ECB: TECBData;
  Port: string;
  TempStr: string;
  PathStr: string;
  RootDir: string;
  DDLFileName: string;
begin
  TempStr := Copy(ARequestInfo.Document, 2, Length(ARequestInfo.Document));
  RootDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
  PathStr := '/' + StrAfter('/', TempStr);
  DDLFileName := StrBefore('/', TempStr);
 
  {$IFDEF Indy9}
    Port := IntToStr(AThread.Connection.Socket.Binding.Port);
  {$ELSE}
    Port := IntToStr(AContext.Binding.Port);
  {$ENDIF}
 
  ECB := ECBDataList.AcquireNewECB;
  try
    FillECBFromRequest(ECB, HTTPServer.KeepAlive, ARequestInfo, RootDir, DDLFileName, Port, PathStr);
    try
      FISAPIServer.Execute(DDLFileName, ECB);
    finally
      FillResponseFromECB(ECB, AResponseInfo);
    end;
  finally
    ECBDataList.DeleteECB(ECB.ECB.ConnID);
  end;
end;

RoboMailer

This is also a new component. It is intended as a simplified mass mailer component. It can also be used to send a single mail easily with no worries on how to set everything up correctly. It comes with a demo application, which can send mass mails. You can personify each mail with the data you provide.  Maybe it will be of use to somebody. It uses ICS as an engine.

Threading unit

This unit got two interesting additions. First one is a lock free stack based on the windows API. No home made code here, just a wrapper around windows API. And the second one is a thread safe queue. It uses critical sections but tries to lock as little as possible. I have done some tests and it is very fast. In fact it is almost as fast as a lock free one, but the code is much much simpler. There is very little difference in speed in fact it is negligible in real world problems.

ChangleLog:

  • 1.4.0
    • Added TLockFreeStack based on Windows SLISTS
    • Added TThreadSafeQueue based on linked lists
  • 1.3.7
    • Added ShutdownTimeout (INFINITE by default)
    • Wait for all tasks to finish then shutting down the task pool

CRON Scheduler

Change Log:

  • 2.0.3
    • added HasValue function for TChronEntry

SimpleLog

Change Log:

  • 1.2.0
    • Use the new rewritten Cromis.Exceptions
    • Log file limit is now working correctly

XTEA

Change Log:

  • 1.1.0
    • Added overloaded procedures for stream and file encryption / decryption

Your passwords security on the Internet

by Iztok Kacin in Security

Before I begin to write about what the title implies, a clarification is in place. Why I haven’t blogged for so long. It has been months since I wrote anything. I do not like it, but the truth is I was extremely busy. Family, start up company, regular job, occasional projects and trying to study for an MA degree really dried up all my free time. Actually I had no time left for anything else. So blog had to wait. I have a lot to blog about, so I promise I will make up for this with a series of posts on different topics.

Ok enough about this, lets get to the topic at hand. Internet is today something we almost cannot live without anymore.  Informations,  social networks, blogs, software etc…, it has a lot to offer. So we spend a lot of our time on the Internet doing various activities. And among those is also creating accounts and identities for ourselves. And because, most of the time this activities are of private nature, we need to protect them, lock the doors, or in case of Internet, protect them with passwords. Your mail account, facebook account, twitter account, online backups, forums, blogs etc.. they are all protected with passwords. They all contain more or less vital information, that you do not want to share with the rest of the world.

But here interesting things begin to happen. We humans are practical beings, trying to always minimize the amount of work involved with each task we take. It is the same with creating passwords. We frequently create passwords that are weak, because we want to easily memorize them. You don’t believe me? Check your password strength and then think about it. Weak passwords are volnurable, even if the attacker gets only the hash of such a password. Rainbow tables and dictionary attacks are easy to implement and can quickly lead to identity and data theft, if a password hash database is compromised. Furthermore, a lot of people only use one or two passwords for most of the sites they log on to. The more sites you log on to, the bigger the statistical probability that one of those will be compromised. So if we combine those two facts we get a huge risk, most of us take on the Internet each day. Even a malicious javascript can steal your password on some untrusted site you visited.

That is exactly what Jeff Atwood blogged about in his last post The Dirty Truth About Web Passwords. He showed, what can happen if a large site like Gawker Media is compromised. They got owned completely and the attackers got hold on a large database of hashes. And because the site used a week encryption and people used week passwords, the attackers easily got hold of the real user passwords. And naturally most of the users used the same password for most of their sites. You just try Gmail for example. It is a high probability that a person will have an account there. (or Facebook) You can even make automated boots to scan most popular sites. Jeff offered (and propagates) OpenID as an alternative so you only use your password on one single spot and then authenticate through that trusted source. Well I see OpenID as a good alternative ,but there are better solutions out there, so I do not agree with Jeff on this one. What is wrong with OpenID?

  1. You still leave you password somewhere (we can call it master password because it controls your logons).
  2. If this single place is compromised (and it is possible of course) all of your sites and accounts are also compromised.
  3. It is not so easy to use, that every grandma will use it. It is ok for technical people out there. But this is not enough.
  4. It redirects you from the current page in most cases and is hence a distraction and not a natural way to do authentication.
  5. A lot of sites do not support OpenID.

We have quite a few drawbacks. The question that arises is, can we do better that OpenID. And the answer, as you probably guessed, is yes we can. The best protection is to have a unique and strong password for each site you log on to. Ok that is a perfect scenario, if one site is compromised, the others are in no danger at all. But how do you remember all those passwords (and strong ones please). You could have a local evidence on your computer or let the browser remember them for you. But all such solutions are far, far from perfect, or even acceptable.

That is why this ingenious solution exists. It is called SuperGenPass. What it does is very simple, yet very powerful, like most good solutions. It takes you master password and the URL of a site you are trying to logon to and creates a unique salted hash for that site (if you have a master password “grandma” and you want to logon to google, it makes a hash from something like “grandma:google”). The hash is a strong password and can be very long, so it is very, very hard to brake (almost impossible). And the best thing is ,you don’t need to remember the password. You can regenerate it every time you need it. And that is exactly what SuperGenPass does. It makes this process very simple for the user. You don’t have to store your master password anywhere. Just make sure that the master password is strong enough.

Is this to good to be true. No not really, it is a real thing. The only possible weakness is that because the solution works as a script that manipulates DOM of the page, a malicious script can be written to capture your master password, while you type it in on some untrusted site. But this can be avoided in several ways.

  1. Use the mobile standalone version of the SuperGenPass to avoid this
  2. Do not type master password on the sites you do not trust
  3. Use browser plugin that locally encapsulates the SuperGenPass. One such plugin for Chrome exists.
  4. Use NoScript plugin for Firefox

The NoScript is another gem out there. It basically prevents execution of all scripting content on every page and notifies you about blocked scripts. Then you can confirm the sites you trust with a single click and allow the scripts to execute. The NoScript then remembers that sites. Not a lot of extra work for a big increase in security. The number of possible browser attacks this prevents is huge. This way your master password cannot be stolen on sites you do not trust.

So think again on how you manage your passwords on the Internet and consider my suggestion as a possible solution to the problem. I am also open for discussion, on even better solutions if they exists.  Act now and do not be sorry later.

TValue and other “Variants” like implementation tests

by Iztok Kacin in Coding

The recent post from Mason Wheeler about TValue speed was very interesting. I always had the impression, that variants were slow, but never tested that. So this was a surprise to me. But how fast the variants really are? There are other “variants” like implementations out there, so I decided to test them in a common test. At least the ones I know that exist. The record implicit operator allows for some very flexible operations on different data types. So I tested 5 different approaches to “variant” implementation.

  • Variants (build in Delphi as long as I can remember :) )
  • TValue (new in Delphi 2010)
  • TAnyValue (recently added to my library)
  • TOmniValue (part of OmniThreadLibrary)
  • Bare-bone variant record implementation for comparison.

Here is the code I tested on. It is just a little changed version of Masons. It forces the compiler to include all lines (by calling Writeln on resulting values) and it averages over integers, strings and floating point values. Just integers seemed unfair to me. After all in real life we would use all possible combinations of data. The test also has range and overflow checks turned on.

The results:

Iterations: 10000000

Times are in milliseconds

Type Variants TValue TAnyValue TOmniValue TVariableRec
j := I 255 4070 916 128 175
j := I/5 332 4081 1400 3265 313
j := IntToStr(I) 4548 11051 7873 6429 2795
ALL 4905 18847 9932 9654 3255

And here is the test code:

program ValuesTest;
 
{$APPTYPE CONSOLE}
uses
  SysUtils,
  rtti,
  diagnostics,
  OtlCommon,
  Cromis.AnyValue;
 
const
  HUNDRED_MILLION = 10000000;
 
type
  TVariableRec = record
  private
    FFLoat: Extended;
    FString: string;
    FInteger: Integer;
    function GetAsFloat: Extended;
    procedure SetAsFloat(const Value: Extended);
    function GetAsInteger: Integer;
    procedure SetAsInteger(const Value: Integer);
    function GetAsString: string;
    procedure SetAsString(const Value: string);
  public
    class operator Implicit(const Value: string): TVariableRec;
    class operator Implicit(const Value: Integer): TVariableRec;
    class operator Implicit(const Value: Extended): TVariableRec;
    class operator Implicit(const Value: TVariableRec): string;
    class operator Implicit(const Value: TVariableRec): Integer;
    class operator Implicit(const Value: TVariableRec): Extended;
    property AsFloat: Extended read GetAsFloat write SetAsFloat;
    property AsString: string read GetAsString write SetAsString;
    property AsInteger: Integer read GetAsInteger write SetAsInteger;
  end;
 
procedure tryTValue;
var
  i: integer;
  j: TValue;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsExtended;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryAnyValue;
var
  i: integer;
  j: TAnyValue;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsFloat;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryOmniValue;
var
  i: integer;
  j: TOmniValue;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsExtended;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryVariants;
var
  i: integer;
  j: variant;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j;
    j := I / 5;
    value2 := j;
    j := IntToStr(I);
    value3 := j;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryVarRec;
var
  i: integer;
  j: TVariableRec;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsFloat;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
var
  stopwatch: TStopWatch;
 
{ TVariableRec }
 
class operator TVariableRec.Implicit(const Value: Extended): TVariableRec;
begin
  Result.AsFloat := Value;
end;
 
function TVariableRec.GetAsFloat: Extended;
begin
  Result := FFLoat;
end;
 
function TVariableRec.GetAsInteger: Integer;
begin
  Result := FInteger;
end;
 
function TVariableRec.GetAsString: string;
begin
  Result := FString;
end;
 
class operator TVariableRec.Implicit(const Value: Integer): TVariableRec;
begin
  Result.AsInteger := Value;
end;
 
class operator TVariableRec.Implicit(const Value: TVariableRec): Integer;
begin
  Result := Value.AsInteger;
end;
 
class operator TVariableRec.Implicit(const Value: TVariableRec): Extended;
begin
  Result := Value.AsFloat;
end;
 
class operator TVariableRec.Implicit(const Value: string): TVariableRec;
begin
  Result.AsString := Value;
end;
 
class operator TVariableRec.Implicit(const Value: TVariableRec): string;
begin
  Result := Value.AsString;
end;
 
procedure TVariableRec.SetAsFloat(const Value: Extended);
begin
  FFLoat := Value;
end;
 
procedure TVariableRec.SetAsInteger(const Value: Integer);
begin
  FInteger := Value;
end;
 
procedure TVariableRec.SetAsString(const Value: string);
begin
  FString := Value;
end;
 
begin
  try
    stopwatch := TStopWatch.StartNew;
    tryVariants;
    stopwatch.Stop;
    writeln('Variants: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryTValue;
    stopwatch.Stop;
    writeln('TValue: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryAnyValue;
    stopwatch.Stop;
    writeln('TAnyValue: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryOmniValue;
    stopwatch.Stop;
    writeln('TOmniValue: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryVarRec;
    stopwatch.Stop;
    writeln('TVariableRec: ', stopwatch.ElapsedMilliseconds);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
 
  readln;
end.

It is obvious and expected that bare-bone record is the fastest. But Variants are not far behind and in real world the difference is negligible. My TAnyValue and TOmniValue from Primož are on the same level. But in most cases his implementation is faster because I use an interface to store the data in a class (because of my other code that uses interface based IAnyValue) and he stores most of the data as numeric type (only some as interface based class). I could speed up my implementation but I find it fast enough in most cases. And I will take elegance over speed if the difference is small enough. But TValue is slow, to slow probably to be used a lot. They should work on it in the future. It is a shame to throw it away since it brings quite a lot of power with it. I wonder if it is necessary for internal data to be stored as generics array

For those of you who don’t like to use variants or want so more flexibility you can look for TAnyValue (which was inspired upon looking at TOmniValue implementation) or TOmniValue. They both work under older version of Delphi as far as I know (Delphi 2005 and up).

If I missed something or if you know of another “variant” like implementation, please let me know. The purpose of this post is to explore this area as well as possible.