Maximizing calls to __Call?

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Maximizing calls to __Call?

13 Oct 2021, 07:14

I have a particular class, which is only used in very narrow circumstances, for which I want the __Call and __Get metafunctions to process as close to EVERY dot resolution as is possible. For test purposes, let's just consider __Call:

Code: Select all

class meta {
  __Call(*) => 'handled'
}
m := meta()
msgbox m.HasProp('z')   ;want this to return 'handled'
But meta's base is still Object.Prototype ... and ultimately Any.Prototype. So the call to .HasProp (or similar) is handled by those prototypes. Is there a way to disable those calls to .base so that meta's __Call will be called? I've tried deleting the Base property, reassigning it, etc., to no avail. Ultimately, everything is an Any. And objects can't really be assigned an incompatible base. And I can't delete the Base property itself.

P.S. I know that calls to Item, Call, etc. never use meta functions, but I'm trying to disable as much resolution from the base chain as is possible in the language... and just for this one weirdo class.
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: Maximizing calls to __Call?

13 Oct 2021, 19:42

  1. define as many custom instance methods/properties of ur own shadowing the names of methods/properties up the prototype chain as u wish to intercept would-be calls to. have ur own methods/properties then delegate to ur own meta-call/get
    • pros:
      • u wrote AHK code
    • cons:
      • meta-call/get wouldnt be the first(and only) thing that was invoked in those particular cases
      • maybe other things depending on what exactly u intend to do with such an object(what im talking about is enumerating its stuff, testing it for specific properties/identity, that kinda thing)
  2. u already know the drill: mess with the internals

    anything deriving from Object(which would be stuff for which (stuff is Object)/or the equivalent HasBase() approach would evaluate to true!! and not stuff for which IsObject(stuff) would, like eg instances of VarRef) has this Base member: https://github.com/Lexikos/AutoHotkey_L/blob/4c61b38d87af2783b84fe11a0ca297b767d9e47c/source/script_object.h#L314

    if u tried assigning something to it using the ahk-provided Any's .Base property, u wouldnt be able to(so much u have already figured out on ur own though): https://github.com/Lexikos/AutoHotkey_L/blob/4c61b38d87af2783b84fe11a0ca297b767d9e47c/source/script_object.cpp#L966-L981

    but that doesnt mean u couldnt assign it something another way. after all, its just pointers, and theres nothing stopping u reading and writing ur own memory.
    in this case, u modify ur class's Prototype to not have any bases up its chain anymore. which takes care of ensuring none of Object's methods/properties and none of Any's are ever invoked instead of ur meta functions.
    CAVEAT: u have to erase the bases on an instance-by-instance basis in the constructor(ie no preemptively doing this inside the class's static __New(), since that would then fuck with the way AHk tries to instantiate objects - ud get a bunch of the same "invalid bases" exceptions u already saw)

    Code: Select all

    #Requires AutoHotkey v2.0-beta.1
    
    ; included just cuz, as a counterpart but its not actually used in the example here
    ObjGetBaseUnchecked(Obj) {
    	if !(Obj is Object)
    		throw TypeError('Expected Object, got ' Type(Obj) '.', -1, 'Obj')
    
    	static offsetof_mBase :=
    		A_PtrSize      ; skip the vtbl
    	  + 4              ; skip the mRefCount
    	  + 4              ; skip the mFlags
    
    	pObj := ObjPtr(Obj)
    	pBase := NumGet(pObj, offsetof_mBase, 'Ptr')
    
    	; we'll be reconstructing an object and returning it,
    	; so incrementing the refcount is required, in this case
    	; otherwise the script would crash at some point when the
    	; object is about meant to get deleted/released
    	Base := ObjFromPtrAddRef(pBase)
    
    	return Base
    }
    
    ObjSetBaseUnchecked(Obj, PtrOrObjNewBase) {
    	if !(Obj is Object)
    		throw TypeError('Expected Object, got ' Type(Obj) '.', -1, 'Obj')
    
    	; we can pass a nullptr or a ptrToObject
    	if (PtrOrObjNewBase is Integer)
    	{
    		pNewBase := PtrOrObjNewBase
    		
    		if pNewBase ; if its an actual ptrToObject
    			ObjAddRef(pNewBase) ; increment its refcount, since we'll assign it as our new base
    		; else
    			; it was a nullptr, so dont have to do anything special
    	}
    	; or we can pass an actual AHK Object
    	else if (PtrOrObjNewBase is Object)
    	{
    		ObjNewBase := PtrOrObjNewBase
    
    		pNewBase := ObjPtrAddRef(ObjNewBase) ; same idea as above, except this consolidated 1-liner function exists
    	}
    	; or ive got no fkin clue what ure passing in, so best not even try doing anything with it
    	else
    		throw TypeError('Expected Integer(ie a ptr to an Object) or Object, got ' Type(PtrOrObjNewBase) '.', -1, 'PtrOrObjNewBase')
    
    	static offsetof_mBase :=
    		A_PtrSize      ; skip the vtbl
    	  + 4              ; skip the mRefCount
    	  + 4              ; skip the mFlags
    
    	pObj := ObjPtr(Obj)
    	pBase := NumGet(pObj, offsetof_mBase, 'Ptr')
    
    	if pBase ; did this obj have an actual Object assigned to it as its base? (eg 'Any' does not)
    		ObjRelease(pBase) ; if so, decrement its refcount, since we'll be overwriting the base of our own object
    
    	; c++ equiv: mBase = aNewBase; 
    	; see https://github.com/Lexikos/AutoHotkey_L/blob/4c61b38d87af2783b84fe11a0ca297b767d9e47c/source/script_object.h#L440
    	NumPut('Ptr', pNewBase, pObj, offsetof_mBase) 
    }
    
    ; this isnt interesting, dont look at it
    fmtCallInfo(caller, methodOrPropName, Params) {
    	callInfo := caller ' called with:`n`n'
    	callInfo .= 'Name = ' methodOrPropName '`n`n'
    	callInfo .= 'Params.Length = ' Params.Length '`n`n'
    
    	for i, param in Params
    		callInfo .= 'Params[' i '] = ' param '`n'
    
    	return callInfo
    }
    
    class Meta
    {
    	__New() {
    		Prototype := this.Base ; equiv to Meta.Prototype, if u were to access it from the class
    		
    		ObjSetBaseUnchecked(Prototype, 0) ; we want to detach any existing bases from the prototype, so assign it a nullptr
    	}
    
    	__Call(Name, Params) {
    		MsgBox fmtCallInfo(A_ThisFunc, Name, Params)
    	}
    
    	__Get(Name, Params) {
    		return fmtCallInfo(A_ThisFunc, Name, Params)
    	}
    }
    
    m := Meta()
    m.HasProp('z') ; invokes ur own-defined metacall, not Any::HasProp
    MsgBox m.Base ; invokes ur own-defined metaget, not Any::Base_getter
    • pros:
      • well, u did write AHK code... in a sense, i guess
      • it does the exact thing u were asking for
    • cons:
      • the AHK code u wrote was written against an (for all intents and purposes) unstable ABI, so any time theres an update it might cease to work(or worse yet cease to work properly)
      • other shit i havent yet thought of. no claims made this was tested extensively
sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Maximizing calls to __Call?

13 Oct 2021, 22:38

@swagfag thanks so much! These are awesome ideas that really help me a lot!

Option A. I spent some of my day today doing the exact thing you proposed in Option A, since that's the only part I really knew how to do. I iterated through the base chain, and within each of those, looped ownprops and shadowed every one (not previously addressed in an earlier base) to call __Call. I was thinking it was a little ugly, but if I do it in static __New of the class, the code only runs once, and none of these duplicative procedures really take up much RAM since they're only in the prototype, right?

Option B. This is really cool! It seemed like such at hack that I shouldn't do it. (Secretly, I was kind of hoping it would turn out to be an lesser-documented but not-too-involved hack, kind of like "".base was much earlier in AHK's existence.) But even though it was very difficult, you still made it happen. Thank you!!! You understand this stuff so well, and your code is so awesome that I absolutely need to put it to use!

