O tratamento do compilador das variáveis ​​de interface implícitas está documentado?

86

Eu fiz uma pergunta semelhante sobre variáveis ​​de interface implícitas não há muito tempo.

A origem desta pergunta foi um bug em meu código devido ao fato de eu não estar ciente da existência de uma variável de interface implícita criada pelo compilador. Esta variável foi finalizada quando o procedimento que a possuía terminou. Isso, por sua vez, causou um bug devido ao tempo de vida da variável ser maior do que eu esperava.

Agora, tenho um projeto simples para ilustrar alguns comportamentos interessantes do compilador:

program ImplicitInterfaceLocals;

{$APPTYPE CONSOLE}

uses
  Classes;

function Create: IInterface;
begin
  Result := TInterfacedObject.Create;
end;

procedure StoreToLocal;
var
  I: IInterface;
begin
  I := Create;
end;

procedure StoreViaPointerToLocal;
var
  I: IInterface;
  P: ^IInterface;
begin
  P := @I;
  P^ := Create;
end;

begin
  StoreToLocal;
  StoreViaPointerToLocal;
end.

StoreToLocalé compilado exatamente como você imagina. A variável local I, o resultado da função, é passada como um varparâmetro implícito para Create. A organização dos StoreToLocalresultados em uma única chamada para IntfClear. Sem surpresas aí.

No entanto, StoreViaPointerToLocalé tratado de forma diferente. O compilador cria uma variável local implícita para a qual ele passa Create. Quando Createretorna, a atribuição a P^é realizada. Isso deixa a rotina com duas variáveis ​​locais contendo referências à interface. A arrumação para StoreViaPointerToLocalresultados em duas chamadas para IntfClear.

O código compilado para StoreViaPointerToLocalé assim:

ImplicitInterfaceLocals.dpr.24: begin
00435C50 55               push ebp
00435C51 8BEC             mov ebp,esp
00435C53 6A00             push $00
00435C55 6A00             push $00
00435C57 6A00             push $00
00435C59 33C0             xor eax,eax
00435C5B 55               push ebp
00435C5C 689E5C4300       push $00435c9e
00435C61 64FF30           push dword ptr fs:[eax]
00435C64 648920           mov fs:[eax],esp
ImplicitInterfaceLocals.dpr.25: P := @I;
00435C67 8D45FC           lea eax,[ebp-$04]
00435C6A 8945F8           mov [ebp-$08],eax
ImplicitInterfaceLocals.dpr.26: P^ := Create;
00435C6D 8D45F4           lea eax,[ebp-$0c]
00435C70 E873FFFFFF       call Create
00435C75 8B55F4           mov edx,[ebp-$0c]
00435C78 8B45F8           mov eax,[ebp-$08]
00435C7B E81032FDFF       call @IntfCopy
ImplicitInterfaceLocals.dpr.27: end;
00435C80 33C0             xor eax,eax
00435C82 5A               pop edx
00435C83 59               pop ecx
00435C84 59               pop ecx
00435C85 648910           mov fs:[eax],edx
00435C88 68A55C4300       push $00435ca5
00435C8D 8D45F4           lea eax,[ebp-$0c]
00435C90 E8E331FDFF       call @IntfClear
00435C95 8D45FC           lea eax,[ebp-$04]
00435C98 E8DB31FDFF       call @IntfClear
00435C9D C3               ret 

Posso imaginar por que o compilador está fazendo isso. Quando puder provar que a atribuição à variável de resultado não levantará uma exceção (ou seja, se a variável for local), ele usará a variável de resultado diretamente. Caso contrário, ele usa um local implícito e copia a interface assim que a função retorna, garantindo assim que não vazemos a referência em caso de uma exceção.

Mas não consigo encontrar nenhuma declaração sobre isso na documentação. É importante porque o tempo de vida da interface é importante e, como programador, você precisa ser capaz de influenciá-lo ocasionalmente.

Então, alguém sabe se existe alguma documentação desse comportamento? Se não, alguém tem mais conhecimento sobre isso? Como são tratados os campos de instância, ainda não verifiquei. Claro que eu poderia experimentar tudo sozinho, mas estou procurando uma declaração mais formal e sempre prefiro evitar depender de detalhes de implementação elaborados por tentativa e erro.

Atualização 1

Para responder à pergunta de Remy, foi importante para mim quando precisei finalizar o objeto por trás da interface antes de realizar outra finalização.

begin
  AcquirePythonGIL;
  try
    PyObject := CreatePythonObject;
    try
      //do stuff with PyObject
    finally
      Finalize(PyObject);
    end;
  finally
    ReleasePythonGIL;
  end;
end;

Como está escrito assim, está tudo bem. Mas no código real eu tinha um segundo local implícito que foi finalizado depois que o GIL foi lançado e explodiu. Resolvi o problema extraindo o código dentro do Acquire / Release GIL em um método separado e, assim, estreitei o escopo da variável de interface.

