Fenner
A linguagem

Memória

Contagem de referências, weak, ligações dono/filho (Ref), listas intrusivas (ChainRoot), ponteiros, absolute e atômicos.

Objetos com contagem de referências#

Instâncias de classe são tipos gerenciados: o compilador gera AddRef/Release nas atribuições e na saída de escopo, e o objeto morre quando a última referência some. Não há coletor de lixo e não há pausa.

memory.fxp
program Memory;

type
  TNode = class(TObject)
  private
    FName: UString;
  public
    constructor Create(const AName: UString);
    destructor Destroy; override;
  end class;

  TChild = class(TObject)
  private
    weak FParent: TNode;      // weak: a ligação não conta referência
  public
    constructor Create(AParent: TNode);
  end class;

constructor TNode.Create(const AName: UString);
begin
  FName := AName;
end constructor;

destructor TNode.Destroy;
begin
  WriteLn('  destruído: ', FName);
  inherited Destroy;
end destructor;

constructor TChild.Create(AParent: TNode);
begin
  FParent := AParent;
end constructor;

procedure Scope;
var
  A, B: TNode;
  Child: TChild;
begin
  A := TNode.Create('nó A');
  B := A;                       // duas referências ao mesmo objeto
  Child := TChild.Create(A);    // weak: não segura o pai vivo
  A := nil;
  WriteLn('  A := nil, mas B ainda segura o objeto');
end procedure;                  // B sai do escopo: o objeto morre

begin
  WriteLn('entrando:');
  Scope;
  WriteLn('depois do escopo');
end.
Saída
entrando:
  A := nil, mas B ainda segura o objeto
  destruído: nó A
depois do escopo
  • A destruição acontece no fim do escopo da última referência, de forma determinística: o destrutor roda ali, não "mais tarde".
  • Cadeias, vetores dinâmicos, interfaces e records gerenciados seguem o mesmo princípio.
  • weak na frente de um campo ou variável desliga a contagem: a ligação existe, mas não segura o objeto vivo. É o que quebra ciclos (o filho aponta o pai como weak).
  • Uma região {$code unsafe} desliga o rastreio de tudo o que está dentro dela: é o modo de escrever código de baixo nível (ponteiros, buffers) sem pagar a contagem, e é por isso que a RTL a usa em vários módulos. O contrário é {$code safe}.

Para achar vazamentos, o programa pode ligar o relatório de saída (ReportMemoryLeaksOnShutdown := True) e a configuração allocsite registra o arquivo e a linha de cada alocação; veja Diagnósticos.

Ligação dono/filho: Ref e Refs#

Ref(TFilho, FCampoDoPai) declara uma ligação de mão dupla: o dono aponta o filho, o filho aponta de volta, e os dois lados se acertam sozinhos.

refs.fxp
program Refs;

type
  TCustomer = class;
  TAddress = class;

  TCustomer = class(TObject)
  private
    // ligação de mão dupla: o dono aponta o filho e o filho aponta de volta
    FAddress: Ref(TAddress, FCustomer);
  public
    property Address: TAddress read FAddress write FAddress;
  end class;

  TAddress = class(TObject)
  private
    weak FCustomer: TCustomer;     // pelo menos um dos lados é weak
  public
    property Customer: TCustomer read FCustomer;
  end class;

var
  C: TCustomer;
  A, B: TAddress;
begin
  C := TCustomer.Create;
  A := TAddress.Create;
  B := TAddress.Create;

  C.Address := A;                  // liga os dois lados de uma vez
  WriteLn('A.Customer = C?  ', A.Customer = C);

  C.Address := B;                  // desliga A e liga B
  WriteLn('A.Customer = nil? ', A.Customer = nil);
  WriteLn('B.Customer = C?  ', B.Customer = C);

  C.Address := nil;                // atribuir nil desliga
  WriteLn('B.Customer = nil? ', B.Customer = nil);
end.
Saída
A.Customer = C?  TRUE
A.Customer = nil? TRUE
B.Customer = C?  TRUE
B.Customer = nil? TRUE

O campo tem o tipo do filho: uma referência de classe já é um ponteiro de 8 bytes. Ler chama _Ref_GetValue; escrever chama _Ref_SetValue, que desliga o filho anterior dos dois lados e liga o novo; atribuir nil desliga; os destrutores desfazem a ligação dos dois lados.

