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.0f := 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.0MsgBox "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 changesOnClipboardChange(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 laterClipSave := ClipboardAll(); Set the clipboard to our message and specify an unique identifier (in this case "AHKClipboardMessage:"); This will trigger ClipChange in ClipRead.ahkA_Clipboard := "AHKClipboardMessage:Sending this text via the Clipboard"; The clipboard should be emptied by ClipRead.ahk, so set up a monitor to detect thatOnClipboardChange(ClipChange); But in case the message wasn't received, restore the Clipboard anyway in 500msSetTimer(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 numberMsgNum := 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.0DetectHiddenWindows 1 ; Receiver.ahk is windowlessreceiverhWnd := WinExist("Receiver.ahk ahk_class AutoHotkey"); The receiver script should have created a message with name "NewAHKScript", so get its numberMsgNum := 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.0DetectHiddenWindows 1receiverhWnd := WinExist("Receiver.ahk ahk_class AutoHotkey"); The receiver script should have created a message with name "NewAHKScript", so get its numberMsgNum := 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 v2sharedObj := {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=4732CreateGUID() { 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.0mtx := 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.0mtx := 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 2sem := Semaphore(2, 2, "Local\AHKSemaphore"); Try to decrease the count by 1if 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... :("), ExitAppclass 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 v2mapping := FileMapping("Local\AHKFileMappingObject")mapping.Write("The data to share")MsgBox "Now run the second script (without closing this MsgBox)"mapping := unsetClass 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 itif !(this.hMapFile := DllCall("CreateFileMapping", "Ptr", INVALID_HANDLE_VALUE, "Ptr", 0, "Int", flProtect, "Int", 0, "Int", dwSize, "Str", szName)) ; CreateFileMapping Failedthrow 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 Failedthrow 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 bufferRead(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 v2mapping := FileMapping("Local\AHKFileMappingObject")MsgBox "Read from file mapping: " mapping.Read()mapping := unsetClass 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 itif !(this.hMapFile := DllCall("CreateFileMapping", "Ptr", INVALID_HANDLE_VALUE, "Ptr", 0, "Int", flProtect, "Int", 0, "Int", dwSize, "Str", szName)) ; CreateFileMapping Failedthrow 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 Failedthrow 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 bufferRead(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 v2PipeName := "\\.\pipe\testpipe"PipeMsg := InputBox("Create a pipe message", "Enter a message to write in " PipeName,, "This is a message").ValueIf !PipeMsg ExitApphPipe := 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 objectf := FileOpen(hPipe, "h"); If the new-line is not included then the message can't be read from the pipef.Write(PipeMsg "`n"); Wait for the responsewhile !(msg := f.ReadLine()) Sleep 200MsgBox "Response: " msg;f.Close() wouldn't close the handleDllCall("CloseHandle", "ptr", hPipe)ExitAppCreateNamedPipe(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 v2PipeName := "\\.\pipe\testpipe"; Wait until the pipe is ready for a connectionDllCall("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 v2myString := "Hello from WriteAtom.ahk"; Create an ATOM, which can store a maximum of 255 character long stringatom := 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 SendMessageDllCall("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 happensOnExit((*) => DllCall("GlobalDeleteAtom", "int", atom))Persistent()
ReadAtom.ahk
Code: Select all
#Requires AutoHotkey v2DetectHiddenWindows "On" ; AutoHotkey scripts' main window is hiddenif !(hWnd := WinExist("WriteAtom.ahk ahk_class AutoHotkey")) throw TargetError("WriteAtom.ahk not found!"); Read the ATOM numberatom := DllCall("GetWindowLongPtr", "ptr", hWnd, "int", GWLP_USERDATA := -21, "ptr"); Get the string associated with the ATOMDllCall("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 v2myString := "Hello from Write.ahk"; The other script needs to know the address of the string and also the size of the data (string) in bytespBuf := 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 attributeDllCall("SetWindowLongPtr", "ptr", A_ScriptHwnd, "int", GWLP_USERDATA := -21, "ptr", pBuf)Persistent()
Read.ahk
Code: Select all
#Requires AutoHotkey v2DetectHiddenWindows "On" ; AutoHotkey scripts' main window is hiddenif !(hWnd := WinExist("Write.ahk ahk_class AutoHotkey")) throw TargetError("Write.ahk not found!"); Read the pointer to the stringpBuf := 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 := 0x10hProc := 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 databuf := Buffer(A_PtrSize+4) ; The size of the buffer is know to usif !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 databuf := 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!)