In partial answer to one of your questions as to purpose... I think I caused much of my own problem here by extending the built-in prototypes of Any and Object. :) The added methods don't get in the way very often, but I do have this functional programming meta-class related to function composition that gets method-chained in such a way that it dynamically needs to process all the gets/calls at runtime. It wouldn't realistically ever conflict with things like .HasProp, but I have added so many other properties that now the conflict possibility is real. This is one of the only places I ever use meta-functions in my code.

As a possible Option C, I did consider converting to bracket notation for an __Item or Map mechanism. After all, this is I assume one of the reasons these data elements' namespaces were partitioned from properties. I still prefer the dot access, and I do like to test the limits of AHK's flexibility and introspection, and on that front, AHK and you never disappoint! Seems like there's always a way you can get it done.

Thanks again for the help!
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: Maximizing calls to __Call?

14 Oct 2021, 05:20

sirksel wrote:
13 Oct 2021, 22:38
Option A.if I do it in static __New of the class, the code only runs once, and none of these duplicative procedures really take up much RAM since they're only in the prototype, right?
i suppose
Option B. It seemed like such at hack that I shouldn't do it.
all this is doing is essentially bypassing this check here: https://github.com/Lexikos/AutoHotkey_L/blob/4c61b38d87af2783b84fe11a0ca297b767d9e47c/source/script_object.cpp#L966-L978
other than that, the ahk rewrite does the same thing as the c++ code. as long as u dont pass in some kind of a nonsense pointer, u should be fine. well, that and the already pointed out brittleness of code like this
(Secretly, I was kind of hoping it would turn out to be an lesser-documented but not-too-involved hack, kind of like "".base was much earlier in AHK's existence.)
well, it seems v2 is trying its damndest to protect you from you, which is why a lot of these things were removed/got replaced to begin with.
but if those internals were exposed, it could unlock a great deal many more cool possibilities. for example, AHK_H already does that by having defined its own custom A_ScriptStruct(from which u can get stuff like Line info, function lists, variable lists) and A_GlobalStruct(from which u can get stuff like the currently executing function) variables. but then again, any code ud write would be subject to the same already explained caveats, and i dont know how often ud have people write such code, and those that would would probably be doing it for very, very, very specialized purposes, so maybe making AHK support this doesnt actually make much sense in the grand scheme of things
Option C, I did consider converting to bracket notation for an __Item or Map mechanism.
but then ud have to use brackets and quote prop/method names everywhere, every time, soo... yuck
lexikos
Posts: 9583
Joined: 30 Sep 2013, 04:07
Contact:

Re: Maximizing calls to __Call?

17 Oct 2021, 03:13

Option D: Create a "COM" object which implements IDispatch (see outdated example) by constructing a method table with Buffer, NumPut and CallbackCreate, and then wrap it with ComValue. All invocation passes through IDispatch::Invoke. Parameter values can be extracted with ComValue(VT_BYREF|VT_VARIANT), or passed on to a standard AutoHotkey function via IDispatch. The drawbacks are added overhead and loss of precision (or type information) with large integers.

Technically it may be possible to copy the object's existing C++ method table and replace the IObject::Invoke implementation (or implement your own IObject method table outright), but it would be heavily dependent on version-specific implementation details and probably wouldn't be safe due to the use of dynamic_cast within the program code.
sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Maximizing calls to __Call?

18 Oct 2021, 12:11

@lexikos thanks! Another great option. I'm attempting this one too, but it's taking me longer than I thought. I should be able to do all of this inside AHK, correct? The link in your example contains another link to the ComVar() script, which appears to be broken. Maybe I don't need that?
lexikos
Posts: 9583
Joined: 30 Sep 2013, 04:07
Contact:

Re: Maximizing calls to __Call?

21 Oct 2021, 05:51

ComVar is defined in the documentation for ComObject (v1) or ComValue (v2).

If you want to handle everything within Invoke, you would need to extract the parameters from pDispParams. However, it may be easier to rely on the built-in COM interop as much as possible. I have two (mutually exclusive) ideas.

1. Create an internal object on which you define your meta-functions in the normal way. Implement IDispatch::GetIDsOfNames and IDispatch::Invoke by basically just calling the (built-in) IDispatch implementation of the internal object, but make GetIDsOfNames alter the name before passing it on. When the internal object is invoked, the alteration to the name will ensure that the meta-functions execute. The meta-functions must then reverse whatever change you made to the property name.

2. Call the IDispatch::Invoke implementation of Array (the class itself), with:
  • DISPID_VALUE for dispIdMember
  • DISPATCH_METHOD for wFlags
  • pDispParams set to whatever you received.
pVarResult should then contain an Array of parameter values.
sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: Maximizing calls to __Call?

22 Oct 2021, 07:28

@lexikos, thanks so much. I will give it a try. I doubted myself at first read (specifically that I could accomplish all this within AHK), but with that added guidance, I think I can figure it out. I know you're busy, and I certainly appreciate all the help.

Return to “Ask for Help (v2)”

Who is online

Users browsing this forum: coder96, CraigM, niCode, Pyper and 41 guests