Communicating between scripts (IPC, inter-process communication)

Helpful script writing tricks and HowTo's
Descolada
Posts: 1155
Joined: 23 Dec 2021, 02:30

Communicating between scripts (IPC, inter-process communication)

10 Jan 2024, 01:43

Communicating between scripts

There are many ways of communicating between scripts with caveats to each method. This post attempts to outline some (but not all!) of the possible ways with their associated pros and cons.

I personally usually use SendMessage, because it is fast, allows sending arbitrary data, doesn't require constant monitoring/waiting for the data, and can easily receive feedback (the return value).
To synchronize access to a shared resource it's best to use a mutex or a semaphore.

Table of contents
1) File
2) Registry
3) Clipboard
4) PostMessage
5) SendMessage, SendMessageCallback, WM_COPYDATA
6) ObjRegisterActive
7) Mutex
8) Semaphore
9) Named shared memory
10) Named pipe
11) Socket
12) GWLP_USERDATA, atoms



1) File
Writing to a shared file seems like one of the simplest options to use. Suppose we have two scripts Script1 and Script2, and both scripts are periodically doing a task where they activate Window1 or Window2 and do some actions (send keystrokes etc). Of course we can't activate two windows at the same time, so the scripts must talk to each other to prevent colliding.

One possible solution would be to write to a file: Script1 writes SCRIPT1, so if Script2 reads the file and sees SCRIPT1 it knows to wait until the file is empty or deleted. However, what would happen if Script2 reads the file at the exact time Script1 is opening it to write? In that case the file would be empty and Script2 would erronously decide to write SCRIPT2 into it! Fortunately FileOpen provides a way to prevent that: namely we can use the "Sharing mode flags" to lock the file for read-writes.

Pros: easy to use; easy to debug (can open and inspect the file manually); stored data is persistant over reboots.
Cons: very slow; too many writes to a file can wear out the hard drive (over millions of writes); fairly resource intensive; insecure (the user or other programs can access the data)

Script1.ahk locks the file "lock.txt" until F1 is pressed:

Code: Select all

#Requires AutoHotkey v2.0

f := FileOpen("lock.txt", "a-rwd")
f.Write("SCRIPT1")
MsgBox "I have locked the file until F1 is pressed"
Persistent()

F1::ExitApp
Script2.ahk waits until the lock is lifted:

Code: Select all

#Requires AutoHotkey v2.0

MsgBox "I will wait until lock.txt is available"
Loop {
    try {
        ; FileOpen will throw an error if the file is already being accessed
        f := FileOpen("lock.txt", "a-rwd") 
        f.Write("SCRIPT2")
        f.Close()
        break
    }
    Sleep 200 ; Retry every 200 milliseconds
}
MsgBox "Script2 now has access to lock.txt"
2) Registry
Using the registry to share information is similar to using a file: we can use RegWrite to write a key and RegRead to read it. However, there is no good way to "lock" the key to prevent two scripts from writing to the same key at the same time! This means that registry can only safely be used for one-way communication: only one script broadcasting information to other scripts.
I recommend using HKEY_CURRENT_USER as the chosen hive, otherwise you might run into user privilege problems.

Pros: easy to use; moderately easy to debug (via RegEdit); stored data is persistant over reboots.
Cons: very slow; insecure (the user or other programs can access the data); limited data types (mostly strings and numbers); if too many registry keys are created it may eventually lead to the whole system slowing down (though this usually isn't an issue to worry about).

Script1.ahk

Code: Select all

RegWrite "I am doing this", "REG_SZ", "HKEY_CURRENT_USER\SOFTWARE\AHKScripts", "Script1"
Script2.ahk

Code: Select all

MsgBox "Script1 state: " RegRead("HKEY_CURRENT_USER\SOFTWARE\AHKScripts", "Script1", "")
3) Clipboard
The clipboard is an easily accessible and easily modifiable shared resource that can also be used for communication. It can also be easily interfered by other scripts, user input etc, so isn't too reliable.

First run ClipRead.ahk, then run ClipWrite.ahk
ClipRead.ahk

Code: Select all

#Requires AutoHotkey v2

; Start monitoring for Clipboard changes
OnClipboardChange(ClipChange)

ClipChange(DataType) {
    if DataType != 1 ; We are only concerned with textual data right now
        return

    data := A_Clipboard
    if SubStr(data, 1, 20) != "AHKClipboardMessage:" ; The message needs to contain our unique identifier
        return

    OnClipboardChange(ClipChange, 0) ; Don't call again
    A_Clipboard := "" ; Emptying the Clipboard signals that we got the message
    MsgBox "Received data: " SubStr(data, 21)
    ExitApp
}
ClipWrite.ahk

Code: Select all

#Requires AutoHotkey v2

; Save the current clipboard to be restored later
ClipSave := ClipboardAll()
; Set the clipboard to our message and specify an unique identifier (in this case "AHKClipboardMessage:")
; This will trigger ClipChange in ClipRead.ahk
A_Clipboard := "AHKClipboardMessage:Sending this text via the Clipboard"
; The clipboard should be emptied by ClipRead.ahk, so set up a monitor to detect that
OnClipboardChange(ClipChange)
; But in case the message wasn't received, restore the Clipboard anyway in 500ms
SetTimer(RestoreClipboard, -500)

