Recent

Author Topic: Idea to solve a circular reference issue with classes.  (Read 2765 times)

jamie

  • Hero Member
  • *****
  • Posts: 7707
Idea to solve a circular reference issue with classes.
« on: March 28, 2026, 07:23:01 pm »
I have been working for a while now in translating a C++ over to FreePascal and have encountered some issues with circular references in the interface section where I cannot include the required unit hosting the needed Class due to the why Pascal handles UNIT references.

 So, for the time I have been declaring the effected class names with TOBJECT and in the Implementation section I then include the needed UNIT There and type-cast.

 What I would really like to see is a switch to possible turn that limitation off or this IDEA;

    1. When encountering a circular reference issue in the INTERFACE section we can declare now currently a forward reference to a CLASS that hasn't been created yet so the prior classes can at least see this However, that fails because if you don't declare the actual CLASS body in the interface section then you get Forward error, of course.

    2.  IDEA 1:
           Forwarding a Class;
           Class Name = Class IS UnitNameThatHostIT.CLASSNAME;
           Maybe the compiler can just look there once for the Class definition?

     3. IDEA 2:
            Have the compiler look at the Uses List in the Implementation section and use that reference if one isn't declared in the interface section with maybe a special indicator or HINT ?

 Just some ideas but if there is some hidden switch that I can turn on in the compiler that allows me to do this it would make my workflow some much simpler.

Jamie

           
           
         
         

The only true wisdom is knowing you know nothing

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12345
  • Debugger - SynEdit - and more
    • wiki
Re: Idea to solve a circular reference issue with classes.
« Reply #1 on: March 28, 2026, 08:57:03 pm »
May I assume this is an automated translation?

Because if so, then maybe the following (not tested, but something along those lines should be possible) may help.

In the interface declare the "unreachable" class as "TFooPlaceHolder = class end;" just to have a unique class, even if empty.

In the implementation, where the class is available, add a type helper. In the type helper add all the (public) methods of the class, and forward them with the help of a typecast (all those helper methods can be "inline;").

From that point forward you should be able to use the class as if it was the real deal.
(you may need an assignment operator, if you mix the placeholder and the real type).

You may even be able to avoid repeating the helper in diff units. Not sure, if you can pack it into a generic in the unit where the real class is, then you can specialize it in all other units.


At least one problem remains unsolved: if you have the class as type for a parameter. Because then you have the placeholder in the param list in the interface, and in the interface you wouldn't be able to have the operator that allows the real class to be passed to it.
« Last Edit: March 28, 2026, 08:58:43 pm by Martin_fr »

jamie

  • Hero Member
  • *****
  • Posts: 7707
Re: Idea to solve a circular reference issue with classes.
« Reply #2 on: March 28, 2026, 09:29:56 pm »
I know this is a hard one to solve if you want to keep with the tradition of Pascal but, either the compiler needs a leniency switch to allow a one level deep search in the interface units so not to get an error of this type or a switch that allows the compiler to check forward in the INTERFACE section when resolving a FORWARD declared class to see if there is a unit listed that host that class and use that as the reference instead and allow this to take place.

 Currently you can't do as you suggested and that is to create an empty class at the top because when you get into the IMPLEMENTATION it knows they aren't the same and that goes for other units that reference this unit, too.

 I realize this wouldn't be DELPHI compatible, but it could be one of those switches where it only works in OBJFPC mode.

 At least doing that to resolve a forward would look more transparent and without the option on it still work as traditional pascal.

 Jamie

The only true wisdom is knowing you know nothing

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12345
  • Debugger - SynEdit - and more
    • wiki
Re: Idea to solve a circular reference issue with classes.
« Reply #3 on: March 28, 2026, 10:55:41 pm »
The empty class would stay empty...

Ok while writing the dummy code, it wont work as I thought... I tried some amendments

