Jump to content
Alexander Halser

Firemonkey DLL: loading a TBitmap in initialization fails (solved)

Recommended Posts

I need some FMX functionality in a VCL app. An FMX Windows DLL should handle the task. So far, the (FMX) DLL is working, but... 
 
Loading a TBitmap in the initialization of the DLL fails. The host application (actually the DLL) comes up with a general error. Loading the TBitmap at a later point in time (e.g. triggered by the host app) works. Unfortunately, the real DLL is a bit more complex and stripping all the "initialization" from all units is not an option.

Static vs. dynamic loading of the DLL by the host VCL app does not make a difference. I've also tried ShareMem (not convinced it would change anything), but to give it a try: same result.

Stripped down to the core, the entire DLL code is in the screenshot below.

To me, it looks as if the GDI+ or DirectX environment is not loaded, when the DLL initializes. Is this a general limitation or could that be worked around by forcing earlier loading of GDI+/DirectX?

fmx-dll-load-bitmap-in-initialization.png

Edited by Alexander Halser

Share this post


Link to post

@Alexander Halser

 

Correct me if I dont understand your propose...

 

I did this test using a DLL (by default Wizard Delphi...) and 2 projects FMX/VCL... of course, exists many differences between 2 frameworks, than the TBitmap class too!!! initially uncompatible to exchange data directly... then I did this:

  • you're using "PNG" in a VCL Bitmap, I think  that needs a "uses  pngimage = Imaging.pngimage = Vcl.Imaging.pngimage;"

 

library prjDLL;

// for tests, uncomment and build it for each platform:

// {$DEFINE FireMonkeyVersion }
{$UNDEF FireMonkeyVersion }
//
// -----------------------------------------
// {$DEFINE VCL }
// {$UNDEF VCL }

uses
  System.SysUtils,
  System.Classes
{$IFDEF FireMonkeyVersion}
    ,
  FMX.Graphics
{$ELSE}
    ,
  Vcl.Graphics
{$ENDIF};

{$IFDEF FireMonkeyVersion}

function MyLoadBitmap(AFilename: string; var ABmp: TBitmap): boolean;
var
  LFMXBmp: TBitmap;
begin
  result := false;
  //
  LFMXBmp := TBitmap.Create;
  try
    try
      LFMXBmp.LoadFromFile(AFilename);
      //
      // FMX -> bmp.SetSize before bmp.CopyFromBitmap
      ABmp.SetSize(LFMXBmp.Size);
      ABmp.CopyFromBitmap(LFMXBmp);
      //
      result := true;
    except
      // ???
    end;
  finally
    LFMXBmp.Free;
  end;
end;
{$ELSE}

function MyLoadBitmap(AFilename: string; var ABmp: TBitmap): boolean;
begin
  result := false;
  //
  try
    ABmp.LoadFromFile(AFilename);
    //
    result := true;
  except
    // ???
  end;
end;
{$ENDIF}

exports
  MyLoadBitmap;

var
  XBmp: TBitmap;

begin
  // this is called first than function above...
  //
  // just for test to DLL initialization... Vcl/Fmx
  XBmp := TBitmap.Create;
  try
    MyLoadBitmap('D:\RADStudioTests\RADRX113Tests\__TEMP\FMX_DLL\MyBitmap.bmp', XBmp);
  finally
    XBmp.Free;
  end;

end.

 

unit uFMXFormMain;

interface

uses
  System.SysUtils,
  System.Types,
  System.UITypes,
  System.Classes,
  System.Variants,
  Winapi.Windows,
  FMX.Types,
  FMX.Controls,
  FMX.Forms,
  FMX.Graphics,
  FMX.Dialogs,
  FMX.Objects,
  FMX.Controls.Presentation,
  FMX.StdCtrls,
  FMX.Memo.Types,
  FMX.ScrollBox,
  FMX.Memo;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Image1: TImage;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

type
  TMyProcessHeader = function(AFilename: string; var ABmp: TBitmap): boolean;

var
  MyDLLPath: string = 'D:\RADStudioTests\RADRX113Tests\__TEMP\FMX_DLL\DLL\Win32\Debug\prjDLL.dll';
  MyProcess: string = 'MyLoadBitmap';