Duas regras que o compilador recusa se você quebrar:

  • Pelo menos um dos dois lados é weak. Duas ligações fortes fariam um ciclo que nenhum dos dois solta. O weak na frente do campo Ref enfraquece a ligação para o filho; no campo de volta, enfraquece a do pai.
  • Um filho, um dono: um segundo Ref para o mesmo campo de volta é erro.

Refs(TFilho, FCampoDoPai) é o irmão plural, com as mesmas regras. O campo não tem o tipo do filho (guarda uma lista) e responde por cinco membros: Count, GetItem(I), Add(X), Remove(X) e Clear. Destruir o dono chama Clear; destruir um filho o tira da lista do dono.

Listas intrusivas: ChainRoot e Chain#

Uma lista encadeada que não aloca nada: os ponteiros de ligação moram dentro do próprio elemento. A raiz mora na classe dona; o elo, na classe do elemento.

chains.fxp
program Chains;

type
  TParcel = class;

  TInvoice = class(TObject)
  private
    // lista intrusiva: não aloca nada, os elos moram dentro de cada TParcel
    FParcels: ChainRoot(TParcel, FChainInInvoice);
  public
    procedure Add(const AParcel: TParcel);
    function Total: Int32;
    function Count: NativeInt;
  end class;

  TParcel = class(TObject)
  public
    Value: Int32;
    FChainInInvoice: Chain;
    constructor Create(const AValue: Int32);
  end class;

constructor TParcel.Create(const AValue: Int32);
begin
  Value := AValue;
end constructor;

procedure TInvoice.Add(const AParcel: TParcel);
begin
  FParcels.AddLast(AParcel);    // a cadeia retém o elemento
end procedure;

function TInvoice.Total: Int32;
var
  P: TParcel;
begin
  Result := 0;
  for P in FParcels do          // do primeiro ao último
    Inc(Result, P.Value);
end function;

function TInvoice.Count: NativeInt;
begin
  Exit(FParcels.GetCount);
end function;

var
  Invoice: TInvoice;
begin
  Invoice := TInvoice.Create;
  Invoice.Add(TParcel.Create(100));
  Invoice.Add(TParcel.Create(250));
  Invoice.Add(TParcel.Create(50));
  WriteLn(Invoice.Count, ' parcelas, total ', Invoice.Total);
end.
Saída
3 parcelas, total 400

Por omissão, a cadeia retém os elementos: adicionar dá AddRef, remover dá Release. Com weak na raiz (weak FParcels: ChainRoot(...)) ela apenas aponta, o que serve para uma lista de observadores.

  • Na raiz: AddFirst, AddLast, AddAfter, AddBefore, Contains, GetCount, GetItem, IsEmpty, IsValidChain, Remove, Sort, TryExchange, TryExtractFirst, TryExtractLast, TryMoveToNext, TryMoveToPrevious, GetFirst, GetLast, Clear.
  • No elo: GetPrevious e GetNext.
  • Um terceiro parâmetro (ChainRoot(TParcel, FElo, FDocumento)) nomeia o campo do filho que aponta para o dono. Com ele, entrar e sair passam por uma reserva atômica desse campo: o mesmo objeto não entra em duas cadeias.
  • As posses são independentes: reter o filho (a cadeia) e reter o pai (o campo do dono) são decisões separadas, e cada weak responde por uma. As duas fortes são erro (ciclo).
  • Os destrutores são automáticos: o pai destruído esvazia a cadeia; o filho destruído se desliga do dono.

A varredura tem forma própria. O próximo elemento é lido antes do corpo, então tirar o corrente da cadeia lá dentro continua válido:

Fenner
for P in FParcels do        // do primeiro ao último
for P downin FParcels do    // do último ao primeiro

// e sobre uma cadeia que o próprio objeto oferece, pelas properties Next/Previous:
for N in chain(AFirst) do
for N downin chain(ALast) do

Eventos que se desligam sozinhos: Delegate#

Um campo of object cru fica apontando para memória morta se o alvo for destruído. Delegate(T) guarda o método na cadeia do objeto-alvo, e o destrutor de TObject zera o código de todos os que apontavam para ele. O weak diz se a ligação conta: um evento normalmente não deve manter vivo quem o escuta.

delegates.fxp
program Delegates;

