Page 1 of 3

ObjRegisterActive

Posted: 29 Jan 2015, 03:22
by lexikos

Code: Select all

/*
    ObjRegisterActive(Object, CLSID, Flags:=0)
    
        Registers an object as the active object for a given class ID.
        Requires AutoHotkey v1.1.17+; may crash earlier versions.
    
    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()
*/
ObjRegisterActive(Object, CLSID, Flags:=0) {
    static cookieJar := {}
    if (!CLSID) {
        if (cookie := cookieJar.Remove(Object)) != ""
            DllCall("oleaut32\RevokeActiveObject", "uint", cookie, "ptr", 0)
        return
    }
    if cookieJar[Object]
        throw Exception("Object is already registered", -1)
    VarSetCapacity(_clsid, 16, 0)
    if (hr := DllCall("ole32\CLSIDFromString", "wstr", CLSID, "ptr", &_clsid)) < 0
        throw Exception("Invalid CLSID", -1, CLSID)
    hr := DllCall("oleaut32\RegisterActiveObject"
        , "ptr", &Object, "ptr", &_clsid, "uint", Flags, "uint*", cookie
        , "uint")
    if hr < 0
        throw Exception(format("Error 0x{:x}", hr), -1)
    cookieJar[Object] := cookie
}
Known limitations:
  • If the script quits while running a method called by a remote script, the remote script will receive an error.
  • There are also limitations related to how AutoHotkey locally interacts with COM objects, and how AutoHotkey objects respond to requests via COM interfaces.
    • For-loops don't support remote objects.
    • Variable can't be passed ByRef to a remote object.
    • Some invocations are ambiguous; for example, foo.bar triggers foo.__Call and then foo.__Get.
To try, run both of the following files:

Code: Select all

; Register our object so that other scripts can get to it.  The second
; parameter is a GUID which I generated.  You should generate one unique
; to your script.  You can use [CreateGUID](http://goo.gl/obfmDc).
ObjRegisterActive(ActiveObject, "{6B39CAA1-A320-4CB0-8DB4-352AA81E460E}")

#Persistent
OnExit Revoke
return

Revoke:
; This "revokes" the object, preventing any new clients from connecting
; to it, but doesn't disconnect any clients that are already connected.
; In practice, it's quite unnecessary to do this on exit.
ObjRegisterActive(ActiveObject, "")
ExitApp

; This is a simple class (object) that clients will interact with.
; You can register any object; it doesn't have to be a class.
class ActiveObject {
    ; Simple message-passing example.
    Message(Data) {
        MsgBox Received message: %Data%
        return 42
    }
    ; "Worker thread" example.
    static WorkQueue := []
    BeginWork(WorkOrder) {
        this.WorkQueue.Insert(WorkOrder)
        SetTimer Work, -100
        return
        Work:
        ActiveObject.Work()
        return
    }
    Work() {
        work := this.WorkQueue.Remove(1)
        ; Pretend we're working.
        Sleep 5000
        ; Tell the boss we're finished.
        work.Complete(this)
    }
    Quit() {
        MsgBox Quit was called.
        DetectHiddenWindows On  ; WM_CLOSE=0x10
        PostMessage 0x10,,,, ahk_id %A_ScriptHwnd%
        ; Now return, so the client's call to Quit() succeeds.
    }
}

Code: Select all

; Get an active something.
x := ComObjActive("{6B39CAA1-A320-4CB0-8DB4-352AA81E460E}")

; Make up a property.  It's an AutoHotkey object, after all.
x.Name := "Bob"

; Call a method.
x.Message("Hello, world!")

; Queue some work to be done. We'll be notified when it's finished.
x.BeginWork({Complete: Func("Completed")})

#Persistent

Completed(work, worker) {
    MsgBox % worker.Name " finished work."
    worker.Quit()  ; Fire?
    ; PostMessage allows us to return before closing.
    DetectHiddenWindows On  ; WM_CLOSE=0x10
    PostMessage 0x10,,,, ahk_id %A_ScriptHwnd%
}
Related: GetActiveObjects()

Re: ObjRegisterActive

Posted: 29 Jan 2015, 07:54
by boiler
Great. Thanks for this.

Re: ObjRegisterActive

Posted: 29 Jan 2015, 08:47
by cyruz
Nice. Aside from IPC can you elaborate on possible use cases?
Is your "Worker thread" example. comment a way to say that with this code we can multithread safely?

Re: ObjRegisterActive

Posted: 29 Jan 2015, 09:00
by min
Nice! Btw, Host.ahk gives error:

Code: Select all

Error:  0x800706BA - The RPC server is unavailable.
Specifically: Complete
---> work.Complete(this)

Re: ObjRegisterActive

Posted: 29 Jan 2015, 09:17
by joedf
@lexikos so this is the multithreading you were talking about... :)

Re: ObjRegisterActive

Posted: 29 Jan 2015, 09:27
by min
Does using ObjRegisterActive mean that now we can write AHK apps which can be controlled by COM by other programs written in other languages?
e.g. some C++ app uses COM and calls a method or sets/gets property in our AHK script? If so --> that's awesome! :D

Re: ObjRegisterActive

Posted: 29 Jan 2015, 10:57
by fincs
Nice script and example. This is exactly the same technique SciTE4AutoHotkey uses to expose its COM object.

Re: ObjRegisterActive

Posted: 29 Jan 2015, 15:25
by Learning one
Lexikos, this is fantastic! 8-)
Thank you so much! :)

Re: ObjRegisterActive

Posted: 29 Jan 2015, 17:14
by lexikos
cyruz wrote:Aside from IPC can you elaborate on possible use cases?
IPC is effectively all that it is. It's just a question of what you use IPC for.
Is your "Worker thread" example. comment a way to say that with this code we can multithread safely?
Multiple processes means multiple threads, so yes. Any IPC method could be used to coordinate multiple processes. This just makes it a bit easier in some cases. For simple tasks you can just run another script with (or without) command line parameters.
min wrote:Host.ahk gives error:
I've updated the examples. I think there was a race condition between the host handling the WM_CLOSE message and the client exiting. The limitation here is that if the script exits while the remote end is waiting for a method to return, they get an error. In one script I used PostMessage to work around that, but in the other script I forgot and used ExitApp. A proper COM server might implement the IExternalConnection interface to detect when all external connections have been released, then exit. However, in the case of just passing objects back and forward in method calls, that wouldn't be very practical.
joedf wrote:@lexikos so this is the multithreading you were talking about... :)
I don't recall making any specific reference to this in regard to multi-threading, only IPC. This is just one of many alternatives to direct multi-threading.
min wrote:Does using ObjRegisterActive mean that now we can write AHK apps which can be controlled by COM by other programs written in other languages?
Yes. It's easy to do from VBScript. However, it seems GetObject() requires a ProgID rather than a CLSID, so you need to register it in the registry.

Code: Select all

' First set HKCR\testy\CLSID (default value) to the GUID
set x = GetObject(,"testy")
x.Message("Hello, world!")
fincs wrote:This is exactly the same technique SciTE4AutoHotkey uses to expose its COM object.
It differs in a number of ways. For instance, this function uses a strong reference by default and doesn't bother with CoLockObjectExternal. Where ComRemote closes connections and revokes the object automatically when the server end releases its "ComRemote" object, this function keeps the object alive and accessible until it is revoked. The function also accepts a normal object, not a ComObject wrapper. An object can be revoked without disconnecting clients, so for instance, a worker process could revoke its object when in use to allow the objects of other (idle) worker processes to be accessed.

Re: ObjRegisterActive

Posted: 29 Jan 2015, 19:53
by Guest
Lexikos,

I tried this on as simple object in script #1:

Code: Select all

AnObject := {one:1, two:2, three:3}
ObjRegisterActive(AnObject, "{6B39CAA1-A320-4CB0-8DB4-352AA81E460E}")

#Persistent
I then accessed it in script #2:

