Delphi [weak] 引用属性在多个库中实现时生成 "Invalid class typecast"

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

Delphi Berlin 10.1 添加了 [weak] 引用。 Marco Cantu's Blog 有一些基础知识。

为了我的测试,我创建了两个包含两种自动化对象类型的 COM 库。容器对象包含内容对象列表,而内容对象包含对其容器的弱引用。

以下两种场景已经过测试并正常工作(弱引用设置为空并释放内存):

但是,当我将组件类放在两个单独的库中时,代码生成 "invalid class typecast",删除 [weak] 属性后错误消失。请原谅奇怪的样本,其目的只是为了使问题最小化,不应被视为标准编码实践

这是第一个定义接口和 CoClass 的库 .ridl 文件 容器:

[
  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;
  };

};

这是我的容器实现

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.

我的第二个库引用了第一个且仅包含我的内容界面的 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;
  };

及其弱引用的实现

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.

我测试如下

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.

为什么拆分实现时会出现错误?我怎样才能缓解这个问题?

不要对 COM 接口使用 [Weak]。它不适用于 COM。 [弱] 只应用于内部非导出 COM 接口。

原因是 COM 接口实现无法正确处理 Delphi class,甚至可能无法实现 class。此外,您拥有的 COM 库不共享基本 TObject 的相同实现。您 可能 使用共享的 rtl 包来构建所有内容,但即便如此……您还是在地雷上跳舞。

正如 Allen Bauer 在他的回答中所解释的那样,[weak] 不适用于 COM 接口,因为它们不能保证得到 Delphi TObject 派生的 类,这是 [weak] 引用在对象被释放时自动归零所必需的。 RTL 在运行时跟踪弱引用,但无法跨库跟踪弱引用,除非在它们之间共享 RTL 库的单个实例(即,如果您在启用运行时包的情况下编译库,然后使用可执行文件部署 RTL BPL ).

但是,只要您不需要使用 [weak] 的自动 nil 功能,您可以使用无类型的 Pointer 代替:

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

只要您需要使用其 methods/properties,您只需将 FContainer 类型转换为 IMyContainer,例如:

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

在 10.1 Berlin 及更高版本中,您可以改用 [unsafe] 属性:

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

如 Marco 的博客所述:

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).