Jump to content
Sign in to follow this  
dummzeuch

Exception.CreateFmt vs. CreateResFmt

Recommended Posts

Posted (edited)

The base Exception class introduces various constructors. If you use ressources strings, there are two options:

 

raise Exception.CreateFmt(MY_RES_STRING, [parameters, go, here]);

raise Exception.CreateResFmt(@MY_RES_STRING, [parameters, go, here]);

The second form is the one used everywhere in the RTL/VCL. It actually accepts a PResStringRec parameter.

Up to now I also used that form in some of my internal libraries.

 

But when I enabled the "Typed @ operator" compiler option today to get the compiler to find some errors in my code, it turned out that this form doesn't compile any more (in Delphi 2007, haven't tried anything else).

 

Given that the first form works fine, why would I want to use the second form anyway?

Edited by dummzeuch

Share this post


Link to post

We are using this. Passing a pointer to a ResourceString instead of the (already translated) ResourceStrings allows us to show localized error messages to the user while using English messages when logging the exceptions to disk.

Share this post


Link to post

Constant resourcestring references are resolved during initialization by System._InitResStrings.

If you change the application language after that, e.g. by calling LoadResourceModule, then these references will not be updated.

 

In the example below the first ShowMessage in unaffected by the change of language while the second one isn't.

resourcestring
  sFoo = 'Foo';
  sBar = 'Bar';

const
  sFooBar: array[boolean] of string = (sFoo, sBar);
  sBetterFooBar: array[boolean] of PResStringRec = (@sFoo, @sBar);
  
begin
  LoadResourceModule('foofoo.bar');
  ShowMessage(sFooBar[True]);
  ShowMessage(LoadResString(sBetterFooBar[True]));
end;

 

Apart from that I suspect the RTL/VCL uses PResStringRec for C++ compatibility.

  • Thanks 1

Share this post


Link to post

I should have mentioned we are using the good old GnuGetText / dxGetText for localization. It seems to do some magic to swap out the method for resolving resource strings from System.pas with something else. I can only say that "It just works".

Share this post


Link to post
1 hour ago, Der schöne Günther said:

It seems to do some magic to swap out the method for resolving resource strings from System.pas with something else.

That's fairly easy and you don't need to hack the system unit. You just need to patch the LoadStringW import.

 

For example the following patches FindResourceW and a few others to implement a DFM fallback mechanism that loads from the main module in case the resource module doesn't contain a given DFM resource.

It can easily be modified to patch LoadStringW to load resourcestrings from a dictionary or whatever.

 

unit amLocalization.FindResourceFallback;

interface

function EnableResourceLoadingFallback: Boolean;

implementation

uses
  Windows;

function HookAPI(const Name, Module: string; Hook: pointer): pointer;
var
  ImageBase, Old: Cardinal;
  PEHeader: PImageNtHeaders;
  PImport: PImageImportDescriptor;
  PRVA_Import: LPDWORD;
  ProcAddress: Pointer;
begin
  Result := nil;

  ImageBase := GetModuleHandle(NIL);
  PEHeader := Pointer(Int64(ImageBase) + PImageDosHeader(ImageBase)._lfanew);
  // pointer to the imports table of the main process:
  PImport := Pointer(PEHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + ImageBase);

  // pointer to the WinAPI function we want to hook:
  ProcAddress := GetProcAddress(GetModuleHandle(PChar(Module)), PChar(Name));
  if ProcAddress = NIL then
    Exit;

  while PImport.Name <> 0 do
  begin
    PRVA_Import := LPDWORD(pImport.FirstThunk + ImageBase);
    while PRVA_Import^ <> 0 do
    begin
      if PPointer(PRVA_Import)^ = ProcAddress then
      begin
        // initially imports table is in read-only segment:
        if not VirtualProtect(PPointer(PRVA_Import), 4, PAGE_READWRITE, Old) then
          Exit;

        Result := PPointer(PRVA_Import)^;

        // replacing import address with our own:
        PPointer(PRVA_Import)^ := Hook;

        // restoring old memory protection mode:
        if not VirtualProtect(PPointer(PRVA_Import), 4, Old, Old) then
        begin
          Result := nil;
          Exit;
        end;

      end;

      Inc(PRVA_Import);
    end;

    Inc(PImport);
  end;
end;


type
  TFindResourceW = function(hModule: HMODULE; lpName, lpType: PWideChar): HRSRC; stdcall;
