An AutoHotkey (AHK) library with functions and classes that facilitate simple, effective interprocess communication (IPC) focusing on asynchronous method calling.
This is a pre-release project. Although the classes and functions currently offered are considered complete and I don't plan for any major changes to them, please anticipate the possibility that a future update might include a breaking change.
- AutoHotkey-Interprocess-Communication
- Related libraries
- AutoHotkey.com post
- Reddit.com post
- How it works
- Intro to COM in AHK
- Writing a COM server
- Using a mutex
- Further reading
- Table of contents generated by Headers2ToC.ahk
- FileMapping - An AutoHotkey (AHK) library for working with the FileMapping API.
Join the conversation on AutoHotkey.com
Join the conversation on Reddit
This library leverages COM, specifically Windows' RegisterActiveObject and AHK's ComObjActive, to share objects between scripts. The library packages together everything you need to design a helper script that another script can use for asynchronous method calling. It also includes 11 demos with many comments and a detailed walkthrough so you can understand how each piece fits together to achieve true asynchronous method calls with native AHK v2.
To get started, you should clone the repo, open the readme, and start working through the demos following the readme's instructions.
ActiveObject is an AHK wrapper around
RegisterActiveObject.
You pass ActiveObject an object and a CLSID, and it register the object, making it available to external processes
using the CLSID.
CLSID is a helper class to generate valid CLSID. It can generate unique CLSID, or you can pass it
a string and it will call CLSIDFromString
for you. There is also a small script scripts\GenerateClsid.ahk which you can use to generate any
number of CLSID and assign them to the clipboard, or it can launch a gui that you can keep open
to generate CLSID at-will.
ExecScript is the function found tucked away in the AHK official docs.
It executes code from string as an external process, so you don't have to save a temp file to disk
if you want to run generated code. Be mindful of the security risks of executing arbitrary code.
Mutex is a wrapper around CreateMutexW.
See section Using a mutex for details.
RegisterWindowMessage calls RegisterWindowMessage, which finds a number that no other process has registered for
use as a window message. It associates the message with a name, so if any other process calls
RegisterWindowMessage with the same name, it will receive the same number. These numbers are to
be used with PostMessage, SendMessage, and OnMessage.
ipc.ahk is a script that simply #includes all the other scripts.
wMsg is a wrapper around the MSG
structure.
CopyDataStruct is a wrapper around COPYDATASTRUCT, for use with WM_COPYDATA.
These two classes are not currently used by the library, but they are useful for IPC.
Disclaimer: I am an amateur programmer who is still learning. If you notice incorrect information, I would appreciate if you submit a pull request or let me know in some way.
Disclaimer: This README and library are not yet complete, but what is currently available is tested and working.
The purpose of this introduction is to make clear what async means in the context of this library, the limitations of this library, some possible issues you might encounter when using the library and how to prevent/circumvent them. This is not a comprehensive introduction to COM; see Further reading.
I highly recommend reading through COM in AHK and running the demo scripts before attempting to work with the library.
This introduction and library assumes you have a moderate understanding of the following:
- AutoHotkey's object model
- Interacting with Windows API functions using DllCall
- Creating data structures
If your learning journey has not yet reached the topics of class objects, pointers or structures, I recommend reading Descolada's "Objects & Classes" which should be a sufficient starting point.
This introduction and library assumes you have a basic understanding of the following:
- Using SendMessage, PostMessage, and OnMessage
- Basic C++ value types
If your learning journey has not yet reached the topic of window messages, you can probably still follow along in this readme without issue; you don't need to have a deep understanding of window messages to make use of this library because those details are handled by the library. But, for learning material there is Rajat's "PostMessage / SendMessage Tutorial" in the AHK official docs. The best learning tools would be Microsoft's documentation and practice.
The Component Object Model (COM) "is a platform-independent, distributed, object-oriented system for creating binary software components that can interact." COM is not a programming language;
"The only language requirement for COM is that code is generated in a language that can create structures of pointers and, either explicitly or implicitly, call functions through pointers...
"COM defines the essential nature of a COM object. In general, a software object is made up of a set of data and the functions that manipulate the data. A COM object is one in which access to an object's data is achieved exclusively through one or more sets of related functions. These function sets are called interfaces, and the functions of an interface are called methods."
When we make an object literal using AHK code, we are making an associative array (a hashtable) with properties and values.
obj := { prop: "val", prop2: "val2" }When our code accesses obj.prop, the AHK interpreter searches the associative array for "prop", which
will return a pointer to the string "val". Although we think about obj.prop as being a value property,
under the hood the AHK interpreter is nonetheless invoking a function that retrieves a pointer to
"val", and that is what gets returned to our code.
Beyond the properties and values, obj is an
IDispatch-compatible
automation object with callable methods. Because of Lexikos' and the other contributors' hard work,
making obj available to external processes as a COM object is as simple as passing obj's pointer to
RegisterActiveObject.
See how the code in this section works by running "test\readme-demo\Using RegisterActiveObject from AHK - process-1.ahk"
When we call RegisterActiveObject, the object becomes available to external processes. External AHK scripts simply call ComObjActive passing the CLSID as a string to obtain a reference to the object.
This library provides classes which facilitate this process. In the below snippet, the class
ActiveObject handles calling RegisterActiveObject, and the class CLSID handles creating
the CLSID. (Note that CLSID will also generate unique CLSID values, and there is a convenience
script "scripts\GenerateClsid.ahk" that you can use to make any number of valid, unique CLSID).
Process 1:
#include <ipc>
obj := { prop: "val", prop2: "val2" }
ao := ActiveObject(obj, CLSID("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"))
PersistentThe external process can obtain a reference to obj by passing the CLSID to ComObjActive.
Process 2:
obj_com := ComObjActive("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}")
MsgBox(obj_com.prop) ; "val"Process 1. obj and obj_com are both references to the same object, but with the above code, if
Process 1 terminates, obj_com becomes invalid. You can see this in action by running
"test\readme-demo\Using RegisterActiveObject from AHK - process-1.ahk". Launch that script,
review the code in the Process 1 and Process 2 scripts, then return to this readme to continue
reading.
Pause and resume here...
The documentation for
RegisterActiveObject
discusses ways to influence this behavior, but I have yet to explore these.
For now, Process 1 must stay alive for extenal references to obj to remain valid, which is why
the line Persistent is necessary in Process 1.
Let's use a more complex class to discuss methods. As I designed, tested, and built this library, I used my class Container as the use case example for testing and development. I wanted to see if I could offload the sorting and binary search methods to an external process, since they have the potential to be somewhat time consuming for very large datasets.
Launch "test\readme-demo\Object methods - process-3.ahk" to observe a real
implementation of RegisterActiveObject and how it frees up the main process's window (Process 3)
so the user can continue to interact with it while Process 4 does an extensive calculation (sorts
300000+ items). Follow the instructions in the gui window, review the code in the Process 3 and
Process 4 scripts, then come back to this readme and finish reading the section.
Pause and resume here...
The demo shows us that the gui for Process 3 remains interactible while the gui for Process 4 is locked up. The reason Process 4 locks up is because it has made a synchronous method call and is waiting for that call to return, same as if it called a method on an object in its own process.
Though it is true that the demo implementation has allowed us to sort a very large number of items without locking up the main gui window, this is not truly an asynchronous process; the reality is somewhat more nuanced than what is plainly apparent.
When Process 4 calls c_com.Sort() (see file "test\readme-demo\Object methods - process-4.ahk"),
the sort code is executing in Process 3. An easy way to verify this is to launch Process 3 with a
debugger. Launch Process 3 with a debugger, click "Launch Process 4", then when you click the
"Post message" button, immediately afterward press the pause button in your debugger and you will
see that it is indeed Process 3 that is executing the code.
Pause and resume here...
Close out of the Process 3 and 4 windows and launch Process 5 ("test\readme-demo\Object methods - process-5.ahk"). Follow the instructions in the window, review the code in the Process 5 and Process 6 scripts (Process 7 is basically the same as Process 6), then come back to this readme.
Pause and resume here...
What you should have observed is that Process 7 finished first, then Process 6 finished after about the same amount of time that it took Process 7 to finish.
Here's what happened:
- Process 5 launches, registering the two
Containerobjects withRegisterActiveObject. - Process 6 launches with the CLSID to
c1, obtaining a reference usingComObjActive. - Process 7 launches with the CLSID to
c2, obtaining a reference usingComObjActive. - Process 5 calls
PostMessage, posting the sort message to Process 6 and Process 7. -
- Process 6 calls
c_com1.Sort()which is calling the "Sort" method ofc1in Process 5. - Concurrently, Process 5 posts the sort message to Process 7.
- Process 6 calls
-
- Process 7 calls
c_com2.Sort()which is calling the "Sort" method ofc2in Process 5. - As Process 5 starts working on
c1.Sort()(from Process 6'sc_com1.Sort()call), Process 5 gets interrupted byc2.Sort()(from Process 7'sc_com2.Sort()call) and executes that call.
- Process 7 calls
- When
c2.Sort()is complete, Process 7 updates its window indicating that it is done. Process 5 resumesc1.Sort(). - When
c1.Sort()is complete, Process 6 updates its window indicating that it is done.
In this demonstration, we see that Process 5 is our COM server, but it only has a maximum of 1 thread available to execute code. Furthermore, we aren't actually offloading work onto an external process; we have the illusion that we are, but if we tried to execute code in Process 5, it would only interrupt the code execution being done for Process 6 and Process 7, delaying their progress. That is why we are able to still move the windows for Process 3 and Process 5 around; when the OS sends a message to the gui to move the window, the sort code execution is interrupted to process the window messages.
You are probably here because you are interested in finding a way to offload work onto an external process to keep your main process free to respond to the user's input, which offers a much better user experience compared to waiting while the window is frozen. Or maybe you work with large data and you want to accelerate a process by using concurrent asynchronous function calling.
We can accomplish this through the use of COM, window messages, and synchronization tools (like a mutex), but the dilemma is this:
- The main process is where all the data and objects are located.
- When an external process accesses and/or interacts with those data and objects, it executes code in the main process, not the external process.
The obvious solution is that we need to keep all of the data in the external process, so the main process can call methods that execute in the external process. However, as we have seen, if we do it that way it will lock up the main process up while it waits for the synchronous method call to execute. The next two sections explore two possible approaches to addressing the problem.
In many cases, using the main process as the COM server (like we seen with Process 3 and Process 4) should be acceptable. What we want is to improve the user experience by keeping the gui window interactible while a function executes. We can give the appearance of an asynchronous method call by using the main process as the COM server and the external process as the COM client. The drawback is that every time the user invokes an event, the code execution is interrupted, delaying the offloaded task. When most event handlers execute quickly this should not be an issue. At the cost of a small amount of additional overhead, the user experience is improved because long-running tasks that would otherwise lock up the gui can be invoked by the COM client.
For truly asynchronous function calls using the tools offered by this library, the main process must be a COM client, and we must apply a system that frees up the main process while it awaits for the COM server to complete code execution. Launch "test\readme-demo\Main process as the COM client - process-8.ahk", follow the instructions on the gui, review the code in the Process 8 and Process 9 scripts, then come back to finish reading this readme.
Pause and resume here...
We now have a truly asynchronous sort method call.
At this point, you should have enough understanding to start building your own COM server for asynchronous method calling. The Process 9 script demonstrate the minimum components needed.
Keep in mind that both the data objects and the function objects must be owned by the server process.
The client process, which is the main process that the user interacts with, must access the data
through the COM object or as a separate active object (registered with RegisterActiveObject using
the ActiveObject class). If you pass an object owned by the client (main process) to a function
owned by the server process, every time the server process accesses a property or method on the object,
the code associated with the property or method will be executed in the client process, and will not
be truly asynchronous.
I am working on a general-use COM server tool. In the meantime, use "test\readme-demo\Main process as the COM client - process-8.ahk" and "test\readme-demo\Main process as the COM client - process-9.ahk" as your guide.
The Mutex class is an AHK wrapper around CreateMutexW and related functions. The word "mutex" is a portmanteau or blend of "mutual" and "exclusive". The purpose of using a mutex is to ensure the mutual exclusivity of an object, particularly with respect to write operations. If a process is writing to an object, it is important that other processes can be made aware that this is occurring so they can wait for the write operation to complete, or take some other action. A mutex makes this easy.
For the most part, you will use Mutex in one of three ways depending on the scenario.
Sometimes we just need to check if an object is available or not. To do this, your code calls
Mutex.Prototype.Wait with a timeout of 0. This will give the calling process ownership of the
object if it is available, or will return immediately if it is not available.
For high-priority tasks, we may want a process to wait until it can take ownership of an object, so the process doesn't risk missing its opportunity to perform the task for which the object is needed. Any of the "Wait" methods is appropriate for this.
Mutex.Protoype.Wait2 and Mutex.Prototype.WaitEx2 are the most flexible methods. With these,
your code provides up to five functions that will be called depending on the return value of
WaitForSingleObject or WaitForSingleObjectEx, respectively.
In the below example, we use a timer to repeatedly check the mutex until it is signaled and the process can complete its task.
#include <Mutex>
; Define a function to check the mutex.
CheckMutex(_mutex) {
fns := Map(
WAIT_OBJECT_0, CallbackObject0
, WAIT_ABANDONED, CallbackAbandoned
, WAIT_TIMEOUT, CallbackTimeout
, WAIT_FAILED, CallbackFailed
)
; 0 timeout so the process can continue doing other things if the mutex is not signaled.
; `result` receives the callback's return value, if there is one.
_mutex.Wait2(fns, 0, &result)
}
; Define functions for each return value of WaitForSingleObject.
; WAIT_OBJECT_0 - This function is called when the mutex is signaled (when the object is currently available for use).
CallbackObject0(_mutex) {
; We cached a reference to the object as a property on the mutex for convenience.
obj := _mutex.obj
; Do work with the object.
; When done, release the mutex.
_mutex.Release()
}
; WAIT_ABANDONED - This function is called if process that has locked the mutex terminates before releasing the mutex.
CallbackAbandoned(_mutex) {
; We cached a reference to the object as a property on the mutex for convenience.
obj := _mutex.obj
; Validate the object's data because the process terminated.
; Do work with the object.
; When done, release the mutex.
_mutex.Release()
}
; WAIT_TIMEOUT - This function is called if the mutex is not signaled (currently locked).
CallbackTimeout(_mutex) {
; Check again in 1 second.
SetTimer(CheckMutex.Bind(_mutex), -1000)
}
; WAIT_FAILED - This function is called if the call to `Mutex.Prototype.Wait2` fails for some reason.
CallbackFailed(*) {
throw OSError()
}
; Assume the object's CLSID and the mutex name were passed as command line parameters.
; Get a handle to the mutex.
_mutex := Mutex({ Name: A_Arg[1] })
; Get a reference to an object owned by the other process.
; Cache it on the mutex object for convenience
_mutex.obj := ComObjActive(A_Arg[2])
; Call the helper function.
CheckMutex(_mutex)The following are topics that are important for understanding how to safely and correctly implement a multithreaded system using COM that are not addressed in this readme. You do not need to read these to use this library; if you simply make sure that no two processes attempt to perform read / write operations on an object at the same time (using a mutex or some other means), that is sufficient. But if that restriction is inconvenient or unrealistic, you will need to explore more advanced topics.