ClipChange(DataType) {
    global ClipSave
    if DataType != 0 ; We are only concerned whether the clipboard was emptied
        return

    SetTimer(RestoreClipboard, 0) ; Disable the timer, it isn't necessary anymore
    RestoreClipboard()
    MsgBox "ClipWrite.ahk: message was received by ClipRead.ahk"
}

RestoreClipboard() {
    OnClipboardChange(ClipChange, 0)
    A_Clipboard := ClipSave
}
4) PostMessage
With this method we can send short messages to other scripts (if we know their window title or handle), even broadcast messages globally such as a new script being opened.

Pros: relatively easy to use; fast; event-driven, meaning we don't need to constantly monitor for the messages; messages can be broadcast globally without needing a target window
Cons: limited data size

Receiver.ahk waits for new scripts being opened that send the message "NewAHKScript" specifically

Code: Select all

#Requires AutoHotkey v2.0

; Register a new window message with the custom name "NewAHKScript"
MsgNum := DllCall("RegisterWindowMessage", "Str", "NewAHKScript")
OnMessage(MsgNum, NewScriptCreated)
Persistent()

NewScriptCreated(wParam, lParam, msg, hwnd) {
    MsgBox "New script with hWnd " hwnd " created!`n`nwParam: " wParam "`nlParam: " lParam
}
Sender.ahk broadcasts that it has opened, at the same time communicating information about it (123 and -456)

Code: Select all

#Requires AutoHotkey v2.0

; The receiver script should have created a message with name "NewAHKScript", so get its number
MsgNum := DllCall("RegisterWindowMessage", "Str", "NewAHKScript")
PostMessage(MsgNum, 123, -456,, 0xFFFF) ; HWND_BROADCAST := 0xFFFF
5) SendMessage, SendMessageCallback, WM_COPYDATA
With SendMessage we can send data to a specific script and wait for its reply. Regular SendMessage can send two numbers like PostMessage can, but using WM_COPYDATA it can send arbitrary data (strings, structures etc., see Example 4 for OnMessage).

Pros: relatively easy to use; fast; event-driven, meaning we don't need to constantly monitor for the messages
Cons: needs a target window; limited data size (except WM_COPYDATA); messages need to be processed as soon as possible or otherwise it can cause slowdowns in both scripts (except with SendMessageCallback)

NOTE: WM_COPYDATA can only send primitive data structures: a singular non-nested structure containing only integers, floats, characters. Pointers (eg to strings or objects) inside a structure will not point to the correct memory location when sent via WM_COPYDATA, so it can't natively send arrays, objects, more than one string etc. This can still be achieved though, but requires manually copying data so it's linearly in the structure and adjusting the pointer values accordingly (using relative values instead of absolute memory locations).

Receiver.ahk (run first)

Code: Select all

#Requires AutoHotkey v2.0

; Register a new window message with the custom name "NewAHKScript"
MsgNum := DllCall("RegisterWindowMessage", "Str", "NewAHKScript")
OnMessage(MsgNum, NewScriptCreated)
Persistent()

NewScriptCreated(wParam, lParam, msg, hwnd) {
    Loop {
        ib := InputBox("Script with hWnd " hwnd " sent message:`n`nwParam: " wParam "`nlParam: " lParam "`n`nReply:", "Message")
        if ib.Result = "Cancel"
            return 0
        else if !IsInteger(IB.Value)
            MsgBox "The reply can only be a number", "Error"
        else
            return IB.Value
    }
}
Sender.ahk (script execution blocked until SendMessage completes)

Code: Select all

#Requires AutoHotkey v2.0
DetectHiddenWindows 1 ; Receiver.ahk is windowless

receiverhWnd := WinExist("Receiver.ahk ahk_class AutoHotkey")
; The receiver script should have created a message with name "NewAHKScript", so get its number
MsgNum := DllCall("RegisterWindowMessage", "Str", "NewAHKScript")
reply := SendMessage(MsgNum, 123, -456,, receiverhWnd,,,, 0)
MsgBox "Got reply: " reply
Sender.ahk (script execution continues after SendMessage):

Code: Select all

#Requires AutoHotkey v2.0
DetectHiddenWindows 1

receiverhWnd := WinExist("Receiver.ahk ahk_class AutoHotkey")
; The receiver script should have created a message with name "NewAHKScript", so get its number
MsgNum := DllCall("RegisterWindowMessage", "Str", "NewAHKScript")
DllCall("SendMessageCallback", "ptr", receiverHwnd, "uint", MsgNum, "ptr", 123, "ptr", -456, "ptr", CallbackCreate(SendAsyncProc), "ptr", 0)
TrayTip "Sent the message and am waiting for reply", "Sender.ahk"
Persistent()

; dwData is the same value as the last argument of SendMessageCallback (in this example, 0)
SendAsyncProc(hWnd, msg, dwData, result) {
    MsgBox "Got reply: " result
    ExitApp
}
6) ObjRegisterActive
ObjRegisterActive can be used to share an AHK object between scripts, and any modifications to the object will also be shared. Since objects are stored in memory, reading and writing to this will be very fast. The downside is that it's hard to monitor for changes in the object, and similarly to using a registry it has concurrency issues (if both scripts are attempting to read-write at the same time) so for example a mutex might be needed to coordinate access.