procedure TForm1.Button1Click(Sender: TObject);
var
  LDLLHandle: NativeUInt;
  LProcess  : TMyProcessHeader;
  LBmp      : TBitmap;
begin
  LDLLHandle := LoadLibrary(PWideChar(MyDLLPath));
  //
  if (LDLLHandle > 0) then
    begin
      try
        LProcess := GetProcAddress(LDLLHandle, PWideChar(MyProcess));
        //
        if (@LProcess <> nil) then
          begin
            LBmp := TBitmap.Create;
            try
              try
                LProcess('D:\RADStudioTests\RADRX113Tests\__TEMP\FMX_DLL\MyBitmap.bmp', LBmp);
              except
                on E: Exception do
                  Memo1.Text := E.Message;
              end;
              //
              if (LBmp <> nil) and not(LBmp.IsEmpty) then
                begin
                  Image1.Bitmap.SetSize(LBmp.Size);
                  Image1.Bitmap.CopyFromBitmap(LBmp);
                end;
            finally
              LBmp.Free;
            end;
          end;
      finally
        FreeLibrary(LDLLHandle);
        LDLLHandle := 0;
      end;
    end;
end;

initialization

ReportMemoryLeaksOnShutdown := true;

end.

 

unit uVCLFormMain;

interface

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

type
  TForm2 = class(TForm)
    Button1: TButton;
    Image1: TImage;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

type
  TMyProcessHeader = function(AFilename: string; var ABmp: TBitmap): boolean;

var
  MyDLLPath: string = 'D:\RADStudioTests\RADRX113Tests\__TEMP\FMX_DLL\DLL\Win32\Debug\prjDLL.dll';
  MyProcess: string = 'MyLoadBitmap';

procedure TForm2.Button1Click(Sender: TObject);
var
  LDLLHandle: NativeUInt;
  LProcess  : TMyProcessHeader;
  LBmp      : TBitmap;
begin
  LDLLHandle := LoadLibrary(PWideChar(MyDLLPath));
  //
  if (LDLLHandle > 0) then
    begin
      try
        LProcess := GetProcAddress(LDLLHandle, PWideChar(MyProcess));
        //
        if (@LProcess <> nil) then
          begin
            LBmp := TBitmap.Create;
            try
              try
                LProcess('D:\RADStudioTests\RADRX113Tests\__TEMP\FMX_DLL\MyBitmap.bmp', LBmp);
                //
                if not(LBmp.Empty) then
                  Image1.Picture.Bitmap.Assign(LBmp);
              except
                on E: Exception do
                  Memo1.Text := E.Message;
              end;
            finally
              LBmp.Free;
            end;
          end;
      finally
        FreeLibrary(LDLLHandle);
        LDLLHandle := 0;
      end;
    end;
end;

initialization

ReportMemoryLeaksOnShutdown := true;

end.

 

Sem título.png

Edited by programmerdelphi2k

Share this post


Link to post

Hello programmerdelphi2k!

I should have been more precise: 

  • Delphi 11.0
  • Win32 (not 64 bit)
  • The DLL is 100% FMX, so TBitmap is an FMX.Graphics.TBitmap
  • The host app is 100% VCL

Host and DLL do not exchange bitmaps, nor other complex data structures. They exchange PChars and function results.

When the host app starts and the DLL loads, I get the message "Load DLL" and after that: "Exception EAccessViolation in Module TestDLL.dll at 0000842F."

 

Host app (VCL 32 bit, 1 form):

unit hostUnit1;

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);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  function LoadImage(Filename: PChar): Boolean; external 'TestDLL.dll';

var
  Form1: TForm1;

implementation
{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  LoadImage('C:\Users\Alexander\Documents\Embarcadero\Studio\Projects\fmx-dll-test\test.png');
end;

end.

DLL Code:

library TestDLL;
uses
  FMX.Forms,
  FMX.Graphics,
  FMX.Dialogs;

{$R *.res}

function LoadImage(Filename: PChar): Boolean;
var
  dummy: FMX.Graphics.TBitmap;
begin
  dummy := FMX.Graphics.TBitmap.Create;
  dummy.LoadFromFile(Filename);
  dummy.Free;
  Showmessage('Image loaded');
end;

exports
  LoadImage;

var
  dummy: FMX.Graphics.TBitmap;
begin
  Showmessage('Load DLL');
  dummy := FMX.Graphics.TBitmap.Create;
//This line included, the DLL fails to load:
//  dummy.LoadFromFile('C:\Users\Alexander\Documents\Embarcadero\Studio\Projects\fmx-dll-test\test.png');  
  dummy.Free;
end.

 

HostApp.zip

Edited by Alexander Halser

Share this post


Link to post

Update, a partial solution, and a question...

 

Observations with an FMX host app + FMX dll:

  • The FMX DLL requires GDI+/DirectX in the intialization section. It creates an FMX.TBitmap and loads an image. To do this, it requires a GDI+/DirectX canvas.
  • Such a DLL cannot be statically linked, fullstop. Not even with an FMX host application, despite the FMX host does initialize GDI+/DirectX.
  • The reason is that statically linked DLLs get loaded before the host app can initialize GDI+/DirectX.
  • Solution for FMX host + FMX dll: link the DLL dynamically, let the host app initialize GDI+/DirectX (as it does by default), then load the DLL.
  • Result: the initialization of the DLL works, because GDI+/DirectX is already available (loaded by the FMX host).

Observations with a VCL host app + FMX dll:

  • It's obvious that GDI+/DirectX must be initialized by the host app before the DLL gets loaded.
  • Dynamic DLL loading is therefore a must.
  • The VCL host does not - at least not by default - initialize GDI+/DirectX, because it does not require it.
  • Question: can we force the VCL host to deliberately load GDI+/DirectX, which is required by the DLL?
  • Answer: yes, we can by including FMX.Graphics into the VCL section. Technically, that works (and the compiler does not complain).

 

VCL host app test unit, modified:

Note, that the VCL app references FMX.Graphics. This adds about 7mb to the EXE and seems to work.

Is this a reliable construct?

Or is there a different way to invoke (just) GDI+/DirectX in the host app, without all the FMX.Graphics overhead?

 

unit VCL_hostUnit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
  FMX.Graphics;  { <--- This works and intializes GDI+/DirectX for the DLL. But will this work reliably? }

type
  TVCLForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  TLoadImage = function(Filename: PChar): Boolean;

var
  VCLForm1: TVCLForm1;
  dllHandle: Cardinal;
  LoadImageFunc: TLoadImage;

implementation

{$R *.dfm}


function LoadDLL: Boolean;
begin
  dllHandle := LoadLibrary('TestDLL.dll') ;
  @LoadImageFunc := GetProcAddress(dllHandle, 'LoadImage') ;
end;

procedure TVCLForm1.Button1Click(Sender: TObject);
begin
  if LoadDLL then
    LoadImageFunc('C:\Users\Alexander\Documents\Embarcadero\Studio\Projects\fmx-dll-test\test.png')
  else
    Showmessage('DLL not loaded');
end;

initialization
  dllHandle := 0;

finalization
  if dllHandle <> 0 then FreeLibrary(dllHandle) ;

end.

 

Share this post


Link to post

I didn't try, but you might succeed with just using Winapi.GDIPOBJ.pas. It calls GdiPlusStartup when not inside a DLL.

  • Like 1

Share this post


Link to post
Si Señor! Winapi.GDIOBJ instead of FMX.Graphics does the trick. And saves a few megabytes.
Thank you very much for the hint, Uwe!
 
To sum up the solution...
 
If an FMX DLL requires drawing capabilities in the initialization (that requirement might be triggered by 3rd party units), then the VCL host application must load GDI+ before loading the DLL:
  1. In the VCL host app, include Winapi.GDIOBJ to init GDI+
  2. Load the FMX DLL dynamically (static linking would cause the DLL to load first, before host can load GDI+)

 

If the host app is FMX and not VCL, it loads GDI+ by default. In this case, point (1) is void, but dynamic loading of the DLL is still required.

Edited by Alexander Halser

Share this post


Link to post

This is another evidence of what Raymond Chen has told in "Some reasons not to do anything scary in your DllMain" articles

 

Share this post


Link to post
On 6/13/2023 at 1:23 PM, Fr0sT.Brutal said:

Some reasons not to do anything scary in your DllMain

You are certainly right. But the real DLL is more complex and involves 3rd party components, which happen to load TBitmaps in the initialization. Quite common, I'd say, and I wouldn't have considered it scary. But obviously, it is :classic_biggrin:

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

×