type
  TNotifyEvent = procedure(const ASender: TObject) of object;

  TButton = class(TObject)
  public
    // Delegate: o evento que se desliga sozinho quando o alvo morre
    weak FOnClick: Delegate(TNotifyEvent);
    procedure Click;
  end class;

  TTarget = class(TObject)
  public
    procedure Pressed(const ASender: TObject);
    destructor Destroy; override;
  end class;

procedure TButton.Click;
begin
  FOnClick!(Self);
end procedure;

procedure TTarget.Pressed(const ASender: TObject);
begin
  WriteLn('  Pressed');
end procedure;

destructor TTarget.Destroy;
begin
  WriteLn('  alvo destruído');
  inherited Destroy;
end destructor;

var
  Button: TButton;
  Target: TTarget;
begin
  Button := TButton.Create;
  Target := TTarget.Create;
  Button.FOnClick := Target.Pressed;

  WriteLn('com o alvo vivo:');
  Button.Click;

  Target := nil;                // weak: a ligação não segura o alvo, que morre
  WriteLn('depois que o alvo morreu:');
  Button.Click;                 // nada acontece, e nenhuma memória morta é tocada
end.
Saída
com o alvo vivo:
  Pressed
  alvo destruído
depois que o alvo morreu:

Num Delegate de função, desligado não chama e devolve o valor zero do tipo; quem precisa distinguir "ninguém ligado" de "respondeu zero" testa o campo contra nil antes.

Ponteiros#

Veja também Tipos. New/Dispose alocam um tipo, GetMem/FreeMem/ReallocMem blocos, e a aritmética de ponteiro está disponível.

Sobreposição: absolute#

A variável declarada com absolute não tem armazenamento próprio: é uma vista sobre o de outra. Não aloca, não inicializa e não finaliza; quem tem o valor é a base.

absolute.fxp
program AbsoluteDemo;

procedure Demo;
var
  P: UInt64;
  X: UInt64 absolute P;       // o mesmo lugar que P
  B: UInt8 absolute P;        // o byte de baixo
begin
  P := $1122334455667788;
  WriteLn('B = ', B);         // $88 = 136

  X := 7;                     // escrever pela vista muda P
  WriteLn('P = ', P);

  B := 1;
  WriteLn('P = ', P);         // só o byte de baixo mudou
end procedure;

begin
  Demo;
end.
Saída
B = 136
P = 7
P = 1

Em código seguro a sobreposição exige que nenhum dos dois lados seja gerenciado (senão seriam duas contagens de referência que não se conhecem) e que a vista não seja maior que a base. Dentro de {$code unsafe} nada disso é cobrado. absolute não é palavra reservada: naquela posição nenhum identificador seria válido, então ela segue servindo de nome.

Os atômicos#

São formas da linguagem: o compilador emite atomicrmw e cmpxchg, que no x86-64 viram lock xchg, lock cmpxchg, lock add e lock sub. Uma instrução cada, sem chamada.

atomics.fxp
program Atomics;

var
  Counter: Int32;
  Old: Int32;

begin
  // cada uma vira uma única instrução com "lock" (sem chamada)
  AtomicIncrement(Counter);
  AtomicIncrement(Counter, 10);
  WriteLn('Counter = ', Counter);

  Old := AtomicExchange(Counter, 100);
  WriteLn('antes: ', Old, ', agora: ', Counter);

  // compara e troca: só grava se o valor atual for o esperado (o último argumento)
  Old := AtomicCmpExchange(Counter, 555, 100);
  WriteLn('trocou? ', Old = 100, ' Counter = ', Counter);
  Old := AtomicCmpExchange(Counter, 999, 100);
  WriteLn('trocou? ', Old = 100, ' Counter = ', Counter);
end.
Saída
Counter = 11
antes: 11, agora: 100
trocou? TRUE Counter = 555
trocou? FALSE Counter = 555
Fenner
AtomicExchange(Alvo, Novo)
AtomicCmpExchange(Alvo, Novo, Comparando [, out Sucesso])
AtomicIncrement(Alvo [, Passo])
AtomicDecrement(Alvo [, Passo])

Valem como comando e aceitam inteiro de 1, 2, 4 e 8 bytes; Exchange e CmpExchange aceitam também ponteiro, classe, interface e enumerado. Increment e Decrement devolvem o valor novo, como no Delphi. Se a RTL declarar _AtomicExchange_<Tipo> (ou as irmãs), o compilador chama aquilo em vez de emitir a instrução; faltar o gancho não é erro.