Register.ahk:

Code: Select all

#Requires AutoHotkey v2

sharedObj := {key:"test"}
ObjRegisterActive(sharedObj, "{EB5BAF88-E58D-48F9-AE79-56392D4C7AF6}")
Persistent()

/*
    ObjRegisterActive(Object, CLSID, Flags:=0)
    
        Registers an object as the active object for a given class ID.
    
    Object:
            Any AutoHotkey object.
    CLSID:
            A GUID or ProgID of your own making.
            Pass an empty string to revoke (unregister) the object.
    Flags:
            One of the following values:
              0 (ACTIVEOBJECT_STRONG)
              1 (ACTIVEOBJECT_WEAK)
            Defaults to 0.
    
    Related:
        http://goo.gl/KJS4Dp - RegisterActiveObject
        http://goo.gl/no6XAS - ProgID
        http://goo.gl/obfmDc - CreateGUID()

    Author: lexikos (https://www.autohotkey.com/boards/viewtopic.php?f=6&t=6148)
*/
ObjRegisterActive(obj, CLSID, Flags:=0) {
    static cookieJar := Map()
    if (!CLSID) {
        if (cookie := cookieJar.Remove(obj)) != ""
            DllCall("oleaut32\RevokeActiveObject", "uint", cookie, "ptr", 0)
        return
    }
    if cookieJar.Has(obj)
        throw Error("Object is already registered", -1)
    _clsid := Buffer(16, 0)
    if (hr := DllCall("ole32\CLSIDFromString", "wstr", CLSID, "ptr", _clsid)) < 0
        throw Error("Invalid CLSID", -1, CLSID)
    hr := DllCall("oleaut32\RegisterActiveObject", "ptr", ObjPtr(obj), "ptr", _clsid, "uint", Flags, "uint*", &cookie:=0, "uint")
    if hr < 0
        throw Error(format("Error 0x{:x}", hr), -1)
    cookieJar[obj] := cookie
}
AccessShared.ahk:

Code: Select all

x := ComObjActive("{EB5BAF88-E58D-48F9-AE79-56392D4C7AF6}")
MsgBox "Shared object key: " x.key
An unique CLSID (or GUID) can be created with the CreateGUID function:

Code: Select all

; https://www.autohotkey.com/boards/viewtopic.php?f=6&t=4732
CreateGUID() {
    if !DllCall("ole32.dll\CoCreateGuid", "ptr", pguid := Buffer(16, 0)) {
        if (DllCall("ole32.dll\StringFromGUID2", "ptr", pguid, "ptr", sguid := Buffer(78, 0), "int", 78))
            return StrGet(sguid)
    }
    return ""
}
7) Mutex
"Mutex" stands for "mutual exclusion," and it is a synchronization mechanism to control access to shared resources, ensuring that only one script can access the resource (eg file, shared object, sending keys to a window) at a time.

A mutex typically has two main operations:

Lock (or signal): a script attempts to acquire the mutex before entering a critical section (eg using a shared resource). If the mutex is currently held by another script, the script execution may be blocked until the mutex becomes available.
Unlock (or release): After finishing the critical section, the script releases the mutex, allowing other scripts to acquire it.

NOTE: because AHK is single-threaded, a mutex can only be used to synchronize access between scripts, but not inside the same script! If that is needed then use a semaphore instead with a maximum count of 1.

