/AhkWin32Projection

Win32 API bindings for 64-bit AutoHotkey V2

Primary LanguageAutoHotkeyMIT LicenseMIT

AhkWin32Projection

Win32 bindings for AutoHotkey V2.

This project allows you to replace this:

rect := Buffer(16, 0)
NumPut("int", 20, rect, 12)

With much more friendly, object-oriented syntax:

myRect := Rect()
myRect.top := 20

Replace DllCalls with far more readable function calls...

hDC := DllCall("GetDC", "ptr", 0)
hDC := Gdi.GetDC(0)                 ;Readable!

With rich IntelliSense features and full documentation directly in your IDE: Modern IntelliSense for APIs from the 90s

Table of Contents

What Is This?

This project provides bindings Win32 APIs in 64-bit AutoHotkey V2. It aims greatly simplify the process of working with structs, non-IDispatch COM interfaces, and DllCalls and to alleviate the plague of magic numbers that afflicts AutoHotkey programmers.

The project provides generated struct proxy objects, COM interface proxy objects, constant values, and friendly DllCall wrappers with doc comments rich Intellisense documentation compatible with AHK++.

Imagine you wanted to enumerate all the fonts on your system:

stdout := FileOpen("*", "w")

hDc := Gdi.GetDC(0)     ;Equivalent to DllCall("GetDC", "ptr", 0)

searchFont := LOGFONTW()
searchFont.lfCharSet := FONT_CHARSET.ANSI_CHARSET
searchFont.lfFaceName := "Papyrus"

callback := CallbackCreate(EnumFontFamExProc, "Fast", 4)
Gdi.EnumFontFamiliesExW(hDc, searchFont, callback, 0, 0)

;https://learn.microsoft.com/en-us/previous-versions/dd162618(v=vs.85)
EnumFontFamExProc(lpelfe, lpntme, fontType, lparam){
    logfont := LOGFONTW(lpelfe)
    stdout.WriteLine(logfont.lfFaceName)

    return 1    ;Return non-zero value to continue enumeration
}

No more fewer magic numbers, much more readable code, no mucking around deprecated Win32 documentation!

Usage

This project is intended to be used as a library. You can "install" it by cloning the repo into an AutoHotkey library directory, and then reference types in your script using <library> syntax. Note that many scripts also include other dependent scripts. Files are organized by namespace:

#Include <AhkWin32Projection\Windows\Win32\Foundation\RECT>
#Include <AhkWin32Projection\Windows\Win32\UI\Controls\HDITEMW>

Common Namespaces

Namespaces can be unintuitive and they aren't really mapped to headers, so a few common namespaces are listed here. GitHub's file search functionality is also great if you're looking for something specific. Namespaces come directly from the metadata, so they won't be changed unless Microsoft changes them.

Commonly used Namespaces

Structs

All structs are represented with proxy objects extending Win32Struct. The base class provides utilities for initializing structs, cloning, copying, and comparing memory blocks. Struct proxy objects have properties whose getters and setters invoke NumGet and NumPut (or occasionally StrGet / StrPut):

cchTextMax {
    get => NumGet(this, 24, "int")
    set => NumPut("int", value, this, 24)
}

Embedded structs are not flattened (though unions are). For example, to get the handle of the window for which a NMHDDISPINFOW message was dispatched, you simply access its hdr property:

hwnd := dispInfo.hdr.hwndFrom

Struct proxies are buffer-like, so you can use them anywhere you would use a native Buffer. You can also access the ptr property directly. Unlike native AutoHotkey Buffers, struct proxies cannot be moved or resized.

See the wiki for more details.

Creating Structs

The Win32Struct.__New takes a pointer as its only argument. If that pointer is 0, a new Buffer is created to serve as the object's backing memory. This buffer is always cleared; every member of a new struct starts as 0 / NULL. If the pointer is not zero, the pointer is taken to be a pointer to the start of the struct proxy's memory block. Thus all you need to create a struct proxy is its pointer, as with the font enumeration example above:

logfont := LOGFONTW(lpelfe)     ;Create a LOGFONTW struct at the pointer lpelfe
logfont := LOGFONTW()           ;Create a new LOGFONTW struct backed by a Buffer

You can also initialize new structs using object literals. This works with embedded structures and array members as well. This method copies properties from the object literal into the struct member of the same name.

Wp := WINDOWPLACEMENT({
	length: WINDOWPLACEMENT.sizeof
	showCmd: 1,
	rcNormalPosition: {	  ; An embedded RECT structure
		top: 0,
		bottom: 100,
		left: 0,
		right: 100
	}
})

Struct members not present in the object literal remain at their default values (0 / NULL). In the above example (WINDOWPLACEMENT), the flags, ptMinPosition, and ptMaxPosition members are left unset and default to 0.

When initializing embedded structs or arrays, you can also provide a reference to an Array object or Win32Struct proxy object of the same type as the embedded struct. It is marginally faster to set the struct members manually, as doing so does not require the creation or disposal of a temporary object nor the enumeration of its properties.

COM Interfaces

COM Interfaces are included and use similar syntax to structs. Unlike native AutoHotkey, projected COM interfaces include interfaces that do not implement IDispatch. All COM interface proxies are objects extending Win32ComInterface. See the wiki for more details.

COM interface proxies, like struct proxies, are ultimately objects holding pointers to unmanaged memory. Interface implementations hold pointers to virtual function tables held in Buffers. You can create an interface the same way you create a structure; with a pointer to its virtual function table:

unk := IUnknown(interfacePtr)

COM Interface proxy methods are ultimately wrappers around ComCall. This is true even of methods on interfaces whose implementations are defined by the script; they simply call into an AHK-managed vtable instead of an external one.

Implementing COM Interfaces

The projection provides tools for implementing COM interfaces using objects as well. Simply pass an object whose properties include your implementation's methods, and the virtual function table will be created automatically. Additionally, the projection provides default implementations for all of the IUnknown methods:

;https://learn.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-ipersist
persist := IPersist({
    guid: Guid.Create(),
    GetClassID: (self, vtable, pClassID) => NumPut("ptr", self.guid.ptr, pClassID)
})

See the wiki for gotchas and more details.

Enums and Constants

Enums are simply classes with a series of static read-only variables like so (doc comments removed)

class DUPLICATE_HANDLE_OPTIONS{
    static DUPLICATE_CLOSE_SOURCE => 1
    static DUPLICATE_SAME_ACCESS => 2
}

Similarly, constant values are static read-only variables attached to their namespace's Apis class - see Methods below.

Methods

Rather than emitting global functions, methods (and global constants) are all static to a class named after their relevant namespace. These classes live in files called Apis.ahk in their relevant directories. This decision was made to improve readability and to prevent the interpreter from allocating global variables which are never used.

To use these methods, simply #Include the relevant Apis file and call them:

#Include <AhkWin32Projection\Windows\Win32\Graphics\Gdi\Apis>

hDC := Gdi.GetDC(0)
; Do stuff ...
Gdi.ReleaseDC(0, hDC)

For methods that set the last error, the error value is cleared before DllCall is invoked and checked immediately afterwards. If it is nonzero (that is, if an error occurred), an OSError is thrown.

See the wiki for more details.

Limitations

  • Only 64-bit AutoHotkey is currently supported
    • Generated files all have the #Requires AutoHotkey v2.0.0 64-bit directive, which should prevent mix-ups.
  • Some structs and methods have ANSI and Unicode variants, both are generated. However, AutoHotkey V2 uses UTF-16 by default, and you will save yourself many headaches by sticking to Unicode (-W) variants whenever possible.
  • Only structs with fixed layouts are supported. This means flexible arrays are not supported in structs.
  • In string constants with unprintable characters, those characters are encoded (e.g. a null character becomes \0000) and may need to be decoded before use.
  • Macros are not included in the win32metadata project, and so are not included in the generated AutoHotkey code. While many macros have corresponding functions, not all of them do.
    • See this issue on the win32metadata project page for details.
  • Code examples in the documentation are generally in C++.

Links