Webview2 iDispatch

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Webview2 iDispatch

Post by kczx3 » 18 Aug 2020, 07:55

The WebView2 control allows you to add host objects to the script. The information on what types it allows is described here - AddHostObjectToScript.

It states that it supports IDispatch and I thought that AHK's objects supported that since we can pass them to IE. I seem to be able to pass an object by first calling ObjPtrAddRef on the object and then passing the returned address. I can see that the browser can see the object however it just shows that its an empty array (which its not). My test object was just { stuff: "things" }. Do I maybe need to manually implement Invoke for the object?
webview2-idispatch.png
webview2-idispatch.png (5.79 KiB) Viewed 2790 times
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Webview2 iDispatch

Post by kczx3 » 18 Aug 2020, 07:58

Guessing I shouldn't implement it directly...
IDispatch wrote:Generally, you should not implement Invoke directly
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Webview2 iDispatch

Post by kczx3 » 18 Aug 2020, 09:55

I'm able to get this to work by passing vref to the browser.

Code: Select all

vbuf := BufferAlloc(24, 0)
vref := ComObject(0x400C, vbuf.ptr)  ; 0x400C is a combination of VT_BYREF and VT_VARIANT.

vref[] := (*) => MsgBox("From AHK", "Hello!", "YN")
Setting vref to an AHK Array sort of works however I can't figure out how to properly access the values. window.chrome.webview.hostObjects.sync.AHK[0] just seems to return the JavaScript proxy object.

Setting vref to a SAFEARRAY works nicely.
webview2-safearray.png
webview2-safearray.png (4.2 KiB) Viewed 2772 times
Setting vref to an AHK object sort of works too. You can directly access the members however trying to call the object itself in the JavaScript console similarly returns a proxy object promise.
webview2-ahk-object.png
webview2-ahk-object.png (35.64 KiB) Viewed 2772 times
Passing a SAFEARRAY directly didn't seem to work either. It seems the VT_BYREF | VT_VARIANT is required or at least circumvents some issue that I'm unfamiliar with.

Passing values from JS to AHK via functions works pretty well. AHK receives a ComObjArray if JS passes an array. If JS passes an object then AHK receives a ComObj but I'm unable to figure out how to reference the properties as dot notation isn't working. Let's say I pass the object {hay: "horses"} from JS to AHK. Trying to access the "hay" property returns this in the JavaScript console: Error: (0x80070005) Access is denied. Value is not modifiable
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Webview2 iDispatch

Post by kczx3 » 26 Aug 2020, 08:24

I feel like the issue with JS objects is somehow related to this note:
Microsoft wrote:Remote JavaScript objects like callback functions are represented as an VT_DISPATCH VARIANT with the object implementing IDispatch. The JavaScript callback method may be invoked using DISPID_VALUE for the DISPID.
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: Webview2 iDispatch

Post by lexikos » 26 Aug 2020, 19:03

All AutoHotkey objects implement IDispatch. There is no need to implement it unless you want to change the behaviour.

