¿Por qué no se puede llevar la dirección a una función local anidada en Delphi de 64 bits?

Resuelto Sertac Akyuz asked hace 12 años • 1 respuestas

COMO. desde que se cerraron las preguntas relacionadas; se agregan más ejemplos a continuación.

El siguiente código simple (que encuentra una ventana de nivel superior, Es decir, y enumera sus elementos secundarios) funciona bien con una plataforma de destino 'Windows de 32 bits'. Tampoco hay problema con versiones anteriores de Delphi:

procedure TForm1.Button1Click(Sender: TObject);

  function EnumChildren(hwnd: HWND; lParam: LPARAM): BOOL; stdcall;
  const
    Server = 'Internet Explorer_Server';
  var
    ClassName: array[0..24] of Char;
  begin
    Assert(IsWindow(hwnd));            // <- Assertion fails with 64-bit
    GetClassName(hwnd, ClassName, Length(ClassName));
    Result := ClassName <> Server;
    if not Result then
      PUINT_PTR(lParam)^ := hwnd;
  end;

var
  Wnd, WndChild: HWND;
begin
  Wnd := FindWindow('IEFrame', nil); // top level IE
  if Wnd <> 0 then begin
    WndChild := 0;
    EnumChildWindows(Wnd, @EnumChildren, UINT_PTR(@WndChild));

    if WndChild <> 0 then
      ..    

end;


Inserté un Assertpara indicar dónde falla con una plataforma de destino de 'Windows de 64 bits'. No hay ningún problema con el código si desanido la devolución de llamada.

No estoy seguro de si los valores erróneos pasados ​​con los parámetros son simplemente basura o se deben a algunas direcciones de memoria mal ubicadas (¿convención de llamada?). ¿Anidar devoluciones de llamadas es algo que nunca debería hacer en primer lugar? ¿O es sólo un defecto con el que tengo que vivir?

editar:
en respuesta a la respuesta de David, el mismo código se EnumChildWindowsdeclaró con una devolución de llamada escrita. Funciona bien con 32 bits:

(editar: lo siguiente realmente no prueba lo que dice David ya que todavía usé el operador '@'. Funciona bien con el operador, pero si lo elimino, de hecho no se compila a menos que desactive -anidar la devolución de llamada)

type
  TFNEnumChild = function(hwnd: HWND; lParam: LPARAM): Bool; stdcall;

function TypedEnumChildWindows(hWndParent: HWND; lpEnumFunc: TFNEnumChild;
    lParam: LPARAM): BOOL; stdcall; external user32 name 'EnumChildWindows';

procedure TForm1.Button1Click(Sender: TObject);

  function EnumChildren(hwnd: HWND; lParam: LPARAM): BOOL; stdcall;
  const
    Server = 'Internet Explorer_Server';
  var
    ClassName: array[0..24] of Char;
  begin
    Assert(IsWindow(hwnd));            // <- Assertion fails with 64-bit
    GetClassName(hwnd, ClassName, Length(ClassName));
    Result := ClassName <> Server;
    if not Result then
      PUINT_PTR(lParam)^ := hwnd;
  end;

var
  Wnd, WndChild: HWND;
begin
  Wnd := FindWindow('IEFrame', nil); // top level IE
  if Wnd <> 0 then begin
    WndChild := 0;
    TypedEnumChildWindows(Wnd, @EnumChildren, UINT_PTR(@WndChild));

    if WndChild <> 0 then
      ..

end;

En realidad, esta limitación no es específica de las devoluciones de llamada de la API de Windows, pero el mismo problema ocurre cuando se toma la dirección de esa función en una variable de procedural typey se pasa, por ejemplo, como un comparador personalizado a TList.Sort.

http://docwiki.embarcadero.com/RADStudio/Rio/en/Procedural_Types

procedure TForm2.btn1Click(Sender: TObject);
var s : TStringList;

  function compare(s : TStringList; i1, i2 : integer) : integer;
  begin
    result := CompareText(s[i1], s[i2]);
  end;

begin
  s := TStringList.Create;
  try
    s.add('s1');
    s.add('s2');
    s.add('s3');
    s.CustomSort(@compare);
  finally
    s.free;
  end;
end;

Funciona como se esperaba cuando se compila en 32 bits, pero falla Access Violationcuando se compila para Win64. Para la versión de 64 bits en función compare, s = nil y i2= algún valor aleatorio;

También funciona como se esperaba incluso para el objetivo Win64, si se extrae comparela función fuera de btn1Clickla función.

Sertac Akyuz avatar Apr 15 '12 21:04 Sertac Akyuz
Aceptado

Este truco nunca fue admitido oficialmente por el lenguaje y hasta la fecha te has salido con la tuya debido a las características específicas de implementación del compilador de 32 bits. La documentación es clara:

Los procedimientos y funciones anidados (rutinas declaradas dentro de otras rutinas) no se pueden utilizar como valores de procedimiento.

Si no recuerdo mal, se pasa un parámetro adicional oculto a funciones anidadas con el puntero al marco de pila adjunto. Esto se omite en el código de 32 bits si no se hace referencia al entorno circundante. En código de 64 bits siempre se pasa el parámetro adicional.

Por supuesto, una gran parte del problema es que la unidad de Windows utiliza tipos de procedimientos sin tipo para sus parámetros de devolución de llamada. Si se utilizaran procedimientos escritos, el compilador podría rechazar su código. De hecho, veo esto como una justificación para creer que el truco que usted utilizó nunca fue legal. Con las devoluciones de llamada escritas nunca se puede utilizar un procedimiento anidado, ni siquiera en el compilador de 32 bits.

De todos modos, la conclusión es que no se puede pasar una función anidada como parámetro a otra función en el compilador de 64 bits.

David Heffernan avatar Apr 15 '2012 14:04 David Heffernan