delphi - ¿Por qué falla este código al declarar TMemoryStream localmente pero funciona cuando se declara globalmente?
delphi-xe7 (1)
La siguiente función toma el texto seleccionado en un control Richedit
, escribe en un TMemoryStream
dentro de una función de devolución de llamada y luego devuelve como una cadena de texto sin formato el código rtf en bruto.
var
MS: TMemoryStream; // declared globally and works.
implementation
function GetSelectedRTFCode(RichEdit: TRichedit): string;
function RichEditCallBack(dwCookie: Longint; pbBuff: PByte;
CB: Longint; var pCB: Pointer): Longint; stdcall;
begin
MS.WriteBuffer(pbBuff^, CB);
Result := CB;
end;
var
EditStream: TEditStream;
SL: TStringList;
begin
MS := TMemoryStream.Create;
try
EditStream.dwCookie := SF_RTF or SFF_SELECTION;
EditStream.dwError := 0;
EditStream.pfnCallback := @RichEditCallBack;
Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, DWord(@EditStream));
MS.Seek(0, soBeginning);
SL := TStringList.Create;
try
SL.LoadFromStream(MS);
Result := SL.Text;
finally
SL.Free;
end;
finally
MS.Free;
end;
end;
Lo anterior funciona como se espera sin ningún error.
Sin embargo, trato de evitar las variables declaradas globalmente cuando sea posible y las mantengo locales para el procedimiento o función que las necesita, pero por algún motivo declaro MS: TMemoryStream;
dentro de la función GetSelectedRTFCode
falla con errores de instrucción privilegiada y violación de acceso.
Así que con eso en mente, y el único cambio a continuación ha sido MS: TMemoryStream;
declarado localmente falla:
function GetSelectedRTFCode(RichEdit: TRichedit): string;
var
MS: TMemoryStream; // declare here instead of globally but fails.
function RichEditCallBack(dwCookie: Longint; pbBuff: PByte;
CB: Longint; var pCB: Pointer): Longint; stdcall;
begin
MS.WriteBuffer(pbBuff^, CB);
Result := CB;
end;
var
EditStream: TEditStream;
SL: TStringList;
begin
MS := TMemoryStream.Create;
try
EditStream.dwCookie := SF_RTF or SFF_SELECTION;
EditStream.dwError := 0;
EditStream.pfnCallback := @RichEditCallBack;
Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, DWord(@EditStream));
MS.Seek(0, soBeginning);
SL := TStringList.Create;
try
SL.LoadFromStream(MS);
Result := SL.Text;
finally
SL.Free;
end;
finally
MS.Free;
end;
end;
¿Por qué la declaración de la variable de flujo de memoria funciona globalmente, pero falla cuando se declara localmente?
El problema es que su uso de una función anidada como devolución de llamada es incorrecto. Por una posibilidad de implementación utilizando una función anidada de esa manera, funciona para el compilador de 32 bits, siempre que la función anidada no se refiera a ninguna variable local de las funciones circundantes.
Sin embargo, tan pronto como la función anidada se refiere a cualquiera de estas variables locales, entonces se debe pasar un parámetro oculto adicional para que la función anidada pueda acceder a los marcos de pila de funciones circundantes. Y para el compilador de 64 bits, siempre se pasa un parámetro extra oculto.
Encontrará muchos ejemplos en la web donde las personas demuestran que pasan funciones anidadas como devoluciones de llamada. Pero todos estos ejemplos rompen las reglas documented del lenguaje:
Los procedimientos y funciones anidadas (rutinas declaradas dentro de otras rutinas) no pueden usarse como valores de procedimiento, ni pueden ser procedimientos y funciones predefinidos.
Lo que debe hacer es dejar de usar funciones anidadas para las devoluciones de llamada. Debe declarar que la función de devolución de llamada tiene alcance global. Pase el flujo de memoria a través del miembro dwCookie
de la estructura EDITSTREAM
.
// This compiles now, but the callback implementation is wrong, see below
function RichEditCallBack(dwCookie: DWORD_PTR; pbBuff: PByte;
CB: Longint; var pCB: Longint): Longint; stdcall;
var
MS: TMemoryStream;
begin
MS := TMemoryStream(dwCookie);
MS.WriteBuffer(pbBuff^, CB);
Result := CB;
end;
function GetSelectedRTFCode(RichEdit: TRichedit): string;
var
MS: TMemoryStream;
EditStream: TEditStream;
SL: TStringList;
begin
MS := TMemoryStream.Create;
try
EditStream.dwCookie := DWORD_PTR(MS);
EditStream.dwError := 0;
EditStream.pfnCallback := RichEditCallBack;
Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, LPARAM(@EditStream));
MS.Seek(0, soBeginning);
SL := TStringList.Create;
try
SL.LoadFromStream(MS);
Result := SL.Text;
finally
SL.Free;
end;
finally
MS.Free;
end;
end;
Tenga en cuenta, en particular, que no he usado el operador @
para obtener la dirección de la función de devolución de llamada. El uso del operador @
en una función resulta en una supresión de la verificación de tipo. Si no hubiera utilizado el operador @
, entonces el compilador habría podido decirle sus errores.
El compilador habría dicho:
[dcc32 Error] E2094 Local procedure/function ''RichEditCallBack'' assigned to procedure variable
Y tenga en cuenta también que su código declara incorrectamente el tipo del parámetro final. Es un parámetro de referencia de tipo Longint
. Nuevamente, el compilador puede informar esto y lo hace, a menos que haya usado @
para obtener la dirección de la función.
Este segundo error conduce a la implementación de la devolución de llamada. Es incorrecto El valor de retorno indica éxito. Un valor de cero se utiliza para indicar el éxito, cualquier otro valor indica el fracaso. El número de bytes escritos debe devolverse a través del parámetro final. Su devolución de llamada debe verse así:
function RichEditCallBack(dwCookie: DWORD_PTR; pbBuff: PByte;
CB: Longint; var CBWritten: Longint): Longint; stdcall;
var
MS: TMemoryStream;
begin
MS := TMemoryStream(dwCookie);
CBWritten := MS.Write(pbBuff^, CB);
Result := IfThen(CB = CBWritten, 0, 1);
end;