Alexander Halser 26 Posted June 12, 2023 (edited) 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? Edited June 13, 2023 by Alexander Halser Share this post Link to post
programmerdelphi2k 237 Posted June 13, 2023 (edited) @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. Edited June 13, 2023 by programmerdelphi2k Share this post Link to post
Alexander Halser 26 Posted June 13, 2023 (edited) 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 June 13, 2023 by Alexander Halser Share this post Link to post
Alexander Halser 26 Posted June 13, 2023 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
Uwe Raabe 2057 Posted June 13, 2023 I didn't try, but you might succeed with just using Winapi.GDIPOBJ.pas. It calls GdiPlusStartup when not inside a DLL. 1 Share this post Link to post
Alexander Halser 26 Posted June 13, 2023 (edited) 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: In the VCL host app, include Winapi.GDIOBJ to init GDI+ 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 June 13, 2023 by Alexander Halser Share this post Link to post
Fr0sT.Brutal 900 Posted June 13, 2023 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
Alexander Halser 26 Posted June 14, 2023 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 Share this post Link to post