Page 1 of 1

[v2] Custom struct types using dynamic class definition

Posted: 01 May 2022, 16:05
by fincs
AutoHotkey v2 sports a completely redesigned object and type system. After reading the documentation with a fresh mind (free of v1 influence) and playing with it for a bit, I've realised the immense power and flexibility that it provides. As opposed to v1, all built-in object types (with very few exceptions) use the same basic expansible class/prototype system. In addition, it is possible to construct and manipulate class prototypes dynamically. Combining these two things enabled me to sketch out a simple way of implementing custom struct types with near zero runtime overhead, while also benefiting from being fully functional native Buffer objects that can be seamlessly used with AutoHotkey v2's built-in library. Here is the proof-of-concept code I came up with:

Code: Select all

#Requires AutoHotkey v2-b+

; Simple example usage using MyStruct
v := MyStruct()
v.someInt := 0xcafe
v.someStr := StrPtr("Hello, world!")
MsgBoxF "Size of MyStruct: {:u} bytes`nType of object: {}`nSome integer: 0x{:08X}`nSome string: {}",
	MyStruct.Size,
	Type(v),
	v.someInt,
	StrGet(v.someStr)

; Structs are fully functional native Buffer objects, and as a result
; they are eligible for optimizations in AutoHotkey's built-in library.
MsgBoxF "MyStruct vtable: 0x{:p}`nBuffer vtable: 0x{:p}",
	NumGet(ObjPtr(v), "Ptr"),
	NumGet(ObjPtr(Buffer()), "Ptr")

; Real world example: Using a Win32 struct
DllCall("GetSystemTime", "Ptr", now := SYSTEMTIME())
MsgBoxF "Today is {:04d}/{:02d}/{:02d}`nCurrent time: {:02d}:{:02d}:{:02d}",
	now.wYear, now.wMonth, now.wDay,
	now.wHour, now.wMinute, now.wSecond

; Helper function for MsgBox + Format (printf-esque)
MsgBoxF(Fmt, Args*) => MsgBox(Format(Fmt, Args*))

; Simple example struct definition
class MyStruct extends EasyStruct {
	static Template := "
	(
		UInt someInt
		Ptr someStr
	)"
}

; https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime
class SYSTEMTIME extends EasyStruct {
	static Template := "
	(
		UShort wYear
		UShort wMonth
		UShort wDayOfWeek
		UShort wDay
		UShort wHour
		UShort wMinute
		UShort wSecond
		UShort wMilliseconds
	)"
}

class EasyStruct extends Buffer {

	; Map of known types along with their sizes for use with NumGet/Put.
	static Types := Map(
		"UChar",  1, "Char",   1,
		"UShort", 2, "Short",  2,
		"UInt",   4, "Int",    4, "Float",  4,
		"Int64",  8,              "Double", 8,

		"Ptr", A_PtrSize, "UPtr", A_PtrSize
	)

	; Disallow creating EasyStruct instances directly.
	; (In the future it could be enhanced to allow creating ad-hoc structs)
	static Call(*) {
		throw Error("Attempted to instantiate an abstract class")
	}

	; This initializer method is called for every descendant of EasyStruct.
	; Build the class prototype dynamically according to its template.
	static __New() {
		if this == EasyStruct
			return

		proto := this.Prototype,
		className := proto.__Class,

		; Remove EasyStruct from the class inheritance
		this.base := EasyStruct.base,
		proto.base := EasyStruct.Prototype.base,

		; Not needed: below will error out if the property does not exist.
		;if not ObjHasOwnProp(this, "Template")
		;	throw Error("Struct " className " has no template definition")

		template := this.Template,
		this.DeleteProp("Template"),

		; Define own-properties for every field in the struct.
		size := 0,
		align := 0
		Loop Parse template, "`n", "`r" {
			if not t := Trim(A_LoopField)
				continue
			if not RegExMatch(t, "^([a-zA-Z_][0-9a-zA-Z_]*)\s+([a-zA-Z_][0-9a-zA-Z_]*)$", &o)
				throw Error("Invalid struct field",, t)
			if not tsize := EasyStruct.Types.Get(tname := o[1], 0)
				throw Error("Invalid field type",, t)

			size := (size + tsize - 1) &~ (tsize - 1), ; Field alignment.
			align := Max(align, size),
			proto.DefineProp(o[2], {
				get: NumGet.Bind(, size, tname),
				set: EasyStruct.NumPutWrap.Bind(, size, tname)
			}),
			size += tsize
		}

		; Finalize the struct class and build a constructor for it.
		size := (size + align - 1) &~ (align - 1), ; Struct alignment.
		this.Size := size,
		this.DefineProp("Call", {
			call: Buffer.Call.Bind(, size, 0)
		})
	}

	; Setter-compatible wrapper for NumPut.
	static NumPutWrap(Offset, Type_, Value) => (NumPut(Type_, Value, this, Offset), Value)

}
In theory it should be possible to build upon this code and design/implement a more complete solution for struct types. I'd say this is quite nice, only made possible by AHKv2 ;)

Re: [v2] Custom struct types using dynamic class definition

Posted: 04 May 2022, 04:36
by AHK_user
@fincs: Great work :bravo: :bravo: :bravo:

Can you modify the class so that It also understands type variables from windows documentation like LONG, WORD,... ?
This makes it even more easier to define the structures.
You could even remove ";" automatically from the template. :D

Re: [v2] Custom struct types using dynamic class definition

Posted: 04 May 2022, 12:23
by AHK_user
I tried MessageBox as an example, but it does not accepts Str parameters, can we fix this?

Code: Select all

#Requires AutoHotkey v2-b+

; Simple example usage using MyStruct
v := MESSAGEBOX()
v.hwnd := 0
v.lpText := "Hello, world!"
v.lpCaption := "Title"
v.uType := 4

DllCall("MessageBox", "Ptr", v)


class MESSAGEBOX extends EasyStruct {
	static Template := "
	(
		Ptr hWnd
		Str lpText
		Str lpCaption
		UInt uType
	)"
}

class EasyStruct extends Buffer {

	; Map of known types along with their sizes for use with NumGet/Put.
	static Types := Map(
		"UChar",  1, "Char",   1,
		"UShort", 2, "Short",  2,
		"UInt",   4, "Int",    4, "Float",  4,
		"Int64",  8,              "Double", 8,

		"Ptr", A_PtrSize, "UPtr", A_PtrSize
	)

	; Disallow creating EasyStruct instances directly.
	; (In the future it could be enhanced to allow creating ad-hoc structs)
	static Call(*) {
		throw Error("Attempted to instantiate an abstract class")
	}

	; This initializer method is called for every descendant of EasyStruct.
	; Build the class prototype dynamically according to its template.
	static __New() {
		if this == EasyStruct
			return

		proto := this.Prototype,
		className := proto.__Class,

		; Remove EasyStruct from the class inheritance
		this.base := EasyStruct.base,
		proto.base := EasyStruct.Prototype.base,

		; Not needed: below will error out if the property does not exist.
		;if not ObjHasOwnProp(this, "Template")
		;	throw Error("Struct " className " has no template definition")

		template := this.Template,
		this.DeleteProp("Template"),

		; Define own-properties for every field in the struct.
		size := 0,
		align := 0
		Loop Parse template, "`n", "`r" {
			if not t := Trim(A_LoopField)
				continue
			if not RegExMatch(t, "^([a-zA-Z_][0-9a-zA-Z_]*)\s+([a-zA-Z_][0-9a-zA-Z_]*)$", &o)
				throw Error("Invalid struct field",, t)
			if not tsize := EasyStruct.Types.Get(tname := o[1], 0)
				throw Error("Invalid field type",, t)

			size := (size + tsize - 1) &~ (tsize - 1), ; Field alignment.
			align := Max(align, size),
			proto.DefineProp(o[2], {
				get: NumGet.Bind(, size, tname),
				set: EasyStruct.NumPutWrap.Bind(, size, tname)
			}),
			size += tsize
		}

		; Finalize the struct class and build a constructor for it.
		size := (size + align - 1) &~ (align - 1), ; Struct alignment.
		this.Size := size,
		this.DefineProp("Call", {
			call: Buffer.Call.Bind(, size, 0)
		})
	}

	; Setter-compatible wrapper for NumPut.
	static NumPutWrap(Offset, Type_, Value) => (NumPut(Type_, Value, this, Offset), Value)

}

Re: [v2] Custom struct types using dynamic class definition

Posted: 05 May 2022, 06:54
by Helgef
Nice one @fincs, the approach with the template string looks really clean :thumbup:

Cheers, and thanks for sharing.
Spoiler

Re: [v2] Custom struct types using dynamic class definition

Posted: 12 Dec 2022, 01:33
by thqby
@fincs

align := Max(align, size),
There's a slight mistake here. It should be align := Max(align, tsize),