record2 := record1; is a copy by reference only not a deep copy

Hi we have just found something close to be a KILLER for our Port from OpenVMS PASCAL (and maybe others) to Oxygene PASCAL (amd mybe others).

remember
var i,j : Int32;

i := 1;
j := i;
j := 2 // is not allowed to change the value of i OK?

type grulage = public record
i1 : Int32;
d2 : Double;
a1 : array [0.5] of Int32 := [1,2,3,4,5,6];
s1 : String := new String(‘abcdef’);
sb : StringBuilder := new StringBuilder(6);
end;

implementation
var gru1 := new grulage;
with gru1 do begin
i1 := 5;
d2 := 6.5;
sb[3] := ‘b’;
end;

// now do a and then change afield in gru2 and watchhow the same field is changed in gru1; this is very worse for us.
gru2 := gru1;

in OpenVMS this are 2 independent equal instances now, not so in Oxygene
because when I chane a filed in gru2 the same filed is changed in gru1 as well.
So the := operator just copies the reference

So the := operator when used in Oxygen does not do a deep copy as it done in older PASCAL’s like OpenVMS PASCAL but just returns a reference in gru2 to gru1 so you are changing the same instances of fields and sub fields. OO is nice buty records are an old concepts and operators working on records (not obejcts) should behave the same or any code port is not a port but a new implementation.

Any Idea how to overload the := operator to do a deep copy when I assigne variables of type record;

And what are array’s doing?

with arrays it is the same ; when any array assigned ( := ) to an other varyable of the same array type, it does not create a new instance of an array and then takes a deep copy of the array elements but just copies a reference to the old array;
consequence: any change will impact both array variables-fields.

any oversigths on my part?

Remember: It does not help us if one writes that records behave more like classes . . . it is the change of the := operator which makes us now a big head atch and which degrades for us the value of Oxygene for our purpose which is PORTING code from a legacy PASCAL to a modern OO-PASCAL.

changing concepts is by far the most worse thing one can do.

I don’t know excactly what is going on in your case, but the := operator on records does exactly what you ask for…
It makes a new deep clone…
try the following code (in a console app):

namespace ConsoleApplication15;

interface

uses
  System.Linq;

type
  ConsoleApp = class
  public
    class method Main(args: array of String);
  end;

  RecA = public record
  public
    property Test: Int32;
  end;


implementation

class method ConsoleApp.Main(args: array of String);
begin
  var a: RecA;
  var b: RecA;
  a.Test := 23;
  b := a;
  Console.WriteLine('a (should be 23) : ' + a.Test.ToString());
  Console.WriteLine('b (should be 23) : ' + b.Test.ToString());
  b.Test := 33;
  Console.WriteLine('a (should still be 23) : ' + a.Test.ToString());
  Console.WriteLine('b (should now be 33) : ' + b.Test.ToString());
  a.Test := 55;
  Console.WriteLine('a (should now be 55) : ' + a.Test.ToString());
  Console.WriteLine('b (should still be 33) : ' + b.Test.ToString());
  Console.ReadLine();
end;

end.

Both record instances do not influence each other…

thank you, I will do so asap and let you know;

I was sure I was not dreaming when I reported this BUG; We have a record type defined with methods and properties. This in contrast to your simple example;

with our public record type GRULAGE at TARIFLIB/PAS/TARIFTYP.PAS we observer the following

// Testing record assignements
var r1 : Grulage;
var r2 : Grulage;
with r1 do begin
    bez := new StringBuilder('ABCDEF',6);
    sex := new StringBuilder('M',1);
    gen := new StringBuilder('95',2);
    zin := 3.50;
    erg := new StringBuilder('ghijkl',6);
    par := new Array[0..5] of Integer;
    par[0] := 0;
    par[1] := 1;
    par[2] := 2;
    par[3] := 3;
    par[4] := 4;
    par[5] := 5;
end;

r2 := r1;

with r2 do begin
    bez := new StringBuilder('ghijkl',6);
    sex := new StringBuilder('W',1);
    gen := new StringBuilder('04',2);
    zin := 3.00;
    erg := new StringBuilder('ABCDEF',6);
    par[0] := 50;
    par[1] := 60;
    par[2] := 70;
    par[3] := 31;
    par[4] := 41;
    par[5] := 51;
end;

then we call
TARIFLIB.TEST.TE_ORD();

namespace TARIFLIB.TEST.TE_ORD;

interface

