Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.1: Implemented callback for SendMessage #330

Open
wants to merge 3 commits into
base: alpha
Choose a base branch
from

Conversation

Descolada
Copy link
Contributor

This pull request implements the use of a callback function with SendMessage to get the result asynchronously. This is done with the use of SendMessageCallback in PostSendMessage in addition to SendMessageTimeout.

The user defined callback function (hereby referred to as aFunc) is passed via the timeout argument. This is safe to use because SendMessageCallback doesn't use a timeout, and the previous version of SendMessage didn't allow non-integer types for the timeout argument. Thus this change should be backwards-compatible.
Because aTimeout is now of type ExprTokenType not optl, TypeErrors are not automatically thrown any more for invalid argument types. I opted to default to the 5000ms timeout in case of strings and floats, but additional checks for them could be implemented.

SendMessageCallbackProc is used as an interim callback function that is passed into SendMessageCallback, and the pointer to aFunc is passed via the dwData argument. SendMessageCallbackProc is used because I couldn't figure out how to use aFunc directly with SendMessageCallback, if it even is possible. The loss of the dwData argument isn't important for the user, because it's trivial to replace using a BoundFunc.
Alternatively I considered using CallbackCreate and returning the created pointer with the SendMessage call, but that would require the user to explicitly use CallbackFree inside the callback function. That would be less convenient than the implementation proposed in this PR.

This implementation uses aFunc->AddRef(); and releases in SendMessageCallbackProc. This theoretically creates a possibility for a memory leak if the callback function is never called, but that risk should be negligible if the SendMessageCallback call is successful. Alternatively the obligation to keep aFunc alive could be passed to the user instead, but I couldn't figure out how to do that.

Example
Receiver.ahk

#Requires AutoHotkey v2.0

MsgNum := DllCall("RegisterWindowMessage", "Str", "AHKSendMessageCallback")
OnMessage(MsgNum, SendMessageReceiver)
Persistent()

SendMessageReceiver(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

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

receiverhWnd := WinExist("Receiver.ahk ahk_class AutoHotkey")
MsgNum := DllCall("RegisterWindowMessage", "Str", "AHKSendMessageCallback")
reply := SendMessage(MsgNum, 123, -456,, receiverhWnd,,,, SendMessageCallback)
Persistent()

SendMessageCallback(hWnd, msg, result) {
	MsgBox("Callback result:`nhWnd: " hWnd "`nmsg: " msg "`nresult: " result)
}

I'm not quite sure how useful this addition would even be though, because SendMessageCallback can also be rather easily used via a DllCall (DllCall("SendMessageCallback", "ptr", receiverHwnd, "uint", MsgNum, "ptr", 123, "ptr", -456, "ptr", CallbackCreate(SendMessageCallback), "ptr", 0)), which compared to this PR is only marginally harder to use and necessitates the additional use of CallbackFree. Since I would consider using SendMessage with a callback an advanced technique and advanced users should know how to use a simple DllCall anyway, then implementing this PR may not be worth it? Although the same could be said for the regular SendMessage...

@Descolada Descolada changed the title [v2.1] Implemented callback for SendMessage v2.1: Implemented callback for SendMessage Jan 14, 2024
@Lexikos
Copy link
Collaborator

Lexikos commented Mar 17, 2024

Putting aside whether this should be merged...

If the target process is terminated before replying to the message, the callback is called with a result of 0.

In any other situation, I think it would have to be assumed that the process could reply eventually, and therefore the function object must not be released.

aTimeout->symbol == SYM_OBJECT is true for function/property return values and constants (such as direct references to global functions by name) but not variables or fat arrow functions (both of which are SYM_VAR). You should instead call TokenToObject first and check for a non-null result (conventionally if (aFunc) rather than the more verbose if (aFunc != nullptr)).

There's no need for _alloca when you are unconditionally allocating a fixed size. ExprTokenType param[3]; would be conventional, although you can probably use ExprTokenType param[] = { hWnd, uMsg, result }; plus appropriate typecasts. Instead of param_count you can use _countof(param).

It probably makes more sense to put the result parameter first, so one can write (result, *) => or similar. Being passed the HWND and message number may be convenient in some cases, but is unnecessary since the caller of SendMessage had those values already (or at least could have determined the HWND by the WinTitle etc.).

@Lexikos
Copy link
Collaborator

Lexikos commented Mar 23, 2024

I had forgotten I had merged this into a local test branch, so accidentally noticed that it causes build warnings/errors on x64.

…hanged SendMessage callback argument list from (hWnd, msg, result) to (result, hWnd, msg).
@Descolada
Copy link
Contributor Author

Descolada commented Mar 24, 2024

If the target process is terminated before replying to the message, the callback is called with a result of 0.

I figure that the user should account for the fact that the process may die in the meanwhile, and can use WinExist in the case result==0 to determine whether 0 was returned by the process, or because it died.

In any other situation, I think it would have to be assumed that the process could reply eventually, and therefore the function object must not be released.

I opted to increase the function object ref count if SendMessageCallback is successful, so the release could be done in SendMessageCallbackProc. I'm not aware of situations where SendMessageCallback could be successful but the callback function wouldn't be called, so I'm not sure whether there actually is potential for a memory leak.

The other suggestions have been addressed now.

@Lexikos
Copy link
Collaborator

Lexikos commented Apr 28, 2024

I'm not aware of situations where SendMessageCallback could be successful but the callback function wouldn't be called

Neither am I. Even if such situations are possible, I've concluded that there will be no way to detect it, so whatever parameter the callback is given must remain valid forever if the callback is never called. This is my answer to your comment about "possibility for a memory leak".

However, a level of indirection can allow the object to be freed before a reply is received. For instance, the script could implement a timeout like this:

SendMessageTimeout(timeout, ..., callback) {
    SendMessage(..., (a*) => callback?.(a*))
    SetTimer () => callback := unset, -timeout
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants