SHInstanceExplorer

Post your working scripts, libraries and tools.
lexikos
Posts: 9583
Joined: 30 Sep 2013, 04:07
Contact:

SHInstanceExplorer

Post by lexikos » 01 Oct 2022, 22:10

SHInstanceExplorer provides a way to detect when an in-process shell component is finished doing its work.

Code: Select all

class SHInstanceExplorer {
    static RefCount := 0
    static AddRef := this => (++this.RefCount)
    static Release := this => (--this.RefCount, this.Released())
    static Released := this => 0
    static Call() => this
    static __new() {
        static meth := A_PtrSize = 8 ? [
            [0xB8481675003A8348, 0x46000000000000C0, 0x8949067508423948, 0x0000C749C3C03308, 0x80004002B8000000, 0xC3], ; QueryInterface
            [0x4C10518B28EC8348, 0x01B941C18B4CD18B, 0x4108498B48000000, 0x00000001B81852FF, 0xC328C48348], ; AddRef
            [0x4810518B28EC8348, 0xC93345C18B4CC18B, 0x331850FF08498B48, 0xC328C48348C0] ; Release
        ] : [
            [0x410B018B08244C8B, 0x00C0087981217504, 0x000C798118750000, 0x24448B0F75460000, 0x33088904244C8B0C, 0x0C24448B000CC2C0, 0x02B80000000000C7, 0x000CC2800040], ; QueryInterface
            [0xFF50016A0424448B, 0x0C408B0470FF0870, 0xC200000001B8D0FF, 0x0004], ; AddRef
            [0xFF50006A0424448B, 0x0C408B0470FF0870, 0x0004C2C033D0FF] ; Release
        ]
        static code := Buffer((meth[1].Length + meth[2].Length + meth[3].Length) * 8)
        static vtbl := Buffer(meth.Length * A_PtrSize)
        offset := 0
        for m in meth {
            NumPut("ptr", code.ptr + offset, vtbl, (A_Index-1)*A_PtrSize)
            for i64 in m {
                NumPut("int64", i64, code, offset)
                offset += 8
            }
        }
        DllCall("VirtualProtect", "ptr", code, "ptr", code.size, "uint", 0x40, "uint*", 0)
        static objb := Buffer(A_PtrSize*4)
        NumPut(
            "ptr", vtbl.ptr,
            "ptr", A_ScriptHwnd,
            "ptr", msg := DllCall("RegisterWindowMessage", "str", "SHIE"),
            "ptr", DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "user32", "ptr"), "astr", "PostMessageW", "ptr"),
            objb
        )
        this.RefCount := 0
        OnMessage(msg, this.AddRefRelease.Bind(this))
        DllCall("shell32\SHSetInstanceExplorer", "ptr", objb)
    }
    static AddRefRelease(pobj, add, nMsg, hwnd) {
        if hwnd != A_ScriptHwnd
            return
        (add) ? this.AddRef() : this.Release()
        return 1
    }
}

Shell extensions that create worker threads need to call the SHGetInstanceExplorer function so that Explorer will not exit while the worker thread is still running. When your worker thread finishes, you release the IUnknown that you obtained to tell the host program, “Okay, I’m done now, thanks for waiting.”

You can read this contract from the other side. Instead of thinking of yourself as the shell extension running inside a host program, think of yourself as the host program that has a shell extension running inside of it.
...
Source: Reading a contract from the other side: SHSetInstanceExplorer and SHGetInstanceExplorer - The Old New Thing
Each time a component calls SHGetInstanceExplorer, the system calls the AddRef method before returning the interface pointer to the calling component. The component then calls the IUnknown::Release method when processing is complete. The process that calls SHSetInstanceExplorer must not terminate while the reference count of the provided interface pointer is nonzero.
Source: SHSetInstanceExplorer function (shlobj_core.h) - Win32 apps | Microsoft Learn

By default, AddRef increments SHInstanceExplorer.RefCount while Release decrements it and calls SHInstanceExplorer.Released() if RefCount reaches 0.

The script is free to redefine the following methods via assignment or DefineProp:
  • SHInstanceExplorer.AddRef()
  • SHInstanceExplorer.Release()
  • SHInstanceExplorer.Released()
Keep in mind that any assigned function must take a reference to SHInstanceExplorer as its first parameter (conventionally named this).


The function uses machine code because the "instance explorer" object is required to be "free threaded"; in other words, the actual interface methods are called from shell worker threads, so cannot be safely implemented directly with CallbackCreate.

C++ source code and disassembly


Example #1: When you use Run to show a file's Properties dialog, it remains open only while the script is running. It's easy enough to wait for the window(s) to close, but SHInstanceExplorer provides an alternative.

Code: Select all

Run 'properties "' A_ScriptFullPath '"'
Run 'properties "' A_ScriptDir '"'

while SHInstanceExplorer.RefCount
    Sleep 100

Example #2: Adding files to a zip via the shell API usually requires a bit of guesswork to determine when the operation has completed, since the API returns while the operation is still in progress. SHInstanceExplorer.Released can be redefined so that the script is notified when the operation is completed. In this example, I just use it to keep the script running.

Code: Select all

; Make the script persistent...
Persistent
; ...until the shell file operation is complete.
SHInstanceExplorer.Released := this => Persistent(this.RefCount > 0)

zipPath := A_WorkingDir "\~test.zip"
sourceDir := A_MyDocuments

try {
    CreateZipFile(zipPath)
    shell := ComObject("Shell.Application")
    itemToZip := shell.Namespace(sourceDir).Items ; The folder's contents
    ; itemToZip := shell.Namespace(0).ParseName(sourceDir) ; The folder itself
    shell.Namespace(zipPath).CopyHere(itemToZip)
}
catch {
    Persistent false
    throw
}

CreateZipFile(path) {
    f := FileOpen(path, "w")
    f.Write("PK")
    f.WriteInt64(0x0605)
    f.WriteInt64(0)
    f.WriteInt(0)
    f.Close()
}

guest3456
Posts: 3462
Joined: 09 Oct 2013, 10:31

Re: SHInstanceExplorer

Post by guest3456 » 01 Oct 2022, 23:25

:crazy:


iseahound
Posts: 1444
Joined: 13 Aug 2016, 21:04
Contact:

Re: SHInstanceExplorer

Post by iseahound » 02 Oct 2022, 08:59

👍 Seems interesting! Is "SHIE" a constant? Why is it okay to use i64 instead of uint64?

guest3456
Posts: 3462
Joined: 09 Oct 2013, 10:31

Re: SHInstanceExplorer

Post by guest3456 » 02 Oct 2022, 11:20

iseahound wrote:
02 Oct 2022, 08:59
Why is it okay to use i64 instead of uint64?
i64 is simlpy a variable name


lexikos
Posts: 9583
Joined: 30 Sep 2013, 04:07
Contact:

Re: SHInstanceExplorer

Post by lexikos » 03 Oct 2022, 00:53

"SHIE" is a literal string. Literal values are constant. ;)

The use of RegisterWindowMessage is perhaps the most ordinary, boring part of this script. As with any other use of RegisterWindowMessage, the purpose is to get a message number that isn't likely to be used by anything else in the script. If I picked message number 0x8000, you can almost guarantee that someone else's function would conflict with it. There's a much slimmer chance of something else in the same script using the string "SHIE" to register a window message for some other purpose. I kept the string short because it is registered throughout the current user session, persisting until logout.

As for uint64, the input value is always a signed 64-bit integer due to how AutoHotkey is implemented, but it doesn't matter either way.
For all integer types, or when passing pure integers, signed vs. unsigned does not affect the result due to the use of two's complement to represent signed integers.
Source: NumPut - Syntax & Usage | AutoHotkey v2

User avatar
Animan8000
Posts: 57
Joined: 11 May 2022, 05:00
Contact:

Re: SHInstanceExplorer

Post by Animan8000 » 03 Oct 2022, 05:07

Nice one! Example #2 will be handy for my case :thumbup:

Post Reply

Return to “Scripts and Functions (v2)”