Delphi [weak] reference attribute produces "Invalid class typecast" when implementation is in more than one library
Asked Answered
E

2

9

Delphi Berlin 10.1 adds [weak] references. Marco Cantu's Blog has some basics on it.

For my test I created two COM libraries holding two automation object types. The container object holds a list of the content objects while the content objects holds a weak reference to their container.

The following two scenarios were tested and worked correctly (weak references are set null and memory is released) :

  • A single COM library with both interfaces and CoClasses.
  • Two COM libraries one with the interfaces and another with the CoClasses

However, when I place the coclasses in two separate libraries the code produces "invalid class typecast", the error goes away when removing the [weak] attribute. Please excuse the odd sample, its purpose is simply to make the problem minimal and should not be taken as standard coding practice

Here is the first library .ridl file that defines both interfaces and the CoClass for the container:

[
  uuid(E1EE3651-A400-49BF-B5C5-006D9943B9C0),
  version(1.0)

]
library DelphiIntfComLib
{

  importlib("stdole2.tlb");

  interface IMyContainer;
  interface IMyContent;
  coclass MyContainer;


  [
    uuid(A7EF86F7-40CD-41EE-9DA1-4D9B7B24F06B),
    helpstring("Dispatch interface for MyContainer Object"),
    dual,
    oleautomation
  ]
  interface IMyContainer: IDispatch
  {
    [id(0x000000C9)]
    HRESULT _stdcall Add([in] IMyContent* AMyContent);
  };

  [
    uuid(BFD6D976-8CEF-4264-B95A-B5DA7817F6B3),
    helpstring("Dispatch interface for MyContent Object"),
    dual,
    oleautomation
  ]
  interface IMyContent: IDispatch
  {
    [id(0x000000C9)]
    HRESULT _stdcall SetWeakReferenceToContainer([in] IMyContainer* AContainer);
  };

  [
    uuid(1F56198B-B1BE-4E11-BC78-0E6FF8E55214)
  ]
  coclass MyContainer
  {
    [default] interface IMyContainer;
  };

};

Here is my container implementation

unit Unit1;

{$WARN SYMBOL_PLATFORM OFF}

interface

uses
  ComObj, ActiveX, DelphiIntfComLib_TLB, StdVcl, Generics.Collections;

type
  TMyContainer = class(TAutoObject, IMyContainer)
  private
     FList: TList<IMyContent>;
  protected
    procedure Add(const AMyContent: IMyContent); safecall;
  public
    Destructor Destroy; override;

    procedure Initialize; override;

  end;

implementation

uses ComServ;

procedure TMyContainer.Add(const AMyContent: IMyContent);
begin
  FList.Add(AMyContent);
  AMyContent.SetWeakReferenceToContainer(self);
end;

destructor TMyContainer.Destroy;
begin
  FList.Free;
  inherited;
end;

procedure TMyContainer.Initialize;
begin
  inherited;
  FList := TList<IMyContent>.create;
end;

initialization
  TAutoObjectFactory.Create(ComServer, TMyContainer, Class_MyContainer,
    ciMultiInstance, tmApartment);
end.

My second library reference the first and only contains my content interface's CoClass