David Heffernan
fonte
8
Não sei por que isso foi rejeitado, exceto que a questão é realmente complexa. Votado por estar acima da minha cabeça. Eu sei que exatamente esse pedaço de arcano resultou em alguns erros sutis de contagem de referência em um aplicativo em que trabalhei um ano atrás. Um de nossos melhores geeks passou horas tentando descobrir isso. No final, contornamos isso, mas nunca entendemos como o compilador deveria funcionar.
Warren P
3
@Serg O compilador fez sua contagem de referência perfeitamente. O problema era que havia uma variável extra contendo uma referência que eu não conseguia ver. O que eu quero saber é o que leva o compilador a tomar uma referência tão extra, oculta.
David Heffernan
3
Eu entendo você, mas uma boa prática é escrever um código que não dependa dessas variáveis ​​extras. Deixe o compilador criar essas variáveis ​​o quanto quiser, um código sólido não deve depender disso.
kludg
2
Outro exemplo quando isso está acontecendo:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle
2
Estou tentado a chamar isso de bug do compilador ... os temporários devem ser limpos depois que saem do escopo, o que deve ser o final da atribuição (e não o final da função). Não fazer isso produz erros sutis, como você descobriu.
nneonneo

Respostas:

15

Se houver alguma documentação desse comportamento, provavelmente será na área de produção do compilador de variáveis ​​temporárias para conter resultados intermediários ao passar resultados de funções como parâmetros. Considere este código:

procedure UseInterface(foo: IInterface);
begin
end;

procedure Test()
begin
    UseInterface(Create());
end;

O compilador deve criar uma variável temporária implícita para conter o resultado de Create conforme é passado para UseInterface, para garantir que a interface tenha um tempo de vida> = o tempo de vida da chamada UseInterface. Essa variável temporária implícita será descartada no final do procedimento que a possui, neste caso, no final do procedimento Test ().

É possível que seu caso de atribuição de ponteiro caia no mesmo bloco que passar valores de interface intermediários como parâmetros de função, uma vez que o compilador não pode "ver" para onde o valor está indo.

Lembro que houve alguns bugs nessa área ao longo dos anos. Há muito tempo (D3? D4?), O compilador não fazia referência à contagem do valor intermediário. Funcionava na maioria das vezes, mas apresentava problemas em situações de alias de parâmetro. Uma vez que isso foi abordado, houve um acompanhamento sobre const params, eu acredito. Sempre houve um desejo de mover o descarte da interface de valor intermediário para o mais rápido possível após a instrução em que ela era necessária, mas não acho que isso tenha sido implementado no otimizador Win32 porque o compilador simplesmente não foi definido para lidar com descarte na instrução ou granularidade de bloco.

dthorpe
fonte
0

Você não pode garantir que o compilador não decidirá criar uma variável invisível temporal.

E mesmo se você fizer isso, a otimização desligada (ou mesmo os frames da pilha?) Pode bagunçar seu código perfeitamente verificado.

E mesmo se você conseguir revisar seu código em todas as combinações possíveis de opções de projeto - compilar seu código em algo como Lazarus ou mesmo uma nova versão do Delphi trará o inferno de volta.

A melhor aposta seria usar a regra "variáveis ​​internas não podem sobreviver à rotina". Normalmente não sabemos se o compilador criaria algumas variáveis ​​internas ou não, mas sabemos que tais variáveis ​​(se criadas) seriam finalizadas quando a rotina existir.

Portanto, se você tiver um código como este:

// 1. Some code which may (or may not) create invisible variables
// 2. Some code which requires release of reference-counted data

Por exemplo:

Lib := LoadLibrary(Lib, 'xyz');
try
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- May be not OK
end;

Então você deve apenas envolver o bloco "Trabalhar com interface" na sub-rotina:

procedure Work(const Lib: HModule);
begin
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
end; // <- Releases hidden variables (if any exist)

Lib := LoadLibrary(Lib, 'xyz');
try
  Work(Lib);
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- OK!
end;

É uma regra simples, mas eficaz.

Alex
fonte
No meu cenário, I: = CreateInterfaceFromLib (...) estava resultando em um local implícito. Portanto, o que você sugere não vai ajudar. Em qualquer caso, eu já demonstrei claramente uma solução alternativa para a questão. Um baseado no tempo de vida de locais implícitos controlados pelo escopo da função. Minha pergunta dizia respeito aos cenários que levariam aos locais implícitos.
David Heffernan
Meu ponto é que esta é uma pergunta errada para se fazer em primeiro lugar.
Alex
1
Você é bem-vindo a esse ponto de vista, mas deve expressá-lo como um comentário. Adicionar código que tenta (sem sucesso) reproduzir as soluções alternativas da questão, parece estranho para mim.
David Heffernan