First run Blocker.ahk (don't close the MsgBox) which locks the mutex, then Waiter.ahk which waits for the mutex to be unlocked, and close the MsgBox from Blocker.ahk to release the mutex.

Blocker.ahk

Code: Select all

#Requires AutoHotkey v2.0

mtx := Mutex("Local\MyMutex")
if (mtx.Lock() = 0)
    MsgBox "I am now blocking the Mutex until this MsgBox is closed"
Else
    MsgBox "Locking the Mutex failed"
mtx.Release()

class Mutex {
    /**
     * Creates a new Mutex, or opens an existing one. The mutex is destroyed once all handles to
     * it are closed.
     * @param name Optional. The name can start with "Local\" to be session-local, or "Global\" to be 
     * available system-wide.
     * @param initialOwner Optional. If this value is TRUE and the caller created the mutex, the 
     * calling thread obtains initial ownership of the mutex object.
     * @param securityAttributes Optional. A pointer to a SECURITY_ATTRIBUTES structure.
     */
    __New(name?, initialOwner := 0, securityAttributes := 0) {
        if !(this.ptr := DllCall("CreateMutex", "ptr", securityAttributes, "int", !!initialOwner, "ptr", IsSet(name) ? StrPtr(name) : 0))
            throw Error("Unable to create or open the mutex", -1)
    }
    /**
     * Tries to lock (or signal) the mutex within the timeout period.
     * @param timeout The timeout period in milliseconds (default is infinite wait)
     * @returns {Integer} 0 = successful, 0x80 = abandoned, 0x120 = timeout, 0xFFFFFFFF = failed
     */
    Lock(timeout:=0xFFFFFFFF) => DllCall("WaitForSingleObject", "ptr", this, "int", timeout, "int")
    ; Releases the mutex (resets it back to the unsignaled state)
    Release() => DllCall("ReleaseMutex", "ptr", this)
    __Delete() => DllCall("CloseHandle", "ptr", this)
}
Waiter.ahk

Code: Select all

#Requires AutoHotkey v2.0

mtx := Mutex("Local\MyMutex")
if mtx.Lock() = 0 ; Success
    mtx.Release()
MsgBox "Unblocked!"

class Mutex {
    /**
     * Creates a new Mutex, or opens an existing one. The mutex is destroyed once all handles to
     * it are closed.
     * @param name Optional. The name can start with "Local\" to be session-local, or "Global\" to be 
     * available system-wide.
     * @param initialOwner Optional. If this value is TRUE and the caller created the mutex, the 
     * calling thread obtains initial ownership of the mutex object.
     * @param securityAttributes Optional. A pointer to a SECURITY_ATTRIBUTES structure.
     */
    __New(name?, initialOwner := 0, securityAttributes := 0) {
        if !(this.ptr := DllCall("CreateMutex", "ptr", securityAttributes, "int", !!initialOwner, "ptr", IsSet(name) ? StrPtr(name) : 0))
            throw Error("Unable to create or open the mutex", -1)
    }
    /**
     * Tries to lock (or signal) the mutex within the timeout period.
     * @param timeout The timeout period in milliseconds (default is infinite wait)
     * @returns {Integer} 0 = successful, 0x80 = abandoned, 0x120 = timeout, 0xFFFFFFFF = failed
     */
    Lock(timeout:=0xFFFFFFFF) => DllCall("WaitForSingleObject", "ptr", this, "int", timeout, "int")
    ; Releases the mutex (resets it back to the unsignaled state)
    Release() => DllCall("ReleaseMutex", "ptr", this)
    __Delete() => DllCall("CloseHandle", "ptr", this)
}
It is also possible to wait for multiple mutexes to be released using WaitForMultipleObjects.

8) Semaphore
A semaphore is another synchronization mechanism similar to a mutex, but unlike a mutex which is binary (locked or unlocked), a semaphore can have a count value greater than 1. A semaphore is created with a maximum value and a starting value (which is usually the maximum value), and then scripts can consume the semaphore one by one until the count decreases to 0, at which point scripts need to wait until the count is greater than 0 again (somebody releases the semaphore and increases the count). This can be useful to limit a resource to a set number.

Semaphore.ahk limits the count of running Semaphore.ahk instances to two.

Code: Select all

#Requires AutoHotkey v2
#SingleInstance Off

; Create a new semaphore (or open an existing one) with maximum count of 2 and initial count of 2
sem := Semaphore(2, 2, "Local\AHKSemaphore")
; Try to decrease the count by 1
if sem.Wait(0) = 0 {
    MsgBox "This script got access to the semaphore.`nPress F1 to kill all running scripts."
    ; If the following line was missing then the semaphore would be released only when all
    ; handles to it are closed and the semaphore is destroyed
    OnExit((*) => sem.Release())
} else
    MsgBox("Two scripts are already running, exiting... :("), ExitApp

class Semaphore {
    /**
     * Creates a new semaphore or opens an existing one. The semaphore is destroyed once all handles
     * to it are closed.
     * 
     * CreateSemaphore argument list:
     * @param initialCount The initial count for the semaphore object. This value must be greater 
     * than or equal to zero and less than or equal to maximumCount.
     * @param maximumCount The maximum count for the semaphore object. This value must be greater than zero.
     * @param name Optional. The name of the semaphore object.
     * @param securityAttributes Optional. A pointer to a SECURITY_ATTRIBUTES structure.
     * @returns {Object}
     * 
     * OpenSemaphore argument list:
     * @param name The name of the semaphore object.
     * @param desiredAccess Optional: The desired access right to the semaphore object. Default is
     * SEMAPHORE_MODIFY_STATE = 0x0002
     * @param inheritHandle Optional: If this value is 1, processes created by this process will inherit the handle.
     * @returns {Object}
     */
    __New(initialCount, maximumCount?, name?, securityAttributes := 0) {
        if IsSet(initialCount) && IsSet(maximumCount) && IsInteger(initialCount) && IsInteger(maximumCount) {
            if !(this.ptr := DllCall("CreateSemaphore", "ptr", securityAttributes, "int", initialCount, "int", maximumCount, "ptr", IsSet(name) ? StrPtr(name) : 0))
                throw Error("Unable to create the semaphore", -1)
        } else if IsSet(initialCount) && initialCount is String {
            if !(this.ptr := DllCall("OpenSemaphore", "int", maximumCount ?? 0x0002, "int", !!(name ?? 0), "ptr", IsSet(initialCount) ? StrPtr(initialCount) : 0))
                throw Error("Unable to open the semaphore", -1)
        } else
            throw ValueError("Invalid parameter list!", -1)
    }
    /**
     * Tries to decrease the semaphore count by 1 within the timeout period.
     * @param timeout The timeout period in milliseconds (default is infinite wait)
     * @returns {Integer} 0 = successful, 0x80 = abandoned, 0x120 = timeout, 0xFFFFFFFF = failed
     */
    Wait(timeout:=0xFFFFFFFF) => DllCall("WaitForSingleObject", "ptr", this, "int", timeout, "int")
    /**
     * Increases the count of the specified semaphore object by a specified amount.
     * @param count Optional. How much to increase the count, default is 1.
     * @param out Is set to the result of the DllCall
     * @returns {number} The previous semaphore count
     */
    Release(count := 1, &out?) => (out := DllCall("ReleaseSemaphore", "ptr", this, "int", count, "int*", &prevCount:=0), prevCount)
    __Delete() => DllCall("CloseHandle", "ptr", this)
}