uses
  System,
  System.Text,
  System.IO,
  Toolbox,
  UnsafeCode,
  TARIFLIB.PAS.*;

PROCEDURE TE_ORD();

var
    Tafel_geladen    : Grulage;
    Tafel_gerechnet  : Grulage;

implementation

PROCEDURE TE_ORD();
var
    gr            :Grulage;
    time1         : String := new String('0000000000000');
    time2         : String := new String('0000000000000');
    z1,z2,zz      :Double;
    x,omega       :Integer;
    f             :TEXT;
    o             :TEXT;
    i1            :TEXT;
    i2            :TEXT;
    str           :String;   

begin

  Tafel_geladen := new Grulage;
  Tafel_gerechnet := new Grulage;

  TARIFLIB.PAS.KOL_TAFELN.Tafel_geladen := Tafel_geladen;
  TARIFLIB.PAS.KOL_TAFELN.Tafel_gerechnet := Tafel_gerechnet;

now comes code to setup gru ! and then we call a Function in

TARIFLIB.PAS.KOL_TAFELN.Ord_l(x,gru);

namespace TARIFLIB.PAS.KOL_TAFELN;

Interface

uses
    System.Text,
    Toolbox,
    TARIFLIB.PAS.*;

among other var’s we have

Tafel_aktuell    : Grulage;
Tafel_geladen    : Grulage;
Tafel_gerechnet  : Grulage;

remember (and see above) we keep a reference to this from the prious caller level such that two of this tables can not go out of scope when the call returns to the caller.

But in any case at this time we have 4 instances of the same type, gru passed as parameter with the call into this DLL, and the 3 tables Tafel_* each of type GRULAGE, and two of them referenced (ankered) at the callers level.

TARIFLIB.PAS.KOL_TAFELN.Ord_l(x,gru);

calles

PROCEDURE Tafel_rechnen (gru: Grulage);
  VAR x,x0,x1:    Integer;              
      i,vn,quote: Double;

BEGIN
  IF NOT Geladen(gru) THEN 
      Tafel_laden(gru);

  Tafel_aktuell := gru;
  //gru.CopyTo(var Tafel_aktuell);  //Tafel_aktuell := gru;
  Tafel_aktuell.erg[4] := 'X'; (* Invarianz der lx[x] bei K/U-Ausschluss *)

Geladen(gru) checks that Tafel_geladen has the same field values then gru.
Tafel_geladen has its reference at the callers level. So we can go in and out without loosing scope.

If Geladen(gru) returns false, then we call Tafel_laden(gru); which is a very complex code pice but it basically does a lot {$INCLUDE …} and a very complex IF THEN ELSE, as done 20 years ago.

In any case after that, Tafel_geladen und gru are both there ! but how ?

Then for the current run through Tafel_rechnen we do

  Tafel_aktuell := gru;
  //gru.CopyTo(var Tafel_aktuell);  //Tafel_aktuell := gru;
  Tafel_aktuell.erg[4] := 'X'; (* Invarianz der lx[x] bei K/U-Ausschluss *)

And here we detect that any change on Tafel_Aktuell makes a change to gru and opposit

See picture of the Visual Studio here : (nothing helps)
why does upload not take place ? I shall send it to you by e-mail given I get your e-mail address.

I think my problem has a lot to do with DLL boundaries and Marshalling !

In short: r2 := r1; all in the same module/DLL seams to work asl long as r1 and r2 are local variables / records / fields.

and it does not work when i.e.r 1 is keept at DLL1 the caller and r2 is keept at DLL2 the one to becalled. In this case I think " r2 := r1" recives only a reference of r1. Hence a change at r2 impacts r1 and opposit.

Do your simple example with 2 DLL’s please and tell me what you observe, if the problem exists and how you intend to cure this, if ever possible.

Josef

Hi Josef,

I do not work for Remobjects :smile:
So I can not create a cure for this…
I just wanted to see if I could reproduce your problem with a small test program…

So thank’s anway;
Let me know what your results are if you move the two instances in two different assemblies (dll’s or exe’s) referring to a common record definition.

even more worse then I expected:
in the example above we have r1 and r2 of type Grulage

and we have

r2 := r1;

this copies all the fileds from r1 to r2 and makes each record including “moste of the fields” in r2 and independent instance;

This is however not true for the par field which is of type

array[0..5] of integer;

the assignment operator puts into r2.par a reference to r1.par and when I change r2.par[3] I change at the same time r1.par[3]

This is a mess;

