Spring4D Dependency Injection in Delphi with dynamic library

133 Views Asked by At

Spring4D seems to do a very good job for Dependency injection in Delphi. I only started to experiment with it recently and things were working fine until I tried to use it in a dynamic library.

The main application registers a class via an interface and then gets an instance from the container to call a method. The library tries to get an instance and call the method too.

The interface is as follows:

type
  IOutputHandler = interface ['{B6C24A62-971F-4D44-85E5-61D1EFC09469}']
    procedure WriteMessage(const AMessage: string);
  end;

The implementation is:

type
  TOutputHandler = class(TInterfacedObject, IOutputHandler)
    procedure WriteMessage(const AMessage: string);
  end;

implementation

procedure TOutputHandler.WriteMessage(const AMessage: string);
begin
  WriteLn(AMessage);
end;

The dynamic library code is:

var
  LocalContainer: TContainer;

procedure InitDI(const AContainer: TContainer);
begin
  Guard.CheckNotNull(Acontainer, 'AContainer');
  LocalContainer := AContainer;
end;

procedure WriteMessage(const AMessage: string);
begin
  var service := LocalContainer.Resolve<IOutputHandler>();
  service.WriteMessage(AMessage);
end;

exports
  InitDI,
  WriteMessage;

The application code is:

type
  TInitDIProc = procedure(const AContainer: TContainer);
  TWriteMessageProc = procedure(const AMessage: string);
...
    var container := GlobalContainer();
    container.RegisterType<IOutputHandler, TOutputHandler>();
    container.Build();

    var service := container.Resolve<IOutputHandler>();
    service.WriteMessage('Message from main application');

    var handle := SafeLoadLibrary('DynamicLibrary.dll');
    var initDI := TInitDIProc(GetProcAddress(handle, 'InitDI'));
    var writeMessage := TWriteMessageProc(GetProcAddress(handle, 'WriteMessage'));
    initDI(container);
    writeMessage('Message from dynamic library');

The problem comes in LocalContainer.Resolve<IOutputHandler>(), where an exception is thrown: EResolveException: Cannot resolve type: IOutputHandler. The reason is most likely that TypeInfo(IOutputHandler) in the application differs from TypeInfo(IOutputHandler) in the DLL.

Is there a way to fix this and make DI work with DLLs? I can send the code for the entire solution if needed.

1

There are 1 best solutions below

2
Nicko On

The solution is based on passing an interface to the DLL, which is used to resolve the references. This interface is defined as:

type
  IDiResolver = interface
    function Resolve(ATypeInfo: PTypeInfo): IUnknown;
  end;

The class implementing it in the main applciation (EXE) is as follows:

type
  TDiResolver = class(TInterfacedObject, IDiResolver)
  private
    FContainer: TContainer;

    function FindServiceTypeByName(ATypeInfo: PTypeInfo): PTypeInfo;
    function Resolve(ATypeInfo: PTypeInfo): IUnknown;

    public
      constructor Create(const AContainer: TContainer);
  end;

implementation

uses
  System.Generics.Collections,
  Spring,
  Spring.Container.Core;

constructor TDiResolver.Create(const AContainer: TContainer);
begin
  Guard.CheckNotNull(AContainer, 'AContainer');

  inherited Create();
  Self.FContainer := AContainer;
end;

function TDiResolver.Resolve(ATypeInfo: PTypeInfo): IUnknown;
begin
  var serviceTypeInfo := Self.FindServiceTypeByName(ATypeInfo);
  if (serviceTypeInfo <> nil) then
  begin
    var value := Self.FContainer.Resolve(serviceTypeInfo, []);
    Exit(value.AsInterface);
  end;

  Result := nil;
end;

function TDiResolver.FindServiceTypeByName(ATypeInfo: PTypeInfo): PTypeInfo;
begin
  var components := Self.FContainer.Kernel.Registry.FindAll();
  for var component: TComponentModel in components do
  begin
    var serviceEntry := component.Services.FirstOrDefault(
      function(const Entry: TPair<string, PTypeInfo>): Boolean
      begin
        Result := (Entry.Value.Name = ATypeInfo.Name) and (Entry.Value.Kind = ATypeInfo.Kind);
      end);
    if (serviceEntry.Value <> nil) then
    begin
      Exit(serviceEntry.Value);
    end;
  end;

  Exit(nil);
end;

Another class is used in the DLLs to resolve the references and it is passed an IDiResolver upon DLL initialization after loading it by the EXE. The class is as follows:

type
  TExternalDiResolver = class
  private
    class var FResolver: IDiResolver;

  public
    class procedure Init(const AResolver: IDiResolver);
    class function Resolve<TInterface: IInterface>(): TInterface;
  end;

implementation

uses
  Spring;

class procedure TExternalDiResolver.Init(const AResolver: IDiResolver);
begin
  Guard.CheckNotNull(AResolver, 'AResolver');
  FResolver := AResolver;
end;

class function TExternalDiResolver.Resolve<TInterface>(): TInterface;
begin
  if (not Assigned(FResolver)) then
  begin
    raise EInvalidOperationException.Create('Resolver not initialized');
  end;

  var interfaceType: PTypeInfo := TypeInfo(TInterface);
  var resultInterface := FResolver.Resolve(interfaceType);
  if (not Assigned(resultInterface)) then
  begin
    raise EInvalidOperationException.CreateFmt('Cannot resolve interface "%s"!', [interfaceType.Name]);
  end;

  Result := TInterface(resultInterface);
end;

Finally, the code in the DLL is even simpler than the initial variant:

procedure InitDI(const AResolver: IDiResolver);
begin
  TExternalDiResolver.Init(AResolver);
end;

procedure WriteMessage(const AMessage: string);
begin
  var service := TExternalDiResolver.Resolve<IOutputHandler>();
  service.WriteMessage(AMessage);
end;

exports
  InitDI,
  WriteMessage;

The code in the main application (EXE) is changed very little - an instance of the TDiResolver class is created and passed to the initialization code in the DLL as an interface:

type
  TInitDIProc = procedure(const ADiResolver: IDiResolver);
...
  var handle := SafeLoadLibrary('DynamicLibrary2.dll');
  var initDI := TInitDIProc(GetProcAddress(handle, 'InitDI'));
  var writeMessage := TWriteMessageProc(GetProcAddress(handle, 'WriteMessage'));

  var resolver := TDiResolver.Create(container);
  initDI(resolver);

  writeMessage('Message from dynamic library');

Please have in mind the registration of all interfaces in the DI container is done in the main application and the IDiResolver is used only in the DLLs.