In JavaScript, there is no distinction between array elements and properties. The JScript engine (which isn't in use here) invokes x['y'] the same way as x.y: it first calls IDispatch::GetIDsOfNames, and then IDispatch::Invoke. AutoHotkey objects assign an ID to each unique name, then map the ID back to a name when invoked. The flags for Invoke specify to invoke a property; there is no flag for array indexing.

Functions are invoked via IDispatch with DISPID_VALUE instead of mapping a name to an ID. DISPID_VALUE maps to either Call or __Item depending on the type of invocation. Since COM supports parameterised properties, clients such as JScript, VBScript and C# will specify the flags for "either property or method". In such cases, AutoHotkey tries one and then the other. So in other words, the JScript call v = arr(n) can result in a call to arr.__Item[n]. However, the statement arr(n) (where the return value is discarded) is only ever a method call.

That's probably your answer for reading arrays: use arr(n), not arr[n].

For writing to arrays, I was surprised to find that arr(n) = value is accepted by JScript and works. I doubt it would work in any other JavaScript engine.
I feel like the issue with JS objects is somehow related to this note:
Which objects are like callback functions? It's too vague. It tells you how to invoke a callback, but nothing about any other type of object or operation.

%obj%() translates to a method call with DISPID_VALUE if obj is a COM object (IDispatch).
You can directly access the members however trying to call the object itself in the JavaScript console similarly returns a proxy object promise.
How did you try to call the object itself? I see no such attempt in your screenshot; you only evaluate the object (.AHK), retrieve its property (.stuff), and invoke two methods (.invoke() and .things()). I'd guess that .invoke() was supposed to do something special, since it seems you've defined things but not invoke.

If you want to call the object itself, you just call the object itself: .AHK(). In JScript this would result in a call to IDispatch::Invoke with DISPID_VALUE, which would translate to Call (I think the flags would only specify a method call in this context).
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Webview2 iDispatch

Post by kczx3 » 31 Aug 2020, 08:12

I'm trying to find the time to gather my thoughts and questions a bit better. And to provide a working example for reference. Please excuse my delay in a formulated response and thank you for your reply.
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Webview2 iDispatch

Post by kczx3 » 15 Apr 2021, 12:30

I've looked at this again. Not sure I am much further along but am finding naunces that I've overcome a bit.

Let's say I expose a plain AHK array via a ComObject of type VT_BYREF|VT_VARIANT. I can do some interesting things with it from the browser side. I can call standard AHK Array methods but I must pass the Array itself as the first argument.

I've attached everything. You just need to have the current version of the Webview2 runtime installed.

Here is the JS for visibility (but it is also included in the zip). The interesting thing is that AHK native Array methods must be called by passing the object itself as the first argument. So it would be arr.has(arr, 1) or arr.push(arr, "test"). I was never able to get element indexing to work. I tried manually calling many combinations of arr[1] or arr.__index[0] or arr.getHostProperty("__index")[0]. Any time I try with __index I get an error in the console stating there are two few parameters being passed. Oddly enough, I implemented a simple get() method and that works just fine from JS without needing to pass the object itself as the first parameter. Its both fun and frustrating haha.

Code: Select all

const container = document.querySelector("#curr-time");
setInterval(() => {
    container.textContent = (new Date()).toLocaleString();
}, 1000);

(async function() {
    const arr = await window.chrome.webview.hostObjects.AHK;
    const len = await arr.getHostProperty("length"); // otherwise we get the Proxy object's length
    
    const first = await arr.push(arr, "of");
    
    // for (let item in arr.applyHostFunction("__enum")) {
    for (let i = 0; i < len; i++) {
        console.log(await arr.get(i));
    }
    
    console.log(first);
})();
Attachments
webview2.zip
(74.68 KiB) Downloaded 78 times
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: Webview2 iDispatch

Post by lexikos » 16 Apr 2021, 06:17

It would have been helpful to mention which alpha you are using, since it isn't the latest one. It must be either v2.0-a128 or v2.0-a129.

Let's say I expose a plain AHK array via a ComObject of type VT_BYREF|VT_VARIANT.
Let's not confuse matters. The ComObject of type VT_BYREF|VT_VARIANT is only used to store the array's IDispatch pointer in the VARIANT. You pass vref to a DllCall "Ptr" parameter, not a COM method - "ptr", vref is equivalent to "ptr", vref.ptr, which is just the address of your VARIANT buffer. You could just as well do this:

Code: Select all

    arr := ["this", "is", "a", "test"]
    NumPut("int64", 9, "ptr", ObjPtr(arr), arrbuf := BufferAlloc(24))
    wv.AddHostObjectToScript("arr", arrbuf)
const arr = await window.chrome.webview.hostObjects.AHK;
I don't think there's any meaning in awaiting the proxy object. You just get another proxy object, apparently representing the same target object.
const len = await arr.getHostProperty("length");
Why does a proxy object have a length property? :facepalm:

You can use arr.Length instead.
I was never able to get element indexing to work. I tried manually calling many combinations of arr[1] or arr.__index[0] or arr.getHostProperty("__index")[0].
I assume you tried __item rather than __index.

JavaScript fundamentally does not support indexed or parameterized properties. arr[1] in JavaScript is just querying the property 1 of arr. arr.__item[0] is querying arr.__item and then the 0 property of the result, like (arr.__item).0 in AutoHotkey. You can't do it that way in AutoHotkey either.

getHostProperty shouldn't be bound by the limitations of the JavaScript language since it's obviously intended for invoking external objects, but if you query it in the console, you'll get something like:
getHostProperty(propertyName) { return (new AsyncRemoteProxy( this._options, true, this._options.remoteMessenger.postRequestMessage( this._remoteObjectId, propertyName, "get", []).then(message => { t…
Clearly it does not support parameters.

The Scripting.Dictionary COM object has a parameterized property named Item. As far as I can tell, it is not possible to call this from WebView2 either. However, the methods of this object work, like arr.get and not like arr.push.
The interesting thing is that AHK native Array methods must be called by passing the object itself as the first argument.
That was interesting, so I fired up the debugger.
  • await arr.push(1) calls arr->IDispatch::Invoke with the DISPATCH_PROPERTYGET flag and no parameters to retrieve arr.push, then calls (Array.Prototype.Push)->IDispatch::Invoke with the DISPATCH_METHOD flag and one parameter, 1. In other words, it is equivalent to the AutoHotkey v2 expression (arr.push)(1).
  • await arr.get(1) calls arr->IDispatch::Invoke with the DISPATCH_PROPERTYGET flag and no parameters to retrieve arr.get, but this fails. Next, arr->IDispatch::Invoke is called again, but with the DISPATCH_METHOD flag and one parameter, 1.
The failure to retrieve arr.get is the key here. It fails not because get is user-defined, but specifically because you used DefineProp {Call}. In v2.0-a128 and v2.0-a129, properties defined this way have no default implementation of get. If you don't define one, attempting to retrieve the property yields a "no such property" result, which is translated to DISP_E_MEMBERNOTFOUND. It appears to be specifically this result which triggers the second call to IDispatch::Invoke on the original target object.

With v2.0-a130 or later, arr.get(1) will not work, because arr.get will succeed; i.e. it will return a Func.

It would be interesting to see whether WebView2 uses the IDispatchEx interface if present. Unlike IDispatch, IDispatchEx allows specifying this in a call.
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Webview2 iDispatch

Post by kczx3 » 16 Apr 2021, 11:26

Apologies about the alpha version discrepancy. Doesn't help I started this post a while ago and the version's changed a lot since then. I see what you're saying on the latest (a131) version.
Why does a proxy object have a length property? :facepalm:
Not sure! But the key was your following comment.
You can use arr.Length instead.
I had to use Length and not length. Since JS is case-sensitive, the lowercase version was retrieving that property locally, hence the use of getHostProperty. Using the capitalized version works :)
I assume you tried __item rather than __index.
You're correct. I confused the names there.
JavaScript fundamentally does not support indexed or parameterized properties.
Right, I think I understand that. So is there no way to get the elements of the array without creating a wrapper method on the AHK-side to get() the item? Certainly seems like this makes it difficult to enumerate the array too with any for-loop structure in JS aside from a traditional for-loop and using the loop iteration to get() the item in question.
It would be interesting to see whether WebView2 uses the IDispatchEx interface if present. Unlike IDispatch, IDispatchEx allows specifying this in a call.
I have posed the question on the Webview2 Feedback repository and will report back what I find out.
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: Webview2 iDispatch

Post by lexikos » 17 Apr 2021, 00:18

IDispatchEx would provide a workaround if implemented, but keep in mind that AutoHotkey objects currently don't implement it by default.

The inability to invoke parameterized properties, even explicitly with something like getHostProperty, seems like a gap in the API that would be simpler to fill than implementing IDispatchEx.

Of course, it would be easier for us if x.y(z) called IDispatch::Invoke with DISPATCH_METHOD by default, but there's probably some reason it doesn't. For instance, maybe the host engine (V8?) always evaluates x.y first, so translating x.y(z) to DISPATCH_METHOD means creating a temporary function object containing just x and "y", which would subsequently be called with the parameter z *. (I suppose this is how you would have to do it if you implemented a proxy in pure JavaScript or Lua; I considered this when I first implemented objects, and designed them to not require this.)

* Oh right, you could do that to work around the issue;

Code: Select all

Array.Prototype.DefineProp("Push", {Get: this => ObjBindMethod(this, "Push"), Call: Array.Prototype.Push})
Note:
  • This is untested.
  • It's better to do this only for your specific host objects, to avoid changing the behaviour of AutoHotkey code.
  • The "Call: ..." part would be unnecessary for v2.0-a130+, since the method is already implemented as a Call accessor and not a value property.
Post Reply

Return to “Ask for Help (v2)”