Почему этот код терпит неудачу при объявлении TMemoryStream локально, но работает, когда объявлен глобально?

Следующая функция принимает выделенный текст в элементе управления Richedit, записывает в TMemoryStream внутри функции обратного вызова, а затем возвращает в виде строки обычного кода необработанный код rtf.

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;

Вышеуказанное работает как ожидалось без ошибок.

Тем не менее, я стараюсь избегать глобально объявленных переменных, когда это возможно, и сохранять их локальными в той процедуре или функции, которая им нужна, но по какой-то причине объявление MS: TMemoryStream; внутри функции GetSelectedRTFCode не выполняется с ошибками Priviliged Instruction и Access Violation.

Таким образом, имея в виду, и единственное изменение ниже MS: TMemoryStream;, объявленное локально, не выполняется:

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;

Почему объявить переменную потока памяти глобально, но не удается, если объявлено локально?

Ответы

Ответ 1

Проблема заключается в неправильном использовании вложенной функции в качестве обратного вызова. Вероятность реализации с использованием вложенной функции таким образом работает для 32-битного компилятора, если вложенная функция не относится к каким-либо локальным переменным окружающих функций.

Однако, как только вложенная функция ссылается на любые такие локальные переменные, необходимо передать дополнительный скрытый параметр, чтобы вложенная функция могла получить доступ к фреймам стека окружающих функций. А для 64-битного компилятора всегда передается скрытый дополнительный параметр.

В Интернете вы найдете множество примеров, где люди демонстрируют передачу вложенных функций в качестве обратных вызовов. Но все эти примеры нарушают документированные правила языка:

Вложенные процедуры и функции (процедуры, объявленные в других подпрограммах) не могут использоваться как процедурные значения, а также не могут предопределяться процедуры и функции.

Вам нужно прекратить использование вложенных функций для обратных вызовов. Вы должны объявить функцию обратного вызова глобальной областью. Передайте поток памяти через член dwCookie структуры 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;

Обратите внимание, в частности, что я не использовал оператор @ для получения адреса функции обратного вызова. Использование оператора @ для функции приводит к подавлению проверки типов. Если бы вы не использовали оператор @, тогда компилятор мог бы сообщить вам свои ошибки.

Компилятор сказал бы:

[dcc32 Error] E2094 Local procedure/function 'RichEditCallBack' assigned to 
procedure variable

Также обратите внимание, что ваш код неправильно объявляет тип окончательного параметра. Это ссылочный параметр типа Longint. Опять же, компилятор может сообщить об этом и сообщить об этом, если вы не использовали @ для получения адреса функции.

Эта вторая ошибка приводит к реализации обратного вызова. Это неверно. Возвращаемое значение указывает на успех. Для обозначения успеха используется значение нуля, любое другое значение указывает на сбой. Количество записанных байтов должно быть возвращено через окончательный параметр. Ваш ответ должен выглядеть следующим образом:

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;