Typed properties - experimental build available

Discuss the future of the AutoHotkey language
iseahound
Posts: 1447
Joined: 13 Aug 2016, 21:04
Contact:

Re: Typed properties - experimental build available

24 Jul 2023, 22:23

Right. My main concern is that someone new to the language would assume typed properties ≠ structs. Thereby using typed properties as a way to formally check their code. I do however enjoy the current proposed implementation, and believe that you could probably get typed properties, structs, and even data classes working with this approach.

Also, thinking ahead, one question that might pop up is: How do I pick and choose which typed properties get included into the struct? A user might want to just use types for type checking, yet would only want some of the types to be applicable to the struct. Or perhaps more common, the user would have 3 properties, only two of which are needed in the struct, so the datasize of the struct must be calculated manually, and they would like to avoid that calculation.
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

25 Jul 2023, 01:17

I am not concerned. If there is exactly one easy way to define a struct, they will learn it or not. Anyone can and should use typed properties for whatever purpose will benefit them.

How do you pick and choose, in a C struct or C++ class, which fields are part of the object's main structure and which fields are separate to it? You don't. How would you want the syntax to work for such a thing? Just define multiple classes.

You can calculate the size of a series of properties, or a single property, from its offset and the offset of the next property or the size of the overall struct.
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

28 Jul 2023, 21:39

As an example of extending typed properties to other parts of the language, consider callbacks.

A function could have typed parameters, automatically performing type validation and providing a signature for a C callback. This could be utilized either by CallbackCreate or transparently when a script function is passed to a native function. For instance, a function like EnumDisplayMonitors could be passed a Closure or BoundFunc without requiring the use of CallbackCreate and CallbackFree.

This could also extend to the creation of COM interfaces from classes.
ntepa
Posts: 429
Joined: 19 Oct 2022, 20:52

Re: Typed properties - experimental build available

31 Jul 2023, 03:50

I tried making an "array". The Get and Set method works, but why does it fail when using the __Item property?

Code: Select all

class Struct {
    Ptr => ObjGetDataPtr(this)
    Size => ObjGetDataSize(this)
}

class FloatArray extends Struct {
    static Call(num) {
        p := {Base:this.Prototype}
        p.DefineProp("Ptr", {Type: num * 4})
        p.DefineProp("Size", {Get:this => num * 4})
        return {Prototype:p}
    }
    __Item[i] {
        Get => NumGet(this, i * 4, "float")
        Set => NumPut("float", value, this, i * 4)
    }
    Get(i) => NumGet(this, i * 4, "float")
    Set(i, value) => NumPut("float", value, this, i * 4)
}

class XStruct {
    f: FloatArray(8)
}

x := XStruct()
x.f.Set(0, 1.1)
MsgBox x.f.Get(0)

x.f[0] := 1.2
MsgBox x.f[0]
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

31 Jul 2023, 07:59

It is caused by the fact that token_for_recursion.mem_to_free in the source code is not initialized.

https://github.com/AutoHotkey/AutoHotkey/blob/ptype/source/script_object.cpp#L729-L730
Adding token_for_recursion.mem_to_free = nullptr; between these two lines can solve the problem.
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

31 Jul 2023, 08:15

ntepa wrote:
31 Jul 2023, 03:50

Code: Select all

class Struct {
    Ptr => ObjGetDataPtr(this)
    Size => ObjGetDataSize(this)
}

class FloatArray extends Struct {
    static Call(num) {
        p := {Base:this.Prototype}
        p.DefineProp("Ptr", {Type: num * 4})
        p.DefineProp("Size", {Get:this => num * 4})
        return {Prototype:p}
    }
    __Item[i] {
        Get => NumGet(this, i * 4, "float")
        Set => NumPut("float", value, this, i * 4)
    }
    Get(i) => NumGet(this, i * 4, "float")
    Set(i, value) => NumPut("float", value, this, i * 4)
}

class XStruct {
    f: FloatArray(8)
}

x := XStruct()
x.f.Set(0, 1.1)
MsgBox x.f.Get(0)

x.f[0] := 1.2
MsgBox x.f[0]
Add meta-function __Delete to XStruct, nested structure calls XStruct's __Delete instead of its own. Is this a bug or intentional?
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: Typed properties - experimental build available

01 Aug 2023, 10:24

