[v2] Custom struct types using dynamic class definition

Put simple Tips and Tricks that are not entire Tutorials in this forum
User avatar
fincs
Posts: 527
Joined: 30 Sep 2013, 14:17
Location: Seville, Spain
Contact:

[v2] Custom struct types using dynamic class definition

01 May 2022, 16:05

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 ;)
fincs
Windows 11 Pro (Version 22H2) | AMD Ryzen 7 3700X with 32 GB of RAM | AutoHotkey v2.0.0 + v1.1.36.02
Get SciTE4AutoHotkey v3.1.0 - [My project list]
AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

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

04 May 2022, 04:36

@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
AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

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

04 May 2022, 12:23

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)

}
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

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

05 May 2022, 06:54

Nice one @fincs, the approach with the template string looks really clean :thumbup:

Cheers, and thanks for sharing.
Spoiler
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

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

12 Dec 2022, 01:33

@fincs

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

Return to “Tips and Tricks”

Who is online

Users browsing this forum: No registered users and 14 guests