Running a single instance of our program – Passing command line from one instance to another instance

01.2023

Some of my programs need to run as single instance. This means that if an instance of my program is already running and the user is trying to start another instance, the second instance should not run. However, before closing the second instance, I want to pass the command line parameters from that second instance to the first instance. This is necessary because the second instance might have been started when the user double clicked a file type that is associated with my program (my program is set to open that file type when that file type is double clicked in explorer).

There is some code that we need to write for this, but not much because my Delphi Light Saber library already provides support for it.
By opening the ccAppData.pas file we see that the TAppData class provides the following public methods:

property  SingleInstClassName: string read FSingleInstClassName;
procedure ResurectInstance(CONST CommandLine: string);
function  InstanceRunning: Boolean;
procedure SetSingleInstanceName(var Params: TCreateParams);
function ExtractData(VAR Msg: TWMCopyData; OUT s: string): Boolean;

How does it work?

During start up, our app uses SetSingleInstanceName to give itself a unique name.
When the user double clicks a file in Explorer (that will open our app) or simply starts our app, we need to check if this is the only instance running in the system or if there is already and instance running. If there is already an instance running, then this second instance needs to close itself. If not, we let this (first) instance run.
We detect an already running instance with InstanceRunning().
But before this second instance closes itself, it needs to send the command line to the first instance. For this we use ResurectInstance. This method uses SendMessage to send a WM_COPYDATA message to the first instance. Inside WM_COPYDATA we put the data we want to send (the command line parameter).

Let’s see the code

We modify the DPR of our project to something like this:

 AppData:= TAppDataEx.Create(‘My cool app’, ctAppWinClassName);

 if AppData.InstanceRunning
 then TAppData.ResurectInstance(ParamStr(1))
 else
  begin
   Application.Initialize;
   Application.CreateForm(TMainForm, MainForm);
   Application.Run;
  end;

ctAppWinClassName is a constant representing our application’s name/ID. This string must be unique! No other application in the system can have the same ID. We use this unique ID in InstanceRunning() to detect if the application is already running, by looking for a window that has the specified class name (ctAppWinClassName):

{ True if an instance of this app was found already running }
function TAppData.InstanceRunning: Boolean;
begin
  Result:= WinApi.Windows.FindWindow(PWideChar(SingleInstClassName), NIL) > 0;
end;

In the MainForm.CreateParams we also need call SetSingleInstanceName to set the class name for our main form:

procedure TMainFrom.CreateParams(var Params: TCreateParams);
begin
inherited;
AppData.SetSingleInstanceName(Params);
end;

The main form also needs to implement this code in WMCopyData. WMCopyData is the message handler that responds to the CopyData message that is sent to our instance when data is passed to it (from the second instance):

{ DATA RECEIVER }
procedure TMainFrom.WMCopyData(var Msg: TWMCopyData);
VAR FileName: string;
begin
if AppData.ExtractData(Msg, FileName)
then Open(FileName)
else inherited;
end;

Yes, the technique might seem complicated. If you don’t fully understand it, no problem: you can still use it with the help from my Light Saber lib. Just modify the program in the three places as shown above. 😊

Why WM_COPYDATA?

Warning: TlDr: We should never use PostMessage() with the WM_COPYDATA message because the data that is passed to the receiving application is only valid during the call. Other methods require shared memory or other IPC mechanisms. Finally, we need to be aware that the call to SendMessage will not return until the message is processed. 

1. Safe Data Sharing Across Processes

  • WM_COPYDATA is specifically designed for safely sending data between two different processes.
  • When you send a pointer (lParam) to another process using a regular message, you’re sending a memory address from your process’s address space—it’s probably meaningless or even dangerous in the recipient’s process.
  • WM_COPYDATA is handled specially by Windows:
    • The OS ensures that the memory referenced by lpData is accessible during the SendMessage call, temporarily mapping it so the recipient can safely read it.
    • This is why you must use SendMessage (not PostMessage) for WM_COPYDATA: the data is only valid for the duration of the call.

2. Guaranteed Lifetime

  • Windows guarantees that the pointer in lpData is valid for the recipient to read only while SendMessage is blocking.
  • PostMessage would queue the message, and by the time it’s processed, the sender’s buffer may be gone—leading to crashes or garbage data.

3. Custom Messages Don’t Work for Data Transfer

  • If you use a custom message (e.g., WM_USER + 123) and put a pointer in lParam, it only works if both sender and receiver are in the same process.
  • Across processes, the pointer is invalid in the recipient’s context—so you’d have to use shared memory, files, or other IPC (inter-process communication) mechanisms.

4. WM_COPYDATA Structure

  • The OS expects the TCopyDataStruct format, which includes the length and type of data.
  • Windows can then safely marshal the data for you.

In Short:

Method Safe Between Processes? Data Lifetime Guaranteed? OS Handles Pointer Mapping?
WM_COPYDATA + SendMessage
Custom Message + lParam ptr
  • WM_COPYDATA is the only message where Windows guarantees the recipient can safely access the data pointed to by lpData, even if sender and receiver are in different processes.
  • For custom messages, the pointer in lParam is meaningless across processes.

 

Recommendation:
1. The receiver procedure should also restore (bring to front) that instance.
2. We should close this second instance after we sent the message to the first instance.

Alternatives

Note that the cmMutexSingleInstance.pas implements an alternative way of doing this, this time using a mutex.


Further reading

Raymond Chen: Why WM_COPYDATA is special

Leave a Comment

Scroll to Top