var
  FindResourceW: TFindResourceW = nil;

function HookedFindResourceW(hModule: HMODULE; lpName, lpType: PWideChar): HRSRC; stdcall;
begin
  Result := FindResourceW(hModule, lpName, lpType);

  if (Result = 0) and (hModule <> hInstance) then
    Result := FindResourceW(hInstance, lpName, lpType);
end;

type
  TLoadResource = function(hModule: HINST; hResInfo: HRSRC): HGLOBAL; stdcall;
var
  LoadResource: TLoadResource = nil;

function HookedLoadResource(hModule: HINST; hResInfo: HRSRC): HGLOBAL; stdcall;
begin
  Result := LoadResource(hModule, hResInfo);

  if (Result = 0) and (hModule <> hInstance) then
    Result := LoadResource(hInstance, hResInfo);
end;


type
  TSizeofResource = function(hModule: HINST; hResInfo: HRSRC): DWORD; stdcall;

var
  SizeofResource: TSizeofResource = nil;

function HookedSizeofResource(hModule: HINST; hResInfo: HRSRC): DWORD; stdcall;
begin
  Result := SizeofResource(hModule, hResInfo);

  if (Result = 0) and (hModule <> hInstance) then
    Result := SizeofResource(hInstance, hResInfo);
end;

function EnableResourceLoadingFallback: Boolean;
begin
  FindResourceW := HookAPI('FindResourceW', kernel32, @HookedFindResourceW);
  Result := Assigned(FindResourceW);

  if (Result) then
  begin
    LoadResource := HookAPI('LoadResource', kernel32, @HookedLoadResource);
    Result := Assigned(LoadResource);
  end;

  if (Result) then
  begin
    SizeofResource := HookAPI('SizeofResource', kernel32, @HookedSizeofResource);
    Result := Assigned(SizeofResource);
  end;
end;

initialization
  EnableResourceLoadingFallback;
finalization
end.

 

Share this post


Link to post

"fairly easy" 🙂

In VirtualProtect calls, hard-coded value "4" - isn't it better to use sizeof(pointer) instead, so that it is safe in 64-bits too? Or this code is for 32-bits only?

Share this post


Link to post
1 hour ago, Vandrovnik said:

isn't it better to use sizeof(pointer) instead

Yes.

I've only used this on 32-bit and I don't know if the structures are the same for 64-bit.

Share this post


Link to post

One reason for using the CreateRes version over the normal Create is to avoid all of the additional code that the compiler generates to load the resource string before passing it to Create.

 

An example to help demonstrate (compiled w/ 10.3.3 in Win32-Release configuration)

procedure DoExceptRes;
var
  lRes: Boolean;
begin
  lRes := DoSomething;
  if not lRes then
    raise Exception.CreateRes(@MY_RES_STRING);
end;

004EA78B 90               nop 
TestMain.pas.117: lRes := DoSomething;
004EA78C E8F7FFFFFF       call DoSomething
TestMain.pas.118: if not lRes then
004EA791 84C0             test al,al
004EA793 7516             jnz +22     ; $004ea7ab
TestMain.pas.119: raise Exception.CreateRes(@MY_RES_STRING);
004EA795 B9B4A04E00       mov ecx,$004ea0b4
004EA79A B201             mov dl,$01
004EA79C A13C9B4100       mov eax,[$00419b3c]
004EA7A1 E886BFF3FF       call Exception.CreateRes
004EA7A6 E8D1F9F1FF       call @RaiseExcept
TestMain.pas.120: end;
004EA7AB C3               ret 
procedure DoExcept;
var
  lRes: Boolean;
begin
  lRes := DoSomething;
  if not lRes then
    raise Exception.Create(MY_RES_STRING);
end;

