Search code examples
multithreadingdelphidllsynchronizationdelphi-7

Dynamically initialize and call LoadLibrary from a TThread by demand only once


I have a Delphi DLL, which needs to be called from my main UI application or worker threads.

I do not want to call LoadLibrary/FreeLibrary each time I call the DLL. But, I also don't want to load it in my application initialization section. because I might not use the DLL at all during the lifetime of the application.

So what I need is the first caller (thread or main UI) to initialize and load the DLL. the DLL will be unloaded in the finalization section. I realize I need some synchronization. so I have used a critical section BUT I can't seem to make it work.

Only one thread should attempt and load the DLL. if it fails other threads should not attempt to load the DLL again and again.

The synchronization is not working as expected!
Can someone suggest why?

MCVE:

program Project1;
{$APPTYPE CONSOLE}
uses
  Windows,
  SysUtils,
  Classes;

const
  MyDLL = 'MyDLL.dll';

type
  TDLLProcessProc = function(A: Integer): Integer; stdcall;

var
  DLLProc: TDLLProcessProc = nil;
  DLLModule: HMODULE = 0;
  DLLInitialized: Boolean = False;
  DLLInitialized_OK: Boolean = False;
  CS: TRTLCriticalSection;

procedure InitDLLByFirstCall;
begin
  if DLLModule = 0 then
  begin
    if DLLInitialized then Exit;
    EnterCriticalSection(CS);
    try
      if DLLInitialized then Exit;
      DLLInitialized := True;
      DLLModule := LoadLibrary(MyDLL);
      if DLLModule = 0 then RaiseLastWin32Error;
      DLLProc := GetProcAddress(DLLModule, 'Process');
      if @DLLProc = nil then RaiseLastWin32Error;
      DLLInitialized_OK := True;
    finally
      LeaveCriticalSection(CS);
    end;
  end;
end;

function DLLProcess(A: Integer): Integer;
begin
  InitDLLByFirstCall;
  if not DLLInitialized_OK then
    raise Exception.Create('DLL was not initialized OK');
  Result := DLLProc(A);
end;

type
  TDLLThread = class(TThread)
  private
    FNum: Integer;
  public
    constructor Create(CreateSuspended: Boolean; ANum: Integer);
    procedure Execute; override;
  end;

constructor TDLLThread.Create(CreateSuspended: Boolean; ANum: Integer);
begin
  FreeOnTerminate := True;
  FNum := ANum;
  inherited Create(CreateSuspended);
end;

procedure TDLLThread.Execute;
var
  RetValue: Integer;
begin
  try
    RetValue := DLLProcess(FNum);
    Sleep(0);
    Writeln('TDLLThread Result=> ' + IntToStr(RetValue));
  except
    on E: Exception do
    begin
      Writeln('TDLLThread Error: ' + E.Message);
    end;
  end;
end;

var
  I: Integer;

begin
  InitializeCriticalSection(CS);
  try
    // First 10 thread always fail!  
    for I := 1 to 10 do
      TDLLThread.Create(False, I);
    Readln;

    for I := 1 to 10 do
      TDLLThread.Create(False, I);
    Readln;
  finally
    DeleteCriticalSection(CS);
  end;
end. 

DLL:

library MyDLL;

uses
  Windows;

{$R *.res}        

function Process(A: Integer): Integer; stdcall;
begin
  Result := A;
end;

exports
  Process;

begin
  IsMultiThread := True;
end.

Solution

  • You need to modify your code in a way that the condition variable that is checked at the start of InitDLLByFirstCall is set only after all initialization has been completed. The DLL handle is therefore a bad choice.

    Second you need to use the same condition variable outside and inside of the critical section - if you use DLLInitialized for that, then there is not really a use for either DLLInitialized_OK nor DLLModule.

    And to make things easier to reason about you should try to get away with the minimum number of variables. Something like the following should work:

    var
      DLLProc: TDLLProcessProc = nil;
      DLLInitialized: Boolean = False;
      CS: TRTLCriticalSection;
    
    procedure InitDLLByFirstCall;
    var
      DLLModule: HMODULE;
    begin
      if DLLInitialized then
        Exit;
    
      EnterCriticalSection(CS);
      try
        if not DLLInitialized then
        try
          DLLModule := LoadLibrary(MyDLL);
          Win32Check(DLLModule <> 0);
    
          DLLProc := GetProcAddress(DLLModule, 'Process');
          Win32Check(Assigned(DLLProc));
        finally
          DLLInitialized := True;
        end;
      finally
        LeaveCriticalSection(CS);
      end;
    end;
    
    function DLLProcess(A: Integer): Integer;
    begin
      InitDLLByFirstCall;
      if @DLLProc = nil then
        raise Exception.Create('DLL was not initialized OK');
      Result := DLLProc(A);
    end;
    

    If you don't want to check for the function address inside of DLLProcess then you could also use an integer or enumeration for the DLLInitialized variable, with different values for not initialized, failed and success.