dummy code
Code: Pascal  [Select][+][-]
  1. interface
  2. type
  3.    TOtherClassPlaceHolder = record // advanced / so you can have type helpers
  4.      Value: TObject; // can hold the real other class
  5.    end;
  6.  
  7.    TFoo = class
  8.    private
  9.       FOther: TOtherClassPlaceHolder;  // can be made to work
  10.    public
  11.        procedure DoFoo(o: TOtherClassPlaceHolder); // will not work / can not be called with the other class
  12.    end;
  13.  
  14. implementation
  15. uses other;
  16. type
  17.   // helper defined nested in generic class, so it can be specialized... not tested if that works.
  18.   // if not the helper must be implemented here (and in every unit....) :(
  19.   TOtherClassHelper = specialize other.TClassContainingHelperFromOtherUnit<TOtherClassPlaceHolder>.TheHelper;
  20.    
  21.  
  22. // not sure if that works for record, or must be in the record, or can go into the typehelper.
  23.   operator := (o: TRealOtherClass): TOtherClassPlaceHolder;
  24. begin
  25.   result.value := o;
  26. end;
  27.  
  28.      
  29. procedure TFoo.
  30. begin
  31.    Fother.DoSomething {Other};  // goes via the helper. The helper will do: TRealOtherClass(Value).DoSomethnig;
  32. end;
  33.  
  34.  

The unit with the real other class then defines a generic
Code: Pascal  [Select][+][-]
  1. generic TClassContainingHelperFromOtherUnit<A:class> = class
  2.   public type
  3.     TheHelper = type helper for A
  4.       procedure DoSomething
  5.     end;
  6. end;

I haven't tested if you can do that nested helper declaration...



And as I said, its not complete.

You can't call "DoFoo" from outside, because the custom operator wont be there, and the types wont be compatible without.



It might no be worth it...

But if all of it is auto generated, and the above actually can be compiled (again not tested), then it takes maybe half of the issues away? and no wait for anything...

You can get the function call param... but then you need a 3rd unit for an empty base class, so "the real other" actually inherits from the empty base class (on mess with lots of abstract / empty class instead).
And the type helper add all the methods to the empty class.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12345
  • Debugger - SynEdit - and more
    • wiki
Re: Idea to solve a circular reference issue with classes.
« Reply #4 on: March 28, 2026, 11:00:23 pm »
ok, overlooked => if the type helper is for a class (gen param), then the placeholder must be class, not record.

If the nested type helper works at all, it may be possible to do that for a record too (I don that with old style object, it works, but its a lot of corners).

You need to pass in a call with a dummy record (in a privat section), and do a helper for that. Then in another unit you can subclass the class and pass in any other record of your choice... (see LazListClasses)

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12345
  • Debugger - SynEdit - and more
    • wiki
Re: Idea to solve a circular reference issue with classes.
« Reply #5 on: March 29, 2026, 12:44:45 am »
Tested, you can have a generic type helper, but only with FPC 3.3.1
Code: Pascal  [Select][+][-]
  1. program Project1;
  2. {$Mode objfpc}
  3. {$ModeSwitch advancedrecords}
  4. {$ModeSwitch typehelpers}
  5.  
  6. type
  7.   TGenParamWrapper = class
  8.     private type
  9.     TFoo = record value: TObject end;
  10.   end;
  11.  
  12.   generic TGenTypeHelperForFoo<P: TGenParamWrapper> = class
  13.   public type
  14.     TheHelper = type helper for P.TFoo
  15.     end;
  16.   end;
  17.  
  18. begin
  19.  
  20. end.
  21.  

inherit TGenParamWrapper  in another unit, and place any other record with a "value" into it, to apply the same type helper.

So that can be provided by the unit with the "other class", and then the type helper can help in the implementation section.

Its not complete, but its a step... In case nothing better comes along.

creaothceann

  • Sr. Member
  • ****
  • Posts: 361
Re: Idea to solve a circular reference issue with classes.
« Reply #6 on: March 29, 2026, 01:03:07 am »
Perhaps these classes could be put into the same unit?

kupferstecher

  • Hero Member
  • *****
  • Posts: 618
Re: Idea to solve a circular reference issue with classes.
« Reply #7 on: March 29, 2026, 01:14:24 am »
IDEA 1: [...]
IDEA 2: [...]

IDEA3:
Allow an external declaration for the class. Then the class could only be used to define member variables (which normally is enough) and not to derive a class etc.
Code: Pascal  [Select][+][-]
  1. Type
  2.   TFoo = class; external;