TestMain.pas.125: begin
004EA7AC 55               push ebp
004EA7AD 8BEC             mov ebp,esp
004EA7AF 6A00             push $00
004EA7B1 33C0             xor eax,eax
004EA7B3 55               push ebp
004EA7B4 68FFA74E00       push $004ea7ff
004EA7B9 64FF30           push dword ptr fs:[eax]
004EA7BC 648920           mov fs:[eax],esp
TestMain.pas.126: lRes := DoSomething;
004EA7BF E8C4FFFFFF       call DoSomething
TestMain.pas.127: if not lRes then
004EA7C4 84C0             test al,al
004EA7C6 7521             jnz +33     ; $004ea7e9
TestMain.pas.128: raise Exception.Create(MY_RES_STRING);
004EA7C8 8D55FC           lea edx,[ebp-$04]
004EA7CB B8B4A04E00       mov eax,$004ea0b4
004EA7D0 E8F755F2FF       call LoadResString
004EA7D5 8B4DFC           mov ecx,[ebp-$04]
004EA7D8 B201             mov dl,$01
004EA7DA A13C9B4100       mov eax,[$00419b3c]
004EA7DF E814BEF3FF       call Exception.Create
004EA7E4 E893F9F1FF       call @RaiseExcept
TestMain.pas.129: end;
004EA7E9 33C0             xor eax,eax
004EA7EB 5A               pop edx
004EA7EC 59               pop ecx
004EA7ED 59               pop ecx
004EA7EE 648910           mov fs:[eax],edx
004EA7F1 6806A84E00       push $004ea806
004EA7F6 8D45FC           lea eax,[ebp-$04]
004EA7F9 E88202F2FF       call @UStrClr
004EA7FE C3               ret 
004EA7FF E998F8F1FF       jmp @HandleFinally
004EA804 EBF0             jmp -16     ; $004ea7f6
004EA806 59               pop ecx
004EA807 5D               pop ebp
004EA808 C3               ret 

Note how in the case where we just call Create instead of CreateRes, Delphi needs to generate a call to LoadResString within the context of our procedure in order to actually turn the resourcestring into a value that can be passed into Exception.Create. This isn't really an issue though because it only happens if we're actually going to raise the exception, and CreateRes will ultimately need to do the same thing internally. The issue comes from the fact that Delphi needs somewhere to put the string it gets back from LoadResString before passing it to Exception.Create. To that end it generate an implicit local string variable, and because strings are managed, and local variable have a scope covering the entire procedure lifetime, it wraps the entire body of our procedure into an implicit try...finally to ensure the string is cleaned up. As a result, we will incur the cost of this try...finally scaffold and the call to UStrClr every time we call DoExcept. Even if it never actually raises an Exception.

A similar thing applies to the use of CreateFmt vs Create(Format(...)). The compiler will generate an implicit variable to store the result of the call to Format, and you will incur this overhead regardless of whether the actual call to Format ever happens.

 

Whether or not this actually makes a measurable real world difference will depend on your use case, but it is worth being aware of.

  • Like 1
  • Thanks 1

Share this post


Link to post
15 hours ago, spbogie said:

As a result, we will incur the cost of this try...finally scaffold and the call to UStrClr every time we call DoExcept. Even if it never actually raises an Exception.

A similar thing applies to the use of CreateFmt vs Create(Format(...)).

Noticed this hidden catch too so turned all my routines to use CreateFmt. Btw, the same goes for string concatenation

raise Exception.Create('Error: '+ErrorText);

 

  • Like 1

Share this post


Link to post
Posted (edited)
15 hours ago, Fr0sT.Brutal said:

Noticed this hidden catch too so turned all my routines to use CreateFmt. Btw, the same goes for string concatenation

That is why for performance critical code such code should be moved to an extra (or nested) routine. Otherwise you get pro- and epilogue polluted with code that is not necessary in the majority of cases.

 

Additional read: https://www.delphitools.info/2009/05/06/code-optimization-go-for-the-jugular/

Edited by Stefan Glienke

Share this post


Link to post
6 hours ago, Stefan Glienke said:

That is why for performance critical code such code should be moved to an extra (or nested) routine. Otherwise you get pro- and epilogue polluted with code that is not necessary in the majority of cases.

Yep, I know 😉 For my projects this fact pushed me to change most of in-place string concatenations to formatting of constant literals which additionally is good preparation for easy localization in future.

Share this post


Link to post
14 hours ago, Stefan Glienke said:

Otherwise you get pro- and epilogue polluted with code that is not necessary in the majority of cases.

At least it is not as performance damaging as preconditions. I remember the Indy project changed all the "if condition then raise" to something like "EIdException.RaiseIf(condition, ExceptionString-Concatenation)" in 2003/2004 and changed all that back because of the performance hit.

 

And even in Java with a JIT-Compiler you shouldn't use preconditions if you don't know the effect it has: Somebody changed all the exception handling in a Base64 decoder to preconditions and the result was that converting a 500 MB file took hours instead of milliseconds. The garbage collector had to clean up all the strings that were created for every byte that was read and there were a lot of preconditions in the inner most loop.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×