Typed properties - experimental build available

Discuss the future of the AutoHotkey language
User avatar
thqby
Posts: 560
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

Post by thqby » 26 Mar 2024, 06:18

Should the __value.get of the pointer class be removed, so that null pointers can be retrieved?
lexikos
Posts: 9780
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

Post by lexikos » 27 Mar 2024, 04:28

@thqby literally just removing the getter would defeat half the point. A field or return value of type RECT.Pointer is supposed to return a pointer-to-RECT, which behaves as a RECT. The object which has the __value property is an implementation detail hidden from the script by __value; it does not behave as a RECT.

The script works by using StructFromPtr to create an actual RECT object with its data pointer set to a pre-existing address. Hypothetically, the built-in implementation would use the RECT.Pointer class object both to define the property type and to implement the RECT-like object.

I intended for __value to return 0 for a null pointer, but forgot.
User avatar
thqby
Posts: 560
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

Post by thqby » 27 Mar 2024, 06:29

If __value returns 0 for a null pointer, there will be ambiguity when the type is int*.
The current solution also makes it impossible for pointer types to be used as arrays, like p[0], p[1].
iseahound
Posts: 1582
Joined: 13 Aug 2016, 21:04
Contact:

Re: Typed properties - experimental build available

Post by iseahound » 27 Mar 2024, 08:22

What's the best solution for a one-off structure returned by a DllCall? I can't define a class inside a function, but would like to wrap the pointer anyways. Also it's possible that a rectangle may be [xywh] or [xyx2y2] so a global definition may not be ideal.
User avatar
thqby
Posts: 560
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

Post by thqby » 27 Mar 2024, 09:39

Code: Select all

fn() {
	static RECT := () {
		obj := Class(Object)
		for k in ['x', 'y', 'w', 'h']
			obj.Prototype.DefineProp(k, { type: 'i32' })
		return obj
	}()
	DllCall(..., RECT, ...)
}
You can create a class dynamically.
lexikos
Posts: 9780
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

Post by lexikos » 28 Mar 2024, 04:19

thqby wrote:
27 Mar 2024, 06:29
If __value returns 0 for a null pointer, there will be ambiguity when the type is int*.
A property of type int* would not return the int value; it would return an int*. How would you get the pointer value if it was automatically dereferenced?

Don't be confused by the fact that a RECT.Pointer property in the pre-release mockup implementation I posted returns a RECT; as I said, "a field or return value of type RECT.Pointer is supposed to return a pointer-to-RECT, which behaves as a RECT." Even if it appears to be a normal struct, the object encapsulates the pointer value, which can be retrieved with the Ptr property.

Assigning to a pointer property stores a new pointer; it does not dererence the existing pointer or new pointer value. If the property was int* rather than RECT*, it would still be for retrieving and setting pointer values, not int values. int* and int& can be distinct types if we ever want the pointer to be automatically dereferenced, returning and setting the int value instead of the pointer.

A single class object couldn't both replicate the behaviour of DllCall's int* return type and allow the pointer value to be retrieved from a struct, since __value's getter would need to have two different behaviours. But we can easily use multiple classes (or just not use a class for one or both purposes).

The current solution also makes it impossible for pointer types to be used as arrays, like p[0], p[1].
What do you mean by "current solution" or "impossible"? What would you have in mind instead?

You can just add an __Item property to the prototype of the class you pass to StructFromPtr (explicitly or via class definition). For now, you also have the option of adding properties to the object returned by StructFromPtr.

Perhaps you are thinking that because a pointer object derives from the same base as an instance of Struct, it can't have an array indexer. But why can't a Struct have an array indexer? It has a Ptr property. Anyway, I don't necessarily intend for pointer objects and struct instances to be the same in the final release. As I said, "I'm not really sure how to handle type identity." That would imply that pointers and struct instances might be different types. Different types can have different properties. Also, "Support for pointers to structs is rudimentary." "There is currently no built-in way to specify a "pointer to struct" type."

The main point of the mockup I posted was just what the Struct class might provide in terms of syntax and reusable functionality for structs. It was hastily written based on whatever ideas I had in my head at the time.
User avatar
thqby
Posts: 560
Joined: 16 Apr 2021, 11:18
Contact:

Re: Typed properties - experimental build available

Post by thqby » 28 Mar 2024, 09:03

lexikos wrote:
28 Mar 2024, 04:19
You can just add an __Item property to the prototype of the class you pass to StructFromPtr (explicitly or via class definition). For now, you also have the option of adding properties to the object returned by StructFromPtr.
Yes, it can be done that way.
The first option is more convenient to use, but the second option is more reasonable and can distinguish their types.
In addition, it is convenient if the value of the structure can be set from an array or from an object key-value pair.

Code: Select all

#Requires AutoHotkey v2.1-alpha.9

class RECT extends Struct {
	left: i32, top: i32, right: i32, bottom: i32
}


PRECT1 := PointerTo(RECT)
PRECT2 := PointerTo2(RECT)
r1 := DllCall('msvcrt\malloc', 'uptr', 32, PRECT1)
r2 := DllCall('msvcrt\malloc', 'uptr', 32, PRECT2)
DllCall("GetWindowRect", "ptr", WinExist("A"), PRECT1, r1)
DllCall("GetWindowRect", "ptr", WinExist("A"), PRECT2, r2)
MsgBox "r1 = {" r1.left "," r1.top "," r1.right "," r1.bottom "}, type = " Type(r1) "`n"
	. "r2 = {" r2[0].left "," r2[0].top "," r2[0].right "," r2[0].bottom "}, type = " Type(r2) ""
r11 := r1[1]
r21 := r2[1]
DllCall("msvcrt\free", PRECT1, r1)
DllCall("msvcrt\free", PRECT2, r2)


class Struct {
	static __new() {
		if this != Struct
			Throw
		this.Prototype.DefineProp('Ptr', { get: ObjGetDataPtr })
		this.Prototype.DefineProp('Size', { get: ObjGetDataSize })
		this.DeleteProp('__new')
		this.DefineProp('at', { call: StructFromPtr })
	}
	static Size => this.Prototype.Size
	static Size(obj) => ObjGetDataSize(obj.Prototype ?? obj)
	static Pointer => (this.DefineProp('Pointer', { value: c := PointerTo(this) }), c)
	__Set(name, args, value) {
		if args.Length
			return this.%name%[args*]
		Throw PropertyError('This value of type "' Type(this) '" has no property named "' name '".', -1)
	}
}

PointerTo(structClass) {
	static cache := Map()
	if c := cache[structClass] ?? false
		return c
	if !ObjGetDataSize(structClass.Prototype)
		Throw TypeError("Invalid class", -1)
	c := Class(Object)
	c.Prototype.cls := structClass
	c.Prototype.DefineProp('Ptr', { type: 'uptr' })
	c.Prototype.DefineProp('__value', {
		get: (this) => this.ptr && StructFromPtr(this.cls, this.ptr).DefineProp('__item', {
			get: (this, index) {
				if !index
					return this
				obj := { base: this.base }
				ObjSetDataPtr(obj, this.ptr + index * ObjGetDataSize(this))
				return obj
			}
		}),
		set: (this, value) {
			if value is Integer
				this.ptr := value
			else if value is this.cls
				this.ptr := value.ptr
			else
				Throw TypeError("Expected a " this.cls.Prototype.__class " or Integer; received a " Type(value), -1)
		}
	})
	cache[structClass] := c
	return c
}

PointerTo2(structClass) {
	static cache := Map()
	if c := cache[structClass] ?? false
		return c
	if !ObjGetDataSize(structClass.Prototype)
		Throw TypeError("Invalid class", -1)
	c := Class(structClass.Prototype.__class '*', Object)
	c.Prototype.cls := structClass
	c.Prototype.DefineProp('Ptr', { type: 'uptr' })
	c.Prototype.DefineProp('__value', {
		set: (this, value) {
			if value is Integer
				this.ptr := value
			else if this.base == value.base || value is this.cls
				this.ptr := value.ptr
			else
				Throw TypeError("Expected a " this.__class " or " this.cls.Prototype.__class " or Integer; received a " Type(value), -1)
		}
	})
	c.Prototype.DefineProp('__item', {
		get: (this, index) {
			if !ptr := this.ptr
				Throw ValueError("null pointer")
			if index
				ptr += index * this.cls.size
			return StructFromPtr(this.cls, ptr)
		}
	})
	cache[structClass] := c
	return c
}
iseahound
Posts: 1582
Joined: 13 Aug 2016, 21:04
Contact:

Re: Typed properties - experimental build available

Post by iseahound » 29 Mar 2024, 21:18

@thqby stupendous code, will be very useful.
lexikos
Posts: 9780
Joined: 30 Sep 2013, 04:07
Contact:

Re: Typed properties - experimental build available

Post by lexikos » 31 Mar 2024, 19:57

@thqby
If the pointer type is used as a property of a struct, a PointerTo-based property will return an object which represents the pointer value. The actual pointer value is copied into the object's internal data pointer field. Modifying the original struct will have no effect on a previously-retrieved pointer value. By contrast, a PointerTo2-based property will return the struct which represents the property itself, which behaves more like a reference to a pointer variable. If the original struct is modified, a previously-retrieved reference to the pointer will use the new pointer value, not the one it had when the reference was retrieved. Mutating the PointerTo2 object will affect the struct as well, but that is a lesser concern.

As for type identity, if R returned from a RECT.Pointer property has all of the properties of a RECT, should R is RECT be true or false? If false:
  • Presumably R is RECT.Pointer should be true.
  • Hypothetically there should be a Pointer base class, so R is Pointer is true.
  • There should be some way to determine the Class (not just type name) of struct it points to.
    (I think that's .cls in your implementation, but I assume that's an implementation detail.)
Because AutoHotkey has no copy-by-value semantics for objects, R created by R := RECT() is effectively no different to a pointer to a RECT, but a smart pointer. The practical differences are:
  • When the smart pointer is no longer referenced, it executes __Delete and deletes the struct.
  • Although both types of pointer objects can (at the moment) have own properties, the smart pointer being the "original" and potentially nested in some other object makes a difference to whether an author might decide to make use of that.
An array indexer for dereferencing the smart pointer doesn't have any practical use because the "array" always has one element, the struct itself. But the same is true for any dumb pointer to a single item.

On the other hand, we can also just say that a RECT.Pointer property returns a reference to a RECT (perhaps the name RECT.UnsafeRef would be more apt for that concept).

To prevent an object created by StructFromPtr from executing __Delete, currently an internal flag is set on the object. This could be more directly exposed to allow the script to determine whether __Delete will be called, and to suppress __Delete for other objects. .NET has something similar: GC.SuppressFinalize(this);
Post Reply

Return to “AutoHotkey Development”