(hypothetic code)

EDIT: No idea how to implement that. I guess at some point the unit where the class is expected has to be specified.
« Last Edit: March 29, 2026, 01:18:10 am by kupferstecher »

jamie

  • Hero Member
  • *****
  • Posts: 7707
Re: Idea to solve a circular reference issue with classes.
« Reply #8 on: March 29, 2026, 01:53:06 am »
I have shuffled units around and it's at the best point of being compilable without any more type cast that I already have.
 
 Even the C++ source code has a couple or EXTERNAL calls in the declarations to reduce this issue.

 The compiler needs to have an extra feature to allow for this and I think what I explained before would be greate.
Interface

 TCircleClass = Class; //Forward reference
 
  .. Other Classes that use TCirCleClass.

IMPLEMENTATION
 
Uses Unit_That_Holds_TCirCleClass;


//
 This way the compiler can use the definition in this section instead if one does not exist in the interface.

 At least it would and should be the same Class Pointer otherwise, class pointers differ although they could be the same.
The only true wisdom is knowing you know nothing

Thaddy

  • Hero Member
  • *****
  • Posts: 19165
  • Glad to be alive.
Re: Idea to solve a circular reference issue with classes.
« Reply #9 on: March 29, 2026, 08:55:57 am »
The usual way to resolve circular dependencies like you describe - if I understand correctly the scenario that can't otherwise be solved by interface/implementation manipulation, is to use an extra unit.
Strong circular dependencies often indicate design flaw, so rethinking code is often the better option.