Code: Select all

x := ComObjActive("{6B39CAA1-A320-4CB0-8DB4-352AA81E460E}")
Msgbox % x.one . "`n" . x.two . "`n" . x.three
for key, value in x
	Msgbox % key . "`n" . value
The first Msgbox works but the for loop complains it cannot find a member. It looks as if x cannot be accessed the same way as it can in the script that created it.

Relayer

Re: ObjRegisterActive

Posted: 29 Jan 2015, 20:35
by geek
@fincs why is S4AHK's code for this so much shorter? Also, do you register a ProgID in the registry?

Re: ObjRegisterActive

Posted: 29 Jan 2015, 21:21
by lexikos
Huh? ComRemote.ahk is not shorter, unless you're including comments. That's even without considering the dependency (Str2GUID) and that it has less error checking. ComInterface.ahk registers a ProgID and the CLSID in the registry; look for RegisterIDs().
Relayer wrote:The first Msgbox works but the for loop complains it cannot find a member.
I haven't tested yet but I presume it's because the ComObject wrapper (for the remote object) tries to use an IEnumVARIANT-based enumerator instead of just calling object._NewEnum(). You should be able to get around it by calling _NewEnum and enum.Next() directly instead of using a for-loop.
[Edit: Wrong. See below.]

Re: ObjRegisterActive

Posted: 29 Jan 2015, 21:32
by geek
You're completely right, of course. Thanks for pointing out ComInterface.ahk, also. HKCU is the one you can write to without admin permissions, right?

How exactly does the inter-process calling of Func()tion references work? I would've expected some kind of RegisterCallback to be required. Was that part of "Added support for passing AutoHotkey objects to COM APIs as IDispatch."?

Re: ObjRegisterActive

Posted: 29 Jan 2015, 22:17
by lexikos
"Added support for passing AutoHotkey objects to COM APIs as IDispatch." is the whole reason that this works at all. Without it, we would have to implement our own IDispatch interface in script, as SciTE4AutoHotkey does. %fn%() calls fn[""](), which usually fails with COM objects but works okay with AutoHotkey objects via COM. However, work.Complete() isn't calling a Func reference; it's calling a method of the work object.

Re: ObjRegisterActive

Posted: 30 Jan 2015, 05:00
by maestrith
Thank you so much lexikos! Studio will have this soon.

Re: ObjRegisterActive

Posted: 31 Jan 2015, 07:45
by lexikos
Guest wrote:The first Msgbox works but the for loop complains it cannot find a member.
There are actually two problems:
  1. "Member not found" comes from requesting the member DISPID_NEWENUM instead of attempting to resolve the name "_NewEnum". This works well with COM objects (since some don't actually resolve the name "_NewEnum"), but AutoHotkey objects don't respond to that ID.
  2. Even if you call _NewEnum() directly, you can't use an AutoHotkey enumerator object remotely because it expects variable references to put the items into. The for-loop passes a normal variable reference, not Variant reference (a ComObject with VT_BYREF|VT_VARIANT). You can pass a Variant reference, but the AutoHotkey object at the other end will just dereference it and pass a useless value to the enumerator.

Re: ObjRegisterActive

Posted: 31 Jan 2015, 21:01
by lexikos
I have posted another example in the form of a new function: LoadFile - Load script file as a separate process.

It demonstrates temporary registration of an object using a unique ID to prevent conflicts.

Re: ObjRegisterActive

Posted: 31 Jan 2015, 21:45
by geek
I want so much to have a use for this. GeekBot comes very close, but he already has a socket based system that works great under WINE, allowing non-WINE programs such as php to interact.

Re: ObjRegisterActive

Posted: 02 Feb 2015, 02:46
by min
Lexikos, what are the terms of use for your ObjRegisterActive(), LoadFile(), LoadLib() ?

Re: ObjRegisterActive

Posted: 02 Feb 2015, 03:25
by lexikos
WTFPL or equivalent.

If use is allowed unconditionally, does that mean there are no terms of use?