为什么此代码在本地声明 TMemoryStream 时失败,但在全局声明时有效?

Why does this code fail when declaring TMemoryStream locally but works when globally declared?

以下函数在 Richedit 控件中获取所选文本,写入回调函数内的 TMemoryStream,然后 returns 作为原始 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;

以上按预期工作,没有任何错误。

但是,我尽量避免全局声明的变量,并将它们保留在需要它的过程或函数的本地,但由于某种原因,在 GetSelectedRTFCode 函数中声明 MS: TMemoryStream; 失败并显示 Priviliged指令和访问冲突错误。

考虑到这一点,下面的唯一更改是 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;

为什么在全局声明内存流变量有效,但在局部声明时失败?

问题是你使用嵌套函数作为回调是错误的。通过以这种方式使用嵌套函数实现的机会适用于 32 位编译器,只要嵌套函数不引用周围函数的任何局部变量。

但是,一旦嵌套函数引用任何此类局部变量,就必须传递一个额外的隐藏参数,以便嵌套函数可以访问周围的函数堆栈帧。而对于64位编译器,总是传递一个隐藏的额外参数。

您会在网络上找到许多示例,人们在这些示例中展示了将嵌套函数作为回调传递。但是所有这些示例都违反了语言的 documented 规则:

Nested procedures and functions (routines declared within other routines) cannot be used as procedural values, nor can predefined procedures and functions.

你必须做的是停止使用嵌套函数进行回调。您需要将回调函数声明为具有全局范围。通过 EDITSTREAM 结构的 dwCookie 成员传递内存流。

// 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 类型的引用参数。同样,编译器可以报告这一点,并且确实报告这一点,除非您使用 @ 获取函数地址。

第二个错误导致回调的执行。这是不正确的。 return 值表示成功。零值表示成功,任何其他值表示失败。写入的字节数必须通过最终参数 returned。您的回调应如下所示:

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;