An option that we at work often deployed is to solve it through the use of interfaces and in this case you can choose either CORBA or COM.
( You don't need the lifetime management of COM, you only need delegation )
objects are fine constructs. You can even initialize them with constructors.

Thaddy

  • Hero Member
  • *****
  • Posts: 19165
  • Glad to be alive.
Re: Idea to solve a circular reference issue with classes.
« Reply #10 on: March 29, 2026, 09:19:37 am »
Small example of the latter [edit:will remove leaks later]:
Code: Pascal  [Select][+][-]
  1. unit UnitA;
  2. {$mode objfpc}
  3. interface
  4.  
  5. type
  6.   IB = interface
  7.     ['{A1B2C3D4-E5F6-47A8-9B0C-112233445566}']
  8.     procedure DoSomethingInB;
  9.   end;
  10.  
  11.   TA = class
  12.   private
  13.     FB: IB;
  14.   public
  15.     constructor Create(const B: IB);
  16.     procedure DoSomethingInA;
  17.   end;
  18.  
  19. implementation
  20.  
  21. constructor TA.Create(const B: IB);
  22. begin
  23.   FB := B;
  24. end;
  25.  
  26. procedure TA.DoSomethingInA;
  27. begin
  28.   FB.DoSomethingInB;
  29. end;
  30. end.
Code: Pascal  [Select][+][-]
  1. unit UnitB;
  2. {$mode objfpc}
  3. interface
  4.  
  5. uses
  6.   UnitA;  // only depends on interface IB, not class TA
  7.  
  8. type
  9.   IA = interface
  10.     ['{B1C2D3E4-F5A6-47B8-9C0D-665544332211}']
  11.     procedure DoSomethingInA;
  12.   end;
  13.  
  14.   TB = class(TInterfacedObject, IB)
  15.   private
  16.     FA: IA;
  17.   public
  18.     constructor Create(const A: IA);
  19.     procedure DoSomethingInB;
  20.   end;
  21.  
  22. implementation
  23.  
  24. constructor TB.Create(const A: IA);
  25. begin
  26.   FA := A;
  27. end;
  28.  
  29. procedure TB.DoSomethingInB;
  30. begin
  31.   FA.DoSomethingInA;
  32. end;
  33. end.
Code: Pascal  [Select][+][-]
  1. program InterfaceCircularBreak;
  2. {$mode objfpc}
  3. uses
  4.   UnitA, UnitB;
  5.  
  6. type
  7.   TAWrapper = class(TInterfacedObject, IA)
  8.   private
  9.     FA: TA;
  10.   public
  11.     constructor Create(A: TA);
  12.     procedure DoSomethingInA;
  13.   end;
  14.  
  15. constructor TAWrapper.Create(A: TA);
  16. begin
  17.   FA := A;
  18. end;
  19.  
  20. procedure TAWrapper.DoSomethingInA;
  21. begin
  22.   FA.DoSomethingInA;
  23. end;
  24.  
  25. var
  26.   A: TA;
  27.   B: TB;
  28.   AIntf: IA;
  29. begin
  30.   A := TA.Create(nil);
  31.   AIntf := TAWrapper.Create(A);
  32.   B := TB.Create(AIntf);
  33.   A := TA.Create(B);
  34.  
  35.   A.DoSomethingInA;
  36. end.


« Last Edit: March 29, 2026, 09:56:03 am by Thaddy »
objects are fine constructs. You can even initialize them with constructors.

Thaddy

  • Hero Member
  • *****
  • Posts: 19165
  • Glad to be alive.
Re: Idea to solve a circular reference issue with classes.
« Reply #11 on: March 29, 2026, 11:49:34 am »
Better example, uses the extra unit too:
Code: Pascal  [Select][+][-]
  1. // Factor out
  2. unit UnitInterfaces;
  3. {$mode objfpc}{$H+}
  4. interface
  5.  
  6. uses
  7.   SysUtils;
  8.  
  9. type
  10.   // Forward-declare both interfaces so they can reference each other
  11.   IAnimal = interface;
  12.   IZooKeeper = interface;
  13.  
  14.   IAnimal = interface(IInterface)
  15.     ['{A1B2C3D4-0001-0000-0000-000000000001}']
  16.     function  GetName: string;
  17.     function  GetKeeper: IZooKeeper;
  18.     procedure SetKeeper(const AKeeper: IZooKeeper);
  19.     procedure MakeSound;
  20.   end;
  21.  
  22.   IZooKeeper = interface(IInterface)
  23.     ['{A1B2C3D4-0002-0000-0000-000000000002}']
  24.     function  GetKeeperName: string;
  25.     function  GetAssignedAnimal: IAnimal;
  26.     procedure AssignAnimal(const AAnimal: IAnimal);
  27.     procedure FeedAnimal;
  28.   end;
  29.  
  30. implementation
  31. end.
Code: Pascal  [Select][+][-]
  1. unit UnitA;
  2. {$mode objfpc}{$H+}
  3. interface
  4.  
  5. uses
  6.   SysUtils,
  7.   UnitInterfaces;   // Only depends on the shared interface unit
  8.  
  9. type
  10.   TAnimal = class(TInterfacedObject, IAnimal)
  11.   private
  12.     FName:   string;
  13.     FKeeper: IZooKeeper;   // strong interface Reference.
  14.   public
  15.     constructor Create(const AName: string);
  16.     // IAnimal
  17.     function  GetName: string;
  18.     function  GetKeeper: IZooKeeper;
  19.     procedure SetKeeper(const AKeeper: IZooKeeper);
  20.     procedure MakeSound;
  21.   end;
  22.  
  23. implementation
  24.  
  25. constructor TAnimal.Create(const AName: string);
  26. begin
  27.   inherited Create;
  28.   FName := AName;
  29. end;
  30.  
  31. function TAnimal.GetName: string;
  32. begin
  33.   Result := FName;
  34. end;
  35.  
  36. function TAnimal.GetKeeper:IZooKeeper ;
  37. begin
  38.   Result := FKeeper;
  39. end;
  40.  
  41. procedure TAnimal.SetKeeper(const AKeeper: IZooKeeper);
  42. begin
  43.    FKeeper:=AKeeper;
  44. end;
  45.  
  46. procedure TAnimal.MakeSound;
  47. begin
  48.   WriteLn(FName + ' says: Roar!');
  49. end;
  50.  
  51. end.
Code: Pascal  [Select][+][-]
  1. unit UnitB;
  2. {$mode objfpc}{$H+}
  3. interface
  4.  
  5. uses
  6.   SysUtils,
  7.   UnitInterfaces;   // Only depends on the shared interface unit
  8.  
  9. type
  10.   TZooKeeper = class(TInterfacedObject, IZooKeeper)
  11.   private
  12.     FKeeperName:     string;
  13.     FAssignedAnimal: Pointer; // weak reference, no UnitA needed
  14.   public
  15.     constructor Create(const AKeeperName: string);
  16.     // IZooKeeper
  17.     function  GetKeeperName: string;
  18.     function  GetAssignedAnimal: IAnimal;
  19.     procedure AssignAnimal(const AAnimal: IAnimal);
  20.     procedure FeedAnimal;
  21.   end;
  22.  
  23. implementation
  24.  
  25. constructor TZooKeeper.Create(const AKeeperName: string);
  26. begin
  27.   inherited Create;
  28.   FKeeperName := AKeeperName;
  29. end;
  30.  
  31. function TZooKeeper.GetKeeperName: string;
  32. begin
  33.   Result := FKeeperName;
  34. end;
  35.  
  36. function TZooKeeper.GetAssignedAnimal: IAnimal;
  37. begin
  38.   // Get the interface without refcounting
  39.   GetInterfaceWeak(IAnimal,FAssignedAnimal);
  40. end;
  41.  
  42. procedure TZooKeeper.AssignAnimal(const AAnimal: IAnimal);
  43. begin
  44.   FAssignedAnimal := AAnimal;
  45.   // Tell the animal about its keeper through the interface
  46.   AAnimal.SetKeeper(Self);
  47. end;
  48.  
  49. procedure TZooKeeper.FeedAnimal;
  50. begin
  51.   if Assigned(FAssignedAnimal) then
  52.   begin
  53.     Write('Keeper ' + FKeeperName + ' feeds ' + IAnimal(FAssignedAnimal).GetName + '. ');
  54.     IAnimal(FAssignedAnimal).MakeSound;
  55.   end
  56.   else
  57.     WriteLn(FKeeperName + ' has no animal to feed.');
  58. end;
  59.  
  60. end.
Code: Pascal  [Select][+][-]
  1. program Main;
  2. {$mode objfpc}{$H+}{$interfaces com}
  3. uses
  4.   SysUtils,
  5.   UnitInterfaces,
  6.   UnitA,
  7.   UnitB;
  8. var
  9.   Animal:    IAnimal;
  10.   Keeper:    IZooKeeper;
  11. begin
  12.   // Create concrete instances, but hold them as interfaces
  13.   Animal := TAnimal.Create('Leo the Lion');
  14.   Keeper := TZooKeeper.Create('Jane');
  15.   // Wire them up — no direct class knowledge across units
  16.   Keeper.AssignAnimal(Animal);
  17.   // Interact entirely through interfaces
  18.   Keeper.FeedAnimal;
  19.   WriteLn('Keeper name : ', Keeper.GetKeeperName);
  20.   WriteLn('Animal name : ', Animal.GetName);
  21.   WriteLn('Animal keeper: ', Animal.GetKeeper.GetKeeperName);
  22.   WriteLn('Done. Memory managed automatically via ref-counting.');
  23. end.
Code: Text  [Select][+][-]
  1. Output
  2. Keeper Jane feeds Leo the Lion. Leo the Lion says: Roar!
  3. Keeper name : Jane
  4. Animal name : Leo the Lion
  5. Animal keeper: Jane
  6. Done.
  7. Memory managed automatically via ref-counting.
  8. Circular reference solved.

This solution is often possible if you don't mind a little extra overhead for using interfaces.




« Last Edit: March 29, 2026, 12:02:30 pm by Thaddy »
objects are fine constructs. You can even initialize them with constructors.

Thaddy

  • Hero Member
  • *****
  • Posts: 19165
  • Glad to be alive.
Re: Idea to solve a circular reference issue with classes.
« Reply #12 on: March 29, 2026, 02:20:40 pm »
What you can also do (this is like interfaces, but old school) is downgrade:
Code: Pascal  [Select][+][-]
  1. program foobar;
  2. {$mode objfpc}{$assertions on}
  3. uses
  4.   ufoo,ubar;
  5. var
  6.   Foo:TFoo;
  7.   Bar:TBar;
  8. begin
  9.   Foo := TFoo.Create(TBar);
  10.   Bar := TBar.Create(TFoo);
  11.   Foo.DoSomething;
  12.   Bar.DoSomething;
  13.   Bar.free;
  14.   Foo.Free;
  15. end.
Code: Pascal  [Select][+][-]
  1. unit ufoo;
  2. {$mode objfpc}{$assertions on}
  3. interface
  4. // no units here
  5. type
  6.   { We downgrade the interface for FBar to TObject but
  7.     that will be resolved later by passing the TBar
  8.     to the constructor: this results in FBar's
  9.     classtype to be of TBar, not TObject }
  10.   TFooClass = class of TFoo;
  11.   TFoo = class
  12.   private
  13.     FBar:TObject;
  14.   public
  15.     constructor create(const b:TClass);
  16.     destructor destroy;override;
  17.     procedure DoSomething;
  18.     property bar:TObject read FBar;
  19.   end;
  20.  
  21. implementation
  22. // but here
  23. uses
  24.   ubar;
  25.  
  26. constructor TFoo.create(const b:TClass);
  27. begin
  28.   inherited create;
  29.   Assert(b = Tbar,'parameter needs to be of type TBar');
  30.   FBar := b.create as TBar;// pass b as TBar
  31. end;
  32.  
  33. destructor TFoo.Destroy;
  34. begin
  35.   If Assigned(FBar) then FBar.Free;
  36.   FBar := nil; //local
  37.   inherited Destroy;
  38. end;
  39.  
  40. procedure TFoo.DoSomething;
  41. begin
  42.   (FBar as Tbar).DoSomething;
  43. end;  
  44. end.
Code: Pascal  [Select][+][-]
  1. unit ubar;
  2. {$mode objfpc}{$assertions on}
  3. interface
  4. // no units here
  5. type
  6.   { We downgrade the interface for FFoo to TObject but
  7.     that will be resolved later by passing TFoo
  8.     to the constructor: this results in TFoo's
  9.     classtype to be of TFoo, not TObject }
  10.   TBarClass = class of TBar;
  11.   TBar = class
  12.   private
  13.     FFoo:TObject;
  14.   public
  15.     constructor create(const f:TClass);
  16.     destructor destroy;override;
  17.     procedure DoSomething;
  18.   end;
  19.  
  20. implementation
  21. // but here
  22. uses
  23.   ufoo;
  24.  
  25. constructor TBar.create(const f:TClass);
  26. begin
  27.    Assert(f = TFoo,'parameter needs to be of type TFoo');
  28.   inherited create;
  29.   FFoo := f.create as TFoo;
  30. end;
  31.  
  32. destructor TBar.destroy;
  33. begin
  34.   if assigned(FFoo) then FFoo.Free;
  35.   FFoo := nil;// local
  36.   inherited destroy;
  37. end;
  38.  
  39. procedure TBar.DoSomething;
  40. begin
  41.   writeln('TEST');
  42. end;  
  43. end.

This is also more lightweight than my previous solutions.
I actually prefer it. A bit old school, but solid.


« Last Edit: March 29, 2026, 02:30:32 pm by Thaddy »
objects are fine constructs. You can even initialize them with constructors.

Zvoni

  • Hero Member
  • *****
  • Posts: 3376
Re: Idea to solve a circular reference issue with classes.
« Reply #13 on: March 30, 2026, 08:53:51 am »
What about Include-Files?
Just have the Class Definition (Optional with Type-Section) in the Include-File
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

jamie

  • Hero Member
  • *****
  • Posts: 7707
Re: Idea to solve a circular reference issue with classes.
« Reply #14 on: March 30, 2026, 12:22:42 pm »
What about Include-Files?
Just have the Class Definition (Optional with Type-Section) in the Include-File
That would be nice since a lot of C compilers will create the class definition on the fly with the same signature, that does not happen with pascal, they are given id's based on where they come from and therefor the two classes don't match and thus fails compile.

Hours have been put into this trying to resolve it, and the only thing I can think of is a single level search for the class definition without it also checking the unit for possible definition of the unit where the search came from has to be a little more lenient restrictions with maybe a hint warning in compile time.

Jamie

The only true wisdom is knowing you know nothing

 

TinyPortal © 2005-2018