Search code examples
delphiaccess-violationvcl

Determining the sender of WM_SYSCOMMAND with LParam 0 in Delphi VCL forms


The problem

When running an application written in C, that uses some dll's written in Delphi XE7, I run into an access violation in the following code, which is in the vcl.forms.pas of the vcl library.

procedure TCustomForm.CMAppSysCommand(var Message: TMessage);
{$IF NOT DEFINED(CLR)}
type
  PWMSysCommand = ^TWMSysCommand;
{$ENDIF}
begin
  Message.Result := 0;
  if (csDesigning in ComponentState) or (FormStyle = fsMDIChild) or
   (Menu = nil) or Menu.AutoMerge then
{$IF DEFINED(CLR)}
    with TWMSysCommand.Create(Message) do
{$ELSE}
    with PWMSysCommand(Message.lParam)^ do
{$ENDIF}
    begin
      SendCancelMode(nil);
      if SendAppMessage(CM_APPSYSCOMMAND, CmdType, Key) <> 0 then   //Here the debugger shows the access violation
        Message.Result := 1;
    end;
end;

The access violation occurs on the line with SendAppMessage, and seems to be caused by the fact that the Message.LParam is 0. The message is a WM_SYSCOMMAND message. Is there a way to track where this message originated? In the call stack, all functions are part of the VCL or system files.

This answer suggest that in general it is hard to trace the sender of a windows message. However, since in my case everything is within the same application, I hope that might make it easier.

What have I tried?

Overruling the vcl source

Previously, this same bug appeared in the forms.pas and was fixed by adding a copy of that file to the project and then checking that LParam <> 0 in this function. I have tried doing the same thing with the vcl.forms.pas that is now used, but this leads to compilation errors. Even with answers as here I was not able to build it. However, many google hits also suggested that it is in general a bad idea to change things in the vcl, so I try to avoid that option.

Other questions on StackOverFlow

This article gave me good information about the underlying system and how it might have occured that the Message.LParam is 0. However, I did not know how to find the source of the message or what class I should be looking for that generated it.

The solution

As described in Remy's accepted answer below, the immediate problem could be solved by having the class provide a CMAppSysCommand function to guard against LParam = 0.


Solution

  • What you describe should not be possible under normal conditions.

    There are only two places in the entire VCL where CM_APPSYSCOMMAND is sent from:

    1. TWinControl.WMSysCommand(), which is called when a UI control receives a WM_SYSCOMMAND message. The LParam of the CM_APPSYSCOMMAND message is never set to 0, it is set to a pointer to the TMessage record of the original WM_SYSCOMMAND message:

      Form := GetParentForm(Self);
      if (Form <> nil) and
        (Form.Perform(CM_APPSYSCOMMAND, 0, Winapi.Windows.LPARAM(@Message)) <> 0) then
        Exit;
      
    2. TCustomForm.CMAppSysCommand(), which is called when a Form receives a CM_APPSYSCOMMAND message. It forwards the message to the TApplication window (using SendAppMessage(), which just calls SendMessage(Application.Handle, ...) with the provided parameters):

      with PWMSysCommand(Message.lParam)^ do
      begin
        ...
        if SendAppMessage(CM_APPSYSCOMMAND, CmdType, Key) <> 0 then
          Message.Result := 1;
      end;
      

    The other question you mention explains how CM_APPSYSCOMMAND is used by the VCL, but does not say anything that would suggest how its LParam could ever be 0 in TCustomForm.CMAppSysCommand(), because it can't ever be 0 under normal circumstances. It can be 0 in TApplication.WndProc(), but that is perfectly OK.

    The only possibility I can think of would be if someone is manually sending a fake CM_APPSYSCOMMAND message (which is CM_BASE + 23 = $B017, aka WM_APP + $3017) directly to your TForm window. Only TWinControl should ever be doing that. And since TWinControl uses Perform() instead of SendMessage() for that send, you should be seeing TWinControl.WMSysCommand() on the call stack of TCustomForm.CMAppSysCommand(). If you do not, then the message is fake. And if it is being sent using SendMessage() instead of Perform(), there is no way to know where the message is coming from.

    However, in any case, this is very easy to guard against, without altering any VCL source code. Simply have your DLL's TForm class provide its own message handler for CM_APPSYSCOMMAND, either using the message directive, or by overriding the virtual WndProc() method. Either way, you can discard the message if the LParam is 0, eg:

    type
      TMyForm = class(TForm)
      ...
      private
        procedure CMAppSysCommand(var Message: TMessage); message CM_APPSYSCOMMAND;
      ...
      end;
    
    procedure TMyForm.CMAppSysCommand(var Message: TMessage);
    begin
      if Message.LParam = 0 then
        Message.Result := 0
      else
        inherited;
    end;
    

    type
      TMyForm = class(TForm)
      ...
      protected
        procedure WndProc(var Message: TMessage); override;
      ...
      end;
    
    procedure TMyForm.WndProc(var Message: TMessage);
    begin
      if (Message.Msg = CM_APPSYSCOMMAND) and (Message.LParam = 0) then
        Message.Result := 0
      else
        inherited;
    end;