[v2] Custom struct types using dynamic class definition
Posted: 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:
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
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)
}