Thanks for your answers.
lexikos wrote: It is more useful, almost by definition.
Strictly speaking, maybe, but practically, sometimes simpler is plain better.

I don't really care if it's int x or x : i32 or whatever, my concern is more related to if we have to define size and ptr props for each struct, or write custom enums if want to traverse a class/struct's typed props only, or similar. I suppose its ok if we have a struct class built-in, to extend. Like some example above.

Anyways, it is difficult for me to asses this as this is still experimental and I cannot see the full picture yet. I will follow this with interest.

Cheers.
iseahound
Posts: 1447
Joined: 13 Aug 2016, 21:04
Contact:

Re: Typed properties - experimental build available

05 Aug 2023, 07:36

Regarding nested classes, there isn't a good way to specify an outer property to reference the outer class, nor is it possible for an instance of an object to access a nested class (as nested classes behave as if static class was written instead). These are 2 points that will provide a stumbling block if every struct as the same layout as a class.
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

05 Aug 2023, 19:24

@iseahound nonsense as usual.

In every other language, if a struct has a "reference" to the outer struct, it's just another field that must be set when the struct is initialized. There is no reason you can't already do this, aside from aviding circular references. It's no different between structs and non-struct objects.

In the current implementation, the inner struct needs a reference to the outer struct to ensure memory safety. This could be exposed to the script, in which case there would be no reason to define a property because you will be able to retrieve the outer struct without doing so. If this would be useful at all, it would mostly be useful with custom objects and not structs, since structs in other languages don't have this capability.

Nested classes and nested structs are entirely separate things, and nested classes have never implied an "inner-outer" or "child-parent" relationship between instances.

I don't believe any of this has any relevance to the issue of whether a struct is defined by dedicated syntax or a dedicated base class, or are just implied by a class having typed properties. Perhaps you can demonstrate otherwise, or explain what you meant by "if every struct as the same layout as a class", which I may have misinterpreted as it doesn't make sense literally as written.
iseahound
Posts: 1447
Joined: 13 Aug 2016, 21:04
Contact:

Re: Typed properties - experimental build available

07 Aug 2023, 09:57

Nevermind, I think I see where you are going with this. In my previous post, I was referring to a "game" where if a person has fields that they want to include into the struct, and fields that they do not, one strategy would be to place the non-struct fields in an outer class, and instantiate an inner class with the required fields. There are of course other approaches, such as passing an object that contains all the non-struct fields, or using globals. But the nested class approach may seem like a natural solution, but contains pitfalls that may be unintuitive.

I personally agree with your current approach. I'm just thinking of the specific approaches a new programmer might take to solve the question of "I have instance properties, some of which should be in the struct, and some which should not, how do I do this?". (For example, one solution may be to place all the struct properties in the front, and the typed properties in the back, but this prevents the size of the struct from being automatically generated.)
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

17 Aug 2023, 23:07

With build v2.1-alpha.2.1, I have added basic support for structs and user-defined types to DllCall.

Struct support is based on PR #291 by thqby. I retained the N and "StructN" arg types for comparison, but may remove one or both.


Parameters

When an arg type is a class, the parameter value can be either an instance of that class or some other value. If it is not an instance of the class, an instance is constructed and the parameter value is assigned to __value (like when you assign a value to a property which is a nested struct). In either case, the structured data of the instance itself is passed. The Ptr property is not queried, so it is safe to define such a property for other uses.

As before, the "Ptr" arg type queries the Ptr property. However, if no such property is defined (or only a setter is defined), it uses the address of the object's structured data.

A most simple and boring example:

Code: Select all

class Point {
    x : i32, y : i32
}
WindowFromPoint := DllCall.Bind("WindowFromPoint", Point,, "uptr")
GetCursorPos := DllCall.Bind("GetCursorPos", "ptr", unset)

pt := Point()
GetCursorPos(pt)
MsgBox WinGetClass(WindowFromPoint(pt))
__value can be used to allow implicit conversion from other values. For example:

Code: Select all

class Point {
    x : i32, y : i32
    __value {
        set {
            this.x := value[1], this.y := value[2]
        }
    }
}
WindowFromPoint := DllCall.Bind("WindowFromPoint", Point,, "uptr")
MsgBox WinGetClass(WindowFromPoint([A_ScreenWidth//2, A_ScreenHeight//2]))
It is still possible to explicitly construct and pass a Point.

Built-in modifiers for pointer or output parameters are not yet implemented, but the script can implement them. For example:

Code: Select all

class Point {
    x : i32, y : i32
}
WindowFromPoint := DllCall.Bind("WindowFromPoint", Point,, "uptr")
GetCursorPos := DllCall.Bind("GetCursorPos", Out(Point), unset)

GetCursorPos(&mp1) ; Let the call construct a new struct.
MsgBox WinGetClass(WindowFromPoint(mp1))

GetCursorPos(mp2 := Point()) ; Output into an existing struct.
MsgBox WinGetClass(WindowFromPoint(mp2))

Out(sc) {
    oc := Class()
    oc.Prototype := op := {}
    op.DefineProp 'p', {type: 'uptr'}
    op.DefineProp '__value', {set: setvalue}
    setvalue(this, value) {
        if value is VarRef
            this.p := ObjGetDataPtr(%value% := sc())
        else if value is sc
            this.p := ObjGetDataPtr(value)
        else
            throw TypeError('Expected a VarRef or ' sc.Prototype.__Class ' but got a ' type(value), -1)
    }
    return oc
}
Class arg types are also useful for arg types that one might not normally think of as a struct, such as BSTR, HSTRING, GUID, or whatever other string convention or abstract type that a library might use.

Example: Transparently handle BSTR.

Code: Select all

MsgBox DllCall("oleaut32\SysStringLen", BSTR, "abc" Chr(0) "123")
MsgBox DllCall("oleaut32\SysAllocString", "wstr", "xyz", BSTR)

class BSTR {  ; This type can also be used in a struct.
    ptr : uptr
    size => DllCall("oleaut32\SysStringByteLen", "ptr", this, "uint")
    __value {
        get => StrGet(this)
        set {
            if this.ptr ; In case of use in a struct.
                this.__delete()
            this.ptr := DllCall("oleaut32\SysAllocStringLen", "wstr", value, "uint", StrLen(value), "ptr")
        }
    }
    __delete => DllCall("oleaut32\SysFreeString", "ptr", this)
}


Return Values

When a return type is a class, an instance is constructed before calling the function. Putting aside the details of the ABI, the instance receives the return value. Prior to returning, DllCall gets the __value property. If the property is defined (and not just a setter), whatever it returns becomes the return value of DllCall.

There aren't many standard functions that return a struct by value, but one example is lldiv (this example is translated from the one on that page):

Code: Select all

class lldiv_t {
    quot : i64, rem : i64
}
res := DllCall("ucrtbase\lldiv", "int64", 31558149, "int64", 3600, lldiv_t)
MsgBox Format("Earth orbit: {} hours and {} seconds.", res.quot, res.rem)
Class return types are useful not only with "structs", but with more opaque or abstract values.

Example: Wrap a HWND.

Code: Select all

class Window {
    hwnd : uptr
    ClassName => WinGetClass(this)
    Title => WinGetTitle(this)
    ;...
}
MsgBox DllCall("GetDesktopWindow", Window).ClassName
MsgBox DllCall("GetForegroundWindow", Window).Title
Also for error-checking.

Example: Wrap a HMODULE and detect failure.

Code: Select all

class LoadedHModule {
    ptr : uptr
    __value {
        get {
            if !this.ptr
                throw OSError(A_LastError)
            return this
        }
    }
    __delete() => DllCall("FreeLibrary", "ptr", this)
}

check
gdip := DllCall("LoadLibrary", "str", "gdiplus", LoadedHModule)
check
gdip := unset
check

DllCall("LoadLibrary", "str", "this is bound to fail", LoadedHModule) ; OSError(126)

check() => MsgBox(DllCall("GetModuleHandle", "str", "gdiplus", "ptr") ? "Loaded" : "Not loaded")
Sometimes the handling for a return value varies by function. Rather than defining a class with the specific behaviour needed for a function, it can be handled with composition. For example:

Code: Select all

class HModule {
    ptr : uptr
}
LoadedHModule := FreedWith(DllCall.Bind("FreeLibrary", "ptr", unset),
                    Checked(ThrowsIf(m => !m.ptr, OSError),
                        HModule))

check
gdip := DllCall("LoadLibrary", "str", "gdiplus", LoadedHModule)
check
gdip := unset
check

DllCall("LoadLibrary", "str", "this is bound to fail", LoadedHModule) ; OSError(126)

Checked(f, t) {
    c := Class()
    c.Prototype := {base: t.Prototype}
    c.Prototype.DefineProp '__value', {get: f}
    return c
}
FreedWith(f, t) {
    c := Class()
    c.Prototype := {base: t.Prototype}
    super_delete := (t.Prototype.__Delete?)
    c.Prototype.DefineProp '__delete', {
        call: this => (
            f(this),
            super_delete?.(this)
        )
    }
    return c
}
ThrowsIf(pred, errorClass) {
    f(p?) {
        if pred(p?)
            throw errorClass(, -1)
        return (p?)
    }
    return f
}

check() => MsgBox(DllCall("GetModuleHandle", "str", "gdiplus", "ptr") ? "Loaded" : "Not loaded")


CDecl

In order to permit the return type to be a class (not a string), CDecl is no longer required. This was already the case on x64, where specifying "CDecl" has no effect because x64 has its own calling convention.

The short version:
  • An error is no longer thrown if you pass parameters to an x86 stdcall function which takes no parameters.
  • Passing the wrong number/size of parameters to any other x86 stdcall function now throws an error even if you erroneously specify CDecl.
  • Specifying CDecl literally has no effect, and all other errors that were previously detected are still detected.
Normally, an x86 stdcall function "removes" its parameters from the stack by adding the total parameter size to the stack pointer (ESP), while a cdecl function leaves ESP at whatever value it had before the call. So in other words, if the call increases ESP by delta bytes, the function must be stdcall and must have expected a parameter list totalling delta bytes, unless delta is zero. For both calling conventions, DllCall restores ESP to whatever value it had at the start. The parameters are prepared on the stack exactly the same for both calling conventions, so the value of ESP is only used to detect errors (sometimes after the damage is done).

If DllCall pushes p bytes of parameters and the call adjusts ESP by delta bytes,
  • When delta = 0, either the function was stdcall with no parameters, or it was cdecl.
    • If p > 0, previous versions threw an error unless CDecl was used. Now no error is thrown regardless of CDecl. This generally isn't an issue because a function which takes no parameters will ignore whatever you put on the stack, and DllCall will restore ESP to its previous value either way.
    • If p = 0, there's no difference between stdcall and cdecl, so no error is or was thrown.
    Otherwise, the non-zero delta proves the function is stdcall.
  • When p = delta, the stdcall function was called with the correct number/size of parameters. In old and new versions, DllCall doesn't care whether you specify CDecl in this case (even though a non-zero delta proves that the function is not CDecl).
  • When p > delta, the parameter list was too large. The function would have ignored the excess parameters, but an error is thrown anyway to inform you that the parameters are incorrect.
  • When p < delta, the parameter list was too small. The function would have used whatever happened to be on the stack, so the behaviour is effectively "undefined". Generally this doesn't corrupt the program state too badly, and DllCall throws an error to inform you of the mistake.
Note that because p && p != delta proves that the function was called incorrectly, an error is now thrown even if CDecl was used. This differs from the previous behaviour, which was to ignore whatever the function does with ESP if CDecl is used.

Again, this only relates to x86. On x64, delta should always be 0.
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

18 Aug 2023, 09:12

@lexikos

Code: Select all

class b {
	a: i8
	__Delete() {
		OutputDebug('b' ObjPtr(this) '`n')
	}
}
class a {
    b: b
    __Delete() {
		OutputDebug('a' ObjPtr(this) '`n')
	}
}

x := a()
x := 0
Nested structures call the __delete function of the external structure at destruct time.
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

18 Aug 2023, 21:13

@thqby I think you already reported that, and I forgot to look into it. Thanks for the reminder.

CallMeta should be mNested[i]->CallMeta in Object::CallNestedDelete.
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

22 Aug 2023, 22:42

@lexikos

https://github.com/AutoHotkey/AutoHotkey/blob/ptype/source/script_object.cpp#L668-L670
Can this be simplified to auto realthis = dynamic_cast<Object*>(TokenToObject(aThisToken));? So that SYM_TYPED_FIELD can also use
Debugger::PropertyWriter::WriteDynamicProperty.
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

24 Aug 2023, 06:09

You mean remove the condition if (aFlags & (IF_SUBSTITUTE_THIS | IF_SUPER)). This condition represents a minority of cases where some additional CPU cycles need to be spent to determine the target object, because the normally obvious and immediately available this isn't the target object. Removing the condition is not a good way to fix the debugger.

Both flags are for situations when (C++) this contains the property but aThisToken represents the actual target object (this in .ahk). WriteDynamicProperty wouldn't work because this is the prototype object (from the debugger pseudo-property .<base>) while aThisToken is the struct. So we should just add one of those flags, and Object::Invoke will then use aThisToken.

They do the same thing, but have a different nuance of meaning. IF_SUPER is used by super.prop, where the target is the base of the prototype object associated with the current function (userFunc->mClass->mBase), while IF_SUBSTITUTE_THIS is used by "".prop, where the target is the prototype object of a Primitive class. There is currently no difference in behaviour; I think the original idea was for IF_SUBSTITUTE_THIS to prohibit creating new properties (in the prototype) for "".prop := value, but IF_SUPER has the same effect since aThisToken doesn't contain an Object.
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

24 Aug 2023, 09:32

All objects that inherit Object can define typed properties, but use the Object constructor when used as parameter types in DllCall.
https://github.com/AutoHotkey/AutoHotkey/blob/ptype/source/lib/DllCall.cpp#L668

Code: Select all

class tp extends Map {
  a: i8
}
DllCall(..., tp, ...)
User avatar
V2User
Posts: 195
Joined: 30 Apr 2021, 04:04

Re: Typed properties - experimental build available

24 Aug 2023, 10:16

lexikos wrote:
15 Jul 2023, 04:14
Properties can be declared in the class body with either name : propType or name : propType := initializer
How about simply insert typename directly into the := operator? It would be prpt1 :i32= 8 rather than prpt1:i32:=8. Since the nested colons use has been decreased, it seems easier to read.
Therefore :i32=8 will becoming an operator, overloading :=, just like operator overload is implemented. It also brings the possibility to apply the feature to variables. In this situation, When var1:Class1=8 is used, Class1().__value will be invoked. Then in the __value, first it will call set(), second it will call get() to return result to var1.
Last edited by V2User on 25 Aug 2023, 11:29, edited 1 time in total.
User avatar
V2User
Posts: 195
Joined: 30 Apr 2021, 04:04

Re: Typed properties - experimental build available

24 Aug 2023, 12:00

lexikos wrote:
17 Aug 2023, 23:07
It is still possible to explicitly construct and pass a Point.
Built-in modifiers for pointer or output parameters are not yet implemented,
There are perhaps two other ways I advised to achieve. You might implement these:
1. Implement something like "typename*"/Ptr*, such as DllCall("WindowFromPoint", Ptr(Point), &mp1Ptr, "uptr"). Whenever Point().__value() is returned, mp1Ptr is updated. mp1Ptr can be a PtrInteger as well as an object with a ptr property waiting to be updated.
2. Just to allow Varref and its Subclass to become a typename of DllCall(), such as:
[Code1:]

Code: Select all

class VarRef2 extends VarRef {
	__value{

	}
}
......
DllCall("WindowFromPoint", VarRef2, &mp1, "uptr").
mp1 is the variable of a Point object. It may get changed to another object after Code1.
Last edited by V2User on 24 Aug 2023, 22:57, edited 1 time in total.
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

24 Aug 2023, 17:06

V2User wrote:There are perhaps two other ways I advised to achieve.
I already demonstrated the first method with class Out.
Just to allow Varref and its Subclass to become a typename of DllCall()
For what purpose?
DllCall("WindowFromPoint", VarRef2, &mp1, "uptr")
There is no reason for VarRef2 to extend VarRef. You are not passing a VarRef2, so it would construct a new VarRef2 and set __value. You cannot pass a VarRef2, because there is no meaningful way to extend VarRef and allow it to be instantiated.
User avatar
V2User
Posts: 195
Joined: 30 Apr 2021, 04:04

Re: Typed properties - experimental build available

24 Aug 2023, 23:14

lexikos wrote:
24 Aug 2023, 17:06
For what purpose?
Because VarRef doesn't have __value property. So, define __value() in its subClass to be called and returned.
Only what I personally thought. :)
Last edited by V2User on 25 Aug 2023, 11:27, edited 1 time in total.

Return to “AutoHotkey Development”

Who is online

Users browsing this forum: No registered users and 77 guests