stringdelphidelphi-2009virtualtreeviewtvirtualstringtree

Access Violation when assigning string in InitNode event of TVirtualStringTree


The given code which works without any problems in Delphi 2007. However in Delphi 2009 I am getting an exception.

Access violation shows read of address $00000000.

The problem exists only when assigning string, it works for numbers.

Also, when I am assigning Data.Text manually via the debugger options I am getting no AV - it works.

Honestly I am lost, anyone could help me with this please?

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, VirtualTrees, StdCtrls;

type
  TTest = record
    Text: String;
    Number: Integer;
  end;
  PTest = ^TTest;

type
  TTestArray = array of TTest;

type
  TForm1 = class(TForm)
    VirtualStringTree1: TVirtualStringTree;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure VirtualStringTree1InitNode(Sender: TBaseVirtualTree; ParentNode,
      Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  TestArray: array of TTest;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  SetLength(TestArray, 1);
  TestArray[0].Text := 'test';
  TestArray[0].Number := 12345;
  VirtualStringTree1.AddChild(VirtualStringTree1.RootNode, @TestArray[0]);

end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  VirtualStringTree1.NodeDataSize := SizeOf(TTest);

end;

procedure TForm1.VirtualStringTree1InitNode(Sender: TBaseVirtualTree;
  ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
  Data: PTest;
  NodeData: PPointer;
begin
  Data := Sender.GetNodeData(Node);
  NodeData := Sender.GetNodeData(Node);
  Data.Number := PTest(NodeData^)^.Number;
  Data.Text := PTest(NodeData^)^.Text; //crash here!
end;

end.

Solution

  • When you call AddChild(..., @TestArray[0]), you're only initializing the first four bytes of the node's data. That's the Text field. The Text field holds a pointer to a TTest structure. It's supposed to hold a string reference.

    The GetNodeData function returns a pointer to the node's data. The tree control has allocated a TVirtualNode record, and immediately after that, in consecutive memory, it has allocated NodeDataSize bytes for you to use, and GetNodeData returns the address of that space. You're supposed to treat that as a pointer to a TTest structure. And you do, for some of your code. It looks like you're trying to skirt the limitation that only the first four bytes of the structure get initialized when you call AddChild. (I can't say I recommend that. There are other ways to associate data with a node that don't require so much type punning.)

    You assign Data correctly for the way the node data is supposed to be used. You assign NodeData correctly for what it really holds at the time of initialization — a pointer to a pointer to a TTest structure. You correctly dereference NodeData to read the Number field, and you also read the Text field correctly. However, the Data.Text field can't be overwritten the way you have it:

    Data.Text := PTest(NodeData^)^.Text;
    

    The Data.Text field doesn't current hold a valid string value, but string variables are required to hold valid values at all times (or at least all times where there's a possibility they'll be read or written). To assign a string variable, the program increments the reference count of the new value and decrements the reference count of the old one, but since the "old value" in this case isn't really a string, there's no valid reference count to decrement, and even if there were, the memory at that location couldn't be freed anyway — it belongs to TestArray.

    There's a way around this, though. Copy the string in two steps. First, read the value from NodeData.Text into a spare string variable. Once you do that, you have no need for NodeData anymore, so you can overwrite the value it points to. If you set it to all-bits-zero, then you'll implicitly overwrite Data.Text as well, and with the value of an empty string. At that point, it's safe to overwrite as a string variable:

    tmp := PTest(NodeData^)^.Text;
    PTest(NodeData^) := nil;
    Data.Text := tmp;
    

    Another way around this is to re-arrange the order of the fields in the node data. Put the Integer field first, and the initialize Data.Number last instead of Data.Text. Integer values are always safe to overwrite, no matter their contents.

    Whatever you do, make sure you finalize the record in the OnFreeNode event:

    var
      Data: PTest;
    begin
      Data := Sender.GetNodeData;
      Finalize(Data^);
    end;
    

    That makes sure the string field gets its reference count reduced, if necessary.