So we like to know from RemObject how the " := " operator shall work with records and nested fields like arrays and fields of other sub-record types.

older compilers support this nicely and consistently so it is a mess for us when we have to port a lot old code to get up to date with a new Pascal compiler.

Arrays are reference types in .NET so if your record contains non-value types there is not much that the compiler can do I think…
SO article about arrays

If the field of the record is a pointer and you create a clone of that record, what do you expect in the pointer field of that new record variable? (because all ref types could be seen as pointers)
Anyways I think that the people from RO can give you a better answer than me :smile:

note: copies the fields. if those fields contain non-value types (ie object references), then those object references will be copies - the objects themselves will not be cloned (who could they, there’s no safe standard mechanism for that).

arrays are, i believe, not value types, so they are treated as any other object type. the reference is copied. this is no different when assigng vars

var x := new MyObject;
vat y := x; // the *reference* is copied from x to y; the object is not duplicated

record1.x := new MyObject
record2 := record1; // same here.

in both of these cases, no second “MyObject” instance is created, ever.

IOW, byw very definition record copies are not deep copies. Deep copy would imply cloning all objects, and all their nested fields, and so on. Not something that is safe to do on a generic type structure.

Can you imagin that there was a time when records even with array and sub-records in it where copied by using r2 := r1?

we have tones of files and statements like that which would make code not working as it idoes today without that deep copy implemented in the compiler code generator;

maybe that was the reason while certain institutes loved to have OpenVMS PASCAL (Niklaus Wirth and Catlin Jensen é all) at DECtimes; They made things like that happen.

My mind is that a new generation of PASCAL should not drop old proven goodies. But I agree, for a compiler machine/byte-code generator maker, in a new OO world, this creates a lot more of not so sexy work to be done.

looking at a " := " operator this has also to do with a change of semantics and behavior from simple data types to complex data types.

We have tones of record types used that way.
now we have to add to each such type the deep copy operator,
second we have to find all places where one record-var

rec2 := rec1;

goes into

rec1.CopyTo(rec2);

so I do not agree on: “the objects themselves will not be cloned (who could they, there’s no safe standard mechanism for that).”

How was that done in the past then with i.e. OpenVMS PASCAL?

But thanks to clarify this; I know now what I have to do at least.

  1. There were no objects.
  2. Arrays were value types.

and why are arrays no longer was they where before?

I can even not sub-type such an array-class because they are sealed like the .NET String and StringBuilder class. for me sealing is “hidding un-finished work !” and BTW: "this makes it impossible to finish his/here work by sub-typing the class in question.

further to that, going with an array from a value type to a class/object type without keeping the old behavior is just - yea, I let it for the user what that is. Definitly not so professional at best.

i don’t know. how was it done?

ask Anders Hejlsberg and the rest of the guys on the .NET team.

it’s not like we changed that recently and broke stuff. it has been like that from version 1.0.

Because arrays-as-value-types are

  1. horrendously inefficient (if you pass one as a method parameter it has to copy the entire array) and
  2. a security risk. Ever hear of buffer overflow exploits? The #1 cause is placing arrays-as-value-types on the stack, and managed code is supposed to make buffer overflows (among other problems) impossible.
3 Likes

A language is a language; And if you change the semantics of a existing syntaxes you cause more problems then you think fixing problems of new frameworks like .NET.

How secure .NET is, can be seen best by how many security patches per months MS has to deliver.

Efficiency is no excuse to missing, dropped or changed functionality. That to deside is by the developer, and second why dod we have introduced concepts “by value” and “by reference” for passing parameters. Again, this is a developer issue.

That’s really a pretty orthogonal discussion (and also, MS delivers pretty few security patches for .NET per se (opposed to for Windows. But even if they did/had to, that’s really nothing to do with the saftely of user-written code.

Yes. this one is Oxygene. not OpenVMS PASCAL (which, as far as i can judge from my limited exposure to it form your posts) is very different that most other Pascal dialects including Turbo/Borland Pascal and Delphi.

I can sympathize that you’re frustrated if things aren’t exactly as you’d like them to be in Oxygene, but at this point it really is a moot discussion. Oxygene has been shipping for almost 10 years now, and it uses the underlying Array type of the .NET framework (which, if you are developing for it, you should try to embrace, not fight), so it’s not like we’re going to change how something as fundamental as this works, now (and break every body’s code.

Sorry,
marc

Eeh yes please don’t change this…
We have 166000 lines of Oxygene code, and I like the arrays like they are just fine…