[
  uuid(65659EE4-1949-4112-88CA-F2D5B5D8DA2C),
  version(1.0)

]
library DelphiImplComLib
{

  importlib("stdole2.tlb");
  importlib("DelphiIntfComLib.dll");

  coclass MyContent;


  [
    uuid(79D1669A-8EB6-4AE6-8F4B-91137E6E6DC1)
  ]
  coclass MyContent
  {
    [default] interface IMyContent;
  };

and its implementation with the weak reference

unit Unit2;

{$WARN SYMBOL_PLATFORM OFF}

interface

uses
  ComObj, ActiveX, DelphiImplComLib_TLB, StdVcl, DelphiIntfComLib_TLB;

type
  TMyContent = class(TAutoObject, IMyContent)
  private
   [Weak] //If included will cause "invalid class typecast" error
    FContainer : IMyContainer;
  protected
    procedure SetWeakReferenceToContainer(const AContainer: IMyContainer); safecall;
  end;

implementation

uses ComServ;

procedure TMyContent.SetWeakReferenceToContainer(const AContainer: IMyContainer);
begin
  FContainer := AContainer;
end;

initialization
  TAutoObjectFactory.Create(ComServer, TMyContent, Class_MyContent,
    ciMultiInstance, tmApartment);
end.

I tested as follows

program Project13;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  DelphiImplComLib_TLB in 'impl\DelphiImplComLib_TLB.pas',
  DelphiIntfComLib_TLB in 'Intf\DelphiIntfComLib_TLB.pas';

var
  GMyContainer : IMyContainer;
  GMyContent : IMyContent;
begin
  GMyContainer := CoMyContainer.Create;
  GMyContent := CoMyContent.Create;
  GMyContainer.Add(GMyContent);
end.

Why do I get an error when I split my implementations? How can I alleviate this problem?

Engineman answered 20/9, 2016 at 13:45 Comment(0)
D
12

As Allen Bauer explained in his answer, [weak] does not work with COM interfaces, as they are not guaranteed to be backed by Delphi TObject-derived classes, which is necessary for [weak] references to be auto-nil'ed when objects are freed. The RTL keeps track of weak references at runtime, but cannot track weak references across libraries unless a single instance of the RTL library is shared between them (ie if you compile the libraries with Runtime Packages enabled, and then deploy the RTL BPL with your executables).

However, as long as you don't need to use the auto-nil functionality of [weak], you can use an untyped Pointer instead:

type
  TMyContent = class(TAutoObject, IMyContent)
  private
    FContainer : Pointer{IMyContainer};
    ...
  end;

You would just have to typecast FContainer to IMyContainer whenever you need to use its methods/properties, eg:

IMyContainer(FContainer).Add(...);

In 10.1 Berlin and later, you can use the [unsafe] attribute instead:

type
  TMyContent = class(TAutoObject, IMyContent)
  private
    [Unsafe] FContainer : IMyContainer;
    ...
  end;

As mentioned on Marco's blog:

Weak and Unsafe Interface References in Delphi 10.1 Berlin

What if the object has a standard reference count implementation and you want to create an interface reference that is kept out of the total count of references? You can now achieve this by adding the [unsafe] attribute to the interface variable declaration, changing the code above to:

procedure TForm3.Button2Click(Sender: TObject);
var
  [unsafe]
  one: ISimpleInterface;
begin
  one := TObjectOne.Create;
  one.DoSomething;
end;

Not that this is a good idea, as the code above would cause a memory leak. By disabling the reference counting, when the variable goes out of scope nothing happens. There are some scenarios in which this is beneficial, as you can still use interfaces and not trigger the extra reference. In other words, an unsafe reference is treated just like... a pointer, with no extra compiler support.

Now before you consider using the unsafe attribute for having a reference without increasing the count, consider that in most cases there is another better option, that is the use of weak references. Weak references also avoid increasing the reference count, but they are managed. This means that the system keeps track of weak references, and in case the actual object gets deleted, it will set the weak reference to nil. With an unsafe reference, instead, you have no way to know the status of the target object (a scenario called dangling reference).

Dustpan answered 20/9, 2016 at 16:49 Comment(4)
I was hoping to have the auto-nil feature, but I'll settle for unsafe. I'll mitigate that risk in other waysEngineman
@JasperSchellingerhout: The auto-nil feature comes from the TObject destructor. When an object is being destroyed, it loops through a global table of weak references and nils any references that refer to that object. So if you have a COM interface that is not implemented by a TObject class, weak references to that object will not auto-nil.Dustpan
In this specific sample case its not too difficult to manage. On destruction the container can traverse and set the weak reference on children to nil. Any methods to remove content can do the same.Engineman
Your comment on Allen's answer on the RTL unable to track weak references across libraries is critical to understanding the reason for the error. Would you be so kind to add it to your answerEngineman
V
17

Do not use [Weak] for COM-interfaces. It is not intended for use with COM. [Weak] should only be used for internal non-exported-COM interfaces.

The reason is that there is no way for a COM-interface implementation, which may not even be implemented by a Delphi class, to properly handle the [weak] references. Additionally, the COM libraries you have aren't sharing the same implementation of the base TObject. You may get away with building everything using the shared rtl package, but even then... you're dancing on a land-mine.

Vhf answered 20/9, 2016 at 16:15 Comment(3)
I understand your point. But in this case my objects are descendants of TObject. What do you mean by "Same implementation"Engineman
@JasperSchellingerhout: Allen is referring to the base RTL that implements the TObject class. The RTL keeps track of weak references at runtime (so it can automatically nil all weak references to a given object when that object is freed from memory). The RTL can't track references across libraries unless a single instance of the RTL library is shared between them (ie you compile the libraries with Runtime Packages enabled, and then deploy the RTL BPL with your executables).Dustpan
@JasperSchellingerhout: you should ask Embarcadero to add these details to its [weak] documentation.Dustpan
D
12

As Allen Bauer explained in his answer, [weak] does not work with COM interfaces, as they are not guaranteed to be backed by Delphi TObject-derived classes, which is necessary for [weak] references to be auto-nil'ed when objects are freed. The RTL keeps track of weak references at runtime, but cannot track weak references across libraries unless a single instance of the RTL library is shared between them (ie if you compile the libraries with Runtime Packages enabled, and then deploy the RTL BPL with your executables).

However, as long as you don't need to use the auto-nil functionality of [weak], you can use an untyped Pointer instead:

type
  TMyContent = class(TAutoObject, IMyContent)
  private
    FContainer : Pointer{IMyContainer};
    ...
  end;

You would just have to typecast FContainer to IMyContainer whenever you need to use its methods/properties, eg:

IMyContainer(FContainer).Add(...);

In 10.1 Berlin and later, you can use the [unsafe] attribute instead:

type
  TMyContent = class(TAutoObject, IMyContent)
  private
    [Unsafe] FContainer : IMyContainer;
    ...
  end;

As mentioned on Marco's blog:

Weak and Unsafe Interface References in Delphi 10.1 Berlin

What if the object has a standard reference count implementation and you want to create an interface reference that is kept out of the total count of references? You can now achieve this by adding the [unsafe] attribute to the interface variable declaration, changing the code above to:

procedure TForm3.Button2Click(Sender: TObject);
var
  [unsafe]
  one: ISimpleInterface;
begin
  one := TObjectOne.Create;
  one.DoSomething;
end;

Not that this is a good idea, as the code above would cause a memory leak. By disabling the reference counting, when the variable goes out of scope nothing happens. There are some scenarios in which this is beneficial, as you can still use interfaces and not trigger the extra reference. In other words, an unsafe reference is treated just like... a pointer, with no extra compiler support.

Now before you consider using the unsafe attribute for having a reference without increasing the count, consider that in most cases there is another better option, that is the use of weak references. Weak references also avoid increasing the reference count, but they are managed. This means that the system keeps track of weak references, and in case the actual object gets deleted, it will set the weak reference to nil. With an unsafe reference, instead, you have no way to know the status of the target object (a scenario called dangling reference).

Dustpan answered 20/9, 2016 at 16:49 Comment(4)
I was hoping to have the auto-nil feature, but I'll settle for unsafe. I'll mitigate that risk in other waysEngineman
@JasperSchellingerhout: The auto-nil feature comes from the TObject destructor. When an object is being destroyed, it loops through a global table of weak references and nils any references that refer to that object. So if you have a COM interface that is not implemented by a TObject class, weak references to that object will not auto-nil.Dustpan
In this specific sample case its not too difficult to manage. On destruction the container can traverse and set the weak reference on children to nil. Any methods to remove content can do the same.Engineman
Your comment on Allen's answer on the RTL unable to track weak references across libraries is critical to understanding the reason for the error. Would you be so kind to add it to your answerEngineman

© 2022 - 2024 — McMap. All rights reserved.