Skip to content

An AutoHotkey (AHK) library with functions and classes that facilitate simple, effective, and secure interprocess communication (IPC).

License

Notifications You must be signed in to change notification settings

Nich-Cebolla/AutoHotkey-Interprocess-Communication

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AutoHotkey-Interprocess-Communication

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.

Table of contents

  1. AutoHotkey-Interprocess-Communication
  2. Related libraries
  3. AutoHotkey.com post
  4. Reddit.com post
  5. How it works
    1. ActiveObject
    2. CLSID
    3. ExecScript
    4. Mutex
    5. RegisterWindowMessage
    6. Extras
  6. Intro to COM in AHK
    1. What is COM
    2. COM in AHK
    3. Using RegisterActiveObject from AHK
      1. What process owns `obj`
      2. Object methods
      3. The dilemma
      4. Main process as the COM server
      5. Main process as the COM client
  7. Writing a COM server
  8. Using a mutex
    1. Checking if an object is available
    2. Waiting on an object
    3. Using Mutex.Prototype.Wait2
  9. Further reading

Related libraries

  • FileMapping - An AutoHotkey (AHK) library for working with the FileMapping API.

AutoHotkey.com post

Join the conversation on AutoHotkey.com

Reddit.com post

Join the conversation on Reddit

How it works

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

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

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

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

Mutex is a wrapper around CreateMutexW. See section Using a mutex for details.

RegisterWindowMessage

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.

Extras

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.

Intro to COM in AHK

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:

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:

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.

What is COM

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."

COM in AHK

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.

Using RegisterActiveObject from AHK

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}"))
Persistent

The 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"

What process owns obj

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.

Object methods

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:

  1. Process 5 launches, registering the two Container objects with RegisterActiveObject.
  2. Process 6 launches with the CLSID to c1, obtaining a reference using ComObjActive.
  3. Process 7 launches with the CLSID to c2, obtaining a reference using ComObjActive.
  4. 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 of c1 in Process 5.
    • Concurrently, Process 5 posts the sort message to Process 7.
    • Process 7 calls c_com2.Sort() which is calling the "Sort" method of c2 in Process 5.
    • As Process 5 starts working on c1.Sort() (from Process 6's c_com1.Sort() call), Process 5 gets interrupted by c2.Sort() (from Process 7's c_com2.Sort() call) and executes that call.
  5. When c2.Sort() is complete, Process 7 updates its window indicating that it is done. Process 5 resumes c1.Sort().
  6. 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.

The dilemma

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.

Main process as the COM server

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.

Main process as 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.

Writing a COM server

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.

Using a mutex

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.

Checking if an object is available

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.

Waiting on an object

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.

Using Mutex.Prototype.Wait2

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)

Further reading

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.

About

An AutoHotkey (AHK) library with functions and classes that facilitate simple, effective, and secure interprocess communication (IPC).

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published