~F1::ExitApp
9) Named shared memory
An alternative to writing to a file is using named shared memory via file mappings. This means the data will be stored in RAM instead of the hard drive, which means read-write access to it is much, much faster. It has the same problem as writing to registry: multiple scripts may be trying to access it at the same time, so it's best to also use a mutex to coordinate read-write access.

WriteFileMapping.ahk (run first)

Code: Select all

#Requires AutoHotkey v2

mapping := FileMapping("Local\AHKFileMappingObject")
mapping.Write("The data to share")
MsgBox "Now run the second script (without closing this MsgBox)"
mapping := unset

Class FileMapping {
	; http://msdn.microsoft.com/en-us/library/windows/desktop/aa366556(v=vs.85).aspx
	; http://www.autohotkey.com/board/topic/86771-i-want-to-share-var-between-2-processes-how-to-copy-memory-do-it/#entry552031
    ; Source: https://www.autohotkey.com/board/topic/93305-filemapping-class/

	__New(szName?, dwDesiredAccess := 0xF001F, flProtect := 0x4, dwSize := 10000) {	; Opens existing or creates new file mapping object with FILE_MAP_ALL_ACCESS, PAGE_READ_WRITE
        static INVALID_HANDLE_VALUE := -1
        this.BUF_SIZE := dwSize, this.szName := szName ?? ""
		if !(this.hMapFile := DllCall("OpenFileMapping", "Ptr", dwDesiredAccess, "Int", 0, "Ptr", IsSet(szName) ? StrPtr(szName) : 0)) {
		    ; OpenFileMapping Failed - file mapping object doesn't exist - that means we have to create it
			if !(this.hMapFile := DllCall("CreateFileMapping", "Ptr", INVALID_HANDLE_VALUE, "Ptr", 0, "Int", flProtect, "Int", 0, "Int", dwSize, "Str", szName)) ; CreateFileMapping Failed
				throw Error("Unable to create or open the file mapping", -1)
		}
		if !(this.pBuf := DllCall("MapViewOfFile", "Ptr", this.hMapFile, "Int", dwDesiredAccess, "Int", 0, "Int", 0, "Int", dwSize))	; MapViewOfFile Failed
			throw Error("Unable to map view of file")
	}
	Write(data, offset := 0) {
		if (this.pBuf) {
            if data is String
			    StrPut(data, this.pBuf+offset, this.BUF_SIZE-offset)
            else if data is Buffer
                DllCall("RtlCopyMemory", "ptr", this.pBuf+offset, "ptr", data, "int", Min(data.Size, this.BUF_SIZE-offset))
            else
                throw TypeError("The data type can be a string or a Buffer object")
        } else
            throw Error("File already closed!")
	}
    ; If a buffer object is provided then data is transferred from the file mapping to the buffer
	Read(buffer?, offset := 0, size?) => IsSet(buffer) ? DllCall("RtlCopyMemory", "ptr", buffer, "ptr", this.pBuf+offset, "int", Min(buffer.size, this.BUF_SIZE-offset, size ?? this.BUF_SIZE-offset)) : StrGet(this.pBuf+offset)
	Close() {
		DllCall("UnmapViewOfFile", "Ptr", this.pBuf), DllCall("CloseHandle", "Ptr", this.hMapFile)
		this.szName := "", this.BUF_SIZE := "", this.hMapFile := "", this.pBuf := ""
	}
	__Delete() => this.Close()
}
ReadFileMapping.ahk (run second)

Code: Select all

#Requires AutoHotkey v2

mapping := FileMapping("Local\AHKFileMappingObject")
MsgBox "Read from file mapping: " mapping.Read()
mapping := unset

Class FileMapping {
	; http://msdn.microsoft.com/en-us/library/windows/desktop/aa366556(v=vs.85).aspx
	; http://www.autohotkey.com/board/topic/86771-i-want-to-share-var-between-2-processes-how-to-copy-memory-do-it/#entry552031
    ; Source: https://www.autohotkey.com/board/topic/93305-filemapping-class/

	__New(szName?, dwDesiredAccess := 0xF001F, flProtect := 0x4, dwSize := 10000) {	; Opens existing or creates new file mapping object with FILE_MAP_ALL_ACCESS, PAGE_READ_WRITE
        static INVALID_HANDLE_VALUE := -1
        this.BUF_SIZE := dwSize, this.szName := szName ?? ""
		if !(this.hMapFile := DllCall("OpenFileMapping", "Ptr", dwDesiredAccess, "Int", 0, "Ptr", IsSet(szName) ? StrPtr(szName) : 0)) {
		    ; OpenFileMapping Failed - file mapping object doesn't exist - that means we have to create it
			if !(this.hMapFile := DllCall("CreateFileMapping", "Ptr", INVALID_HANDLE_VALUE, "Ptr", 0, "Int", flProtect, "Int", 0, "Int", dwSize, "Str", szName)) ; CreateFileMapping Failed
				throw Error("Unable to create or open the file mapping", -1)
		}
		if !(this.pBuf := DllCall("MapViewOfFile", "Ptr", this.hMapFile, "Int", dwDesiredAccess, "Int", 0, "Int", 0, "Int", dwSize))	; MapViewOfFile Failed
			throw Error("Unable to map view of file")
	}
	Write(data, offset := 0) {
		if (this.pBuf) {
            if data is String
			    StrPut(data, this.pBuf+offset, this.BUF_SIZE-offset)
            else if data is Buffer
                DllCall("RtlCopyMemory", "ptr", this.pBuf+offset, "ptr", data, "int", Min(data.Size, this.BUF_SIZE-offset))
            else
                throw TypeError("The data type can be a string or a Buffer object")
        } else
            throw Error("File already closed!")
	}
    ; If a buffer object is provided then data is transferred from the file mapping to the buffer
	Read(buffer?, offset := 0, size?) => IsSet(buffer) ? DllCall("RtlCopyMemory", "ptr", buffer, "ptr", this.pBuf+offset, "int", Min(buffer.size, this.BUF_SIZE-offset, size ?? this.BUF_SIZE-offset)) : StrGet(this.pBuf+offset)
	Close() {
		DllCall("UnmapViewOfFile", "Ptr", this.pBuf), DllCall("CloseHandle", "Ptr", this.hMapFile)
		this.szName := "", this.BUF_SIZE := "", this.hMapFile := "", this.pBuf := ""
	}
	__Delete() => this.Close()
}
10) Named pipe
Named pipes can be used for communication between scripts running on the same machine or on different machines over a network. First a "server" script creates a named pipe, and then "client" scripts can connect to it, and both server and client can read-write data to the pipe like to a regular file.

Pros: relatively simple to implement; fast; communication can be two-way; can be implemented in both blocking and non-blocking manner; supports multiple clients
Cons: while multiple clients are supported, it might be hard to scale in AHK; non-blocking communication hard to implement

Server.ahk

Code: Select all

#Requires AutoHotkey v2

PipeName := "\\.\pipe\testpipe"

PipeMsg := InputBox("Create a pipe message", "Enter a message to write in " PipeName,, "This is a message").Value
If !PipeMsg
    ExitApp

hPipe := CreateNamedPipe(PipeName)
If (hPipe = -1)
    throw Error("Creating the named pipe failed")

; Wait for a client to connect. This can be made non-blocking as well (https://www.codeproject.com/Articles/5347611/Implementing-an-Asynchronous-Named-Pipe-Server-Par)
DllCall("ConnectNamedPipe", "ptr", hPipe, "ptr", 0)

; Wrap the handle in a file object
f := FileOpen(hPipe, "h")
; If the new-line is not included then the message can't be read from the pipe
f.Write(PipeMsg "`n")
; Wait for the response
while !(msg := f.ReadLine())
    Sleep 200
MsgBox "Response: " msg
;f.Close() wouldn't close the handle
DllCall("CloseHandle", "ptr", hPipe)
ExitApp

CreateNamedPipe(Name, OpenMode:=3, PipeMode:=0, MaxInstances:=255) => DllCall("CreateNamedPipe", "str", Name, "uint", OpenMode, "uint", PipeMode, "uint", MaxInstances, "uint", 0, "uint", 0, "uint", 0, "ptr", 0, "ptr")
Client.ahk

Code: Select all

#Requires AutoHotkey v2
PipeName := "\\.\pipe\testpipe"

; Wait until the pipe is ready for a connection
DllCall("WaitNamedPipe", "Str", PipeName, "UInt", 0xffffffff)

f := FileOpen(PipeName, "rw")
MsgBox f.ReadLine()
f.Write("I message back!`n")
f.Close()
 
ExitApp
11) Socket
Sockets can be used for communication over networks rather than locally on one computer. Sockets are identified by a combination of an IP address and a port number, and can use various communication protocols (but mostly TCP and UDP).
See an example of using sockets here.

12) GWLP_USERDATA
Every AutoHotkey script has a hidden main window, and all windows have a GWLP_USERDATA attribute which can be used to store a pointer-sized amount data, which can readily be read by any other script that knows our main window name (that is, script name). This means we can store numbers (for example a window message number which specifies how to use Send/PostMessage), or pointers that can be read using ReadProcessMemory.

Pros: very simple to implement; fast
Cons: one-way communication; reading data larger than a single number can be complex

Sharing a single number is easy: write with DllCall("SetWindowLongPtr", "ptr", A_ScriptHwnd, "int", GWLP_USERDATA := -21, "ptr", 1234), and read from another script with MsgBox DllCall("GetWindowLongPtr", "ptr", WinExist("Write.ahk ahk_class AutoHotkey"), "int", GWLP_USERDATA := -21, "ptr")

Sharing strings gets more complicated. We can use atoms to associate a number to a string and communicate that number, but the string can be a maximum length of 255.
WriteAtom.ahk

Code: Select all

#Requires AutoHotkey v2
myString := "Hello from WriteAtom.ahk"
; Create an ATOM, which can store a maximum of 255 character long string
atom := DllCall("GlobalAddAtom", "str", myString, "ptr")
; Write the ATOM to our script main window GWLP_USERDATA attribute
; Alternatively we could write any number instead, for example a message number to communicate via SendMessage
DllCall("SetWindowLongPtr", "ptr", A_ScriptHwnd, "int", GWLP_USERDATA := -21, "ptr", atom)
; Atoms aren't deleted on script exit, so we need to release it before that happens
OnExit((*) => DllCall("GlobalDeleteAtom", "int", atom))
Persistent()
ReadAtom.ahk

Code: Select all

#Requires AutoHotkey v2
DetectHiddenWindows "On" ; AutoHotkey scripts' main window is hidden
if !(hWnd := WinExist("WriteAtom.ahk ahk_class AutoHotkey"))
    throw TargetError("WriteAtom.ahk not found!")
; Read the ATOM number
atom := DllCall("GetWindowLongPtr", "ptr", hWnd, "int", GWLP_USERDATA := -21, "ptr")
; Get the string associated with the ATOM
DllCall("GlobalGetAtomName", "int", atom, "ptr", buf := Buffer(255), "int", 255)
MsgBox StrGet(buf)
Sharing longer strings or arbitrary data gets more complex, because it requires the use of ReadProcessMemory:
Write.ahk (run first)

Code: Select all

#Requires AutoHotkey v2

myString := "Hello from Write.ahk"
; The other script needs to know the address of the string and also the size of the data (string) in bytes
pBuf := Buffer(A_PtrSize + 4)
NumPut("ptr", StrPtr(myString), "int", StrLen(myString)*2+2, pBuf)
; Write the buffers pointer to our script main window GWLP_USERDATA attribute
DllCall("SetWindowLongPtr", "ptr", A_ScriptHwnd, "int", GWLP_USERDATA := -21, "ptr", pBuf)
Persistent()
Read.ahk

Code: Select all

#Requires AutoHotkey v2

DetectHiddenWindows "On" ; AutoHotkey scripts' main window is hidden
if !(hWnd := WinExist("Write.ahk ahk_class AutoHotkey"))
    throw TargetError("Write.ahk not found!")
; Read the pointer to the string
pBuf := DllCall("GetWindowLongPtr", "ptr", hWnd, "int", GWLP_USERDATA := -21, "ptr")
; Since the pointer is not located in this script then we need to read it using ReadProcessMemory.
; For that we need to get a handle to the process first.
PROCESS_QUERY_INFORMATION := 0x400, PROCESS_VM_READ := 0x10
hProc := DllCall("OpenProcess", "uint", PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, "int", 0, "uint", WinGetPID(hWnd), "ptr")
if !hProc
    throw Error("Unable to open Write.ahk process")

; First read the buffer to get a pointer to the actual data and the size of the data
buf := Buffer(A_PtrSize+4) ; The size of the buffer is know to us
if !DllCall("ReadProcessMemory", "ptr", hProc, "ptr", pBuf, "ptr", buf, "int", A_PtrSize+4, "int*", &lpNumberOfBytesRead:=0) || lpNumberOfBytesRead != A_PtrSize+4 {
    DllCall("CloseHandle", "ptr", hProc)
    throw Error("Unable to read Write.ahk memory")
}

pData := NumGet(buf, "ptr"), nSize := NumGet(buf, A_PtrSize, "int")
; Now we can read the actual data
buf := Buffer(nSize)
if !DllCall("ReadProcessMemory", "ptr", hProc, "ptr", pData, "ptr", buf, "int", nSize, "int*", &lpNumberOfBytesRead:=0) {
    DllCall("CloseHandle", "ptr", hProc)
    throw Error("Unable to read Write.ahk memory")
}
DllCall("CloseHandle", "ptr", hProc)
data := StrGet(buf)
MsgBox data

Edit history
03.02.24. Added GWLP_USERDATA, which also demonstrates use of atoms (thanks, iseahound!)
Last edited by Descolada on 03 Feb 2024, 16:50, edited 3 times in total.
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Re: Communicating between scripts (IPC, inter-process communication)

14 Jan 2024, 12:57

Great tutorial. I think this covers all of them, the advanced methods shown are very useful. This might be the first time I've seen semaphores and mutexes (done via WINAPI) in AutoHotkey. One note however: I don't believe the semaphore or mutex shares memory across scripts at all. It only manages them.

Here's a CreateThread example that uses asynchronous window messages by lexikos. viewtopic.php?f=6&t=21223 FYI for anyone who reads this, using os-level threads via CreateThread is a no-go unless you enjoy pain and suffering.
Descolada
Posts: 1155
Joined: 23 Dec 2021, 02:30

Re: Communicating between scripts (IPC, inter-process communication)

14 Jan 2024, 16:32

@iseahound, I guess it depends on what you mean by "sharing memory", but I don't think I claimed that they do that, rather that they can be used for communication in a limited manner. Mutex and semaphore are mainly synchronization methods, where the kernel controls the assignment of object ownership which eliminates race conditions. However, this can still be used to share information, even if it is a binary yes/no as in the case of a mutex. Would this be considered sharing one bit of memory?
User avatar
boiler
Posts: 17120
Joined: 21 Dec 2014, 02:44

Re: Communicating between scripts (IPC, inter-process communication)

15 Jan 2024, 08:22

This is excellent. Thanks for posting it.

I’ll just mention one other approach that is about as easy to implement as using a file but doesn’t involve the disk. One script can create a hidden GUI with a title that is unique and known to the other script. Both scripts can read and change the contents of one or more Text controls. The script that created the GUI window uses the Text property of the GuiControl object to read and modify the text, while the other script uses ControlGetText and ControlSetText with control name Static1 (and 2, 3, etc. if multiple) and DetectHiddenWindows set to True.
neogna2
Posts: 595
Joined: 15 Sep 2016, 15:44

Re: Communicating between scripts (IPC, inter-process communication)

15 Jan 2024, 19:35

Great tutorial!
boiler wrote:
15 Jan 2024, 08:22
One script can create a hidden GUI with a title that is unique and known to the other script. Both scripts can read and change the contents of one or more Text controls.
A variant of that is to read/write text from/to the title of a script's auto-created hidden window - no extra GUI needed.
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Re: Communicating between scripts (IPC, inter-process communication)

03 Feb 2024, 12:03

I think one idea that hasn't been mentioned is to use PostMessage with HWND_BROADCAST to notify all windows. This could be useful to transmit the name of a shared memory object, such as the name used for CreateFileMapping. Assuming the other script is listening for this message, (WM_APP 0x8000) they could automatically kickstart and call OpenFileMapping for example.

Also, you're free to use the GWLP_USERDATA to store public data as well, which could be a pointer to the name of the string for Create/OpenFileMapping.

There's also some antiquated functions like GlobalAddAtom, which I don't think need to be included in your already excellent compilation.
Descolada
Posts: 1155
Joined: 23 Dec 2021, 02:30

Re: Communicating between scripts (IPC, inter-process communication)

03 Feb 2024, 16:53

iseahound wrote:
03 Feb 2024, 12:03
I think one idea that hasn't been mentioned is to use PostMessage with HWND_BROADCAST to notify all windows. This could be useful to transmit the name of a shared memory object, such as the name used for CreateFileMapping. Assuming the other script is listening for this message, (WM_APP 0x8000) they could automatically kickstart and call OpenFileMapping for example.
An example of HWND_BROADCAST is shown under PostMessage.
Also, you're free to use the GWLP_USERDATA to store public data as well, which could be a pointer to the name of the string for Create/OpenFileMapping.
Thanks, I added GWLP_USERDATA to the list. Hadn't heard of it before ;)
There's also some antiquated functions like GlobalAddAtom, which I don't think need to be included in your already excellent compilation.
Atoms coincidentally seem well-paired with GWLP_USERDATA, because they offer an easier way to share names for Mappings, Mutexes and other objects. Thus I added an example using atoms to GWLP_USERDATA. Unless there is some other easy way to share strings globally without the limitations of atoms?
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Re: Communicating between scripts (IPC, inter-process communication)

03 Feb 2024, 20:39

That's pretty clever to use atoms and GWLP_UUSERDATA together. Incidentally, I was just writing down my thoughts here. (ImagePut contains a hidden way to share images between processes here, and I was trying to figure out how to make it more user friendly.)

So maybe the blueprint for doing so would be from your above comment:
  1. CreateFileMapping using some arbitrary string.
  2. Convert the string to an atom, which is an integer.
  3. Send the atom to separate script using PostMessage in its lParam or wParam.
  4. The listening script should take the atom, convert it to a string, and then call OpenFileMapping automatically.
This seems more like a protocol for automatically creating and consuming resources between scripts. The problem being solved here is the fact that script #1 must be opened before #2, this protocol is agnostic, and can handle even script #3, and onwards.

As for the question of storing strings globally, I think 16 bytes or 36 characters is enough for a UUID, which is almost guaranteed to be unique if not random. But 255 characters should be enough for something to be passed to a semaphore or mutex or filemapping?
Descolada
Posts: 1155
Joined: 23 Dec 2021, 02:30

Re: Communicating between scripts (IPC, inter-process communication)

04 Feb 2024, 03:37

@iseahound I think the best approach depends on how exactly you want to share the images between processes, how many processes there are, and whether you want to notify other scripts of changes as well.

If you are sharing it with only one script, then you could skip the atom altogether if you use DuplicateHandle:
1) Create a file mapping and put the image there
2) Duplicate the handle to the file mapping, which requires a handle to the target process, which you can acquire via the PID
3) Send/PostMessage the handle to the target script
4) Target can read the file mapping using the duplicated handle

If sharing with multiple scripts then yes, you could share the name of the file mapping using an atom:
1) Create a file mapping and put the image there
2) Store the name of the file mapping in an atom
3) Broadcast the atom number to all target scripts with PostMessage HWND_BROADCAST
4) Target scripts can read the file mapping name from the atom and then open and read the file mapping
Though this usually still requires some kind of way to identify target scripts which is usually something previously agreed upon (eg a message number), so why not agree on a file mapping naming standard and skip the atom altogether? Eg name the mapping something like IseahoundImagePutFileMappingCommon or IseahoundImagePutFileMappingN, and then you can just broadcast a message alerting that a modification to the mapping N was made.

And yes, 255 characters should be plenty for kernel object names and UUID/GUIDs.

Return to “Tutorials (v2)”

Who is online

Users browsing this forum: No registered users and 4 guests