With build v2.1-alpha.2.1, I have added basic support for structs and user-defined types to DllCall.
Struct support is based on
PR #291 by thqby. I retained the
N and "Struct
N" arg types for comparison, but may remove one or both.
Parameters
When an arg type is a class, the parameter value can be either an instance of that class or some other value. If it is not an instance of the class, an instance is constructed and the parameter value is assigned to
__value (like when you assign a value to a property which is a nested struct). In either case, the structured data of the instance itself is passed. The Ptr property is not queried, so it is safe to define such a property for other uses.
As before, the "Ptr" arg type queries the Ptr property. However, if no such property is defined (or only a setter is defined), it uses the address of the object's structured data.
A most simple and boring example:
Code: Select all
class Point {
x : i32, y : i32
}
WindowFromPoint := DllCall.Bind("WindowFromPoint", Point,, "uptr")
GetCursorPos := DllCall.Bind("GetCursorPos", "ptr", unset)
pt := Point()
GetCursorPos(pt)
MsgBox WinGetClass(WindowFromPoint(pt))
__value can be used to allow implicit conversion from other values. For example:
Code: Select all
class Point {
x : i32, y : i32
__value {
set {
this.x := value[1], this.y := value[2]
}
}
}
WindowFromPoint := DllCall.Bind("WindowFromPoint", Point,, "uptr")
MsgBox WinGetClass(WindowFromPoint([A_ScreenWidth//2, A_ScreenHeight//2]))
It is still possible to explicitly construct and pass a Point.
Built-in modifiers for pointer or output parameters are not yet implemented, but the script can implement them. For example:
Code: Select all
class Point {
x : i32, y : i32
}
WindowFromPoint := DllCall.Bind("WindowFromPoint", Point,, "uptr")
GetCursorPos := DllCall.Bind("GetCursorPos", Out(Point), unset)
GetCursorPos(&mp1) ; Let the call construct a new struct.
MsgBox WinGetClass(WindowFromPoint(mp1))
GetCursorPos(mp2 := Point()) ; Output into an existing struct.
MsgBox WinGetClass(WindowFromPoint(mp2))
Out(sc) {
oc := Class()
oc.Prototype := op := {}
op.DefineProp 'p', {type: 'uptr'}
op.DefineProp '__value', {set: setvalue}
setvalue(this, value) {
if value is VarRef
this.p := ObjGetDataPtr(%value% := sc())
else if value is sc
this.p := ObjGetDataPtr(value)
else
throw TypeError('Expected a VarRef or ' sc.Prototype.__Class ' but got a ' type(value), -1)
}
return oc
}
Class arg types are also useful for arg types that one might not normally think of as a struct, such as BSTR, HSTRING, GUID, or whatever other string convention or abstract type that a library might use.
Example: Transparently handle BSTR.
Code: Select all
MsgBox DllCall("oleaut32\SysStringLen", BSTR, "abc" Chr(0) "123")
MsgBox DllCall("oleaut32\SysAllocString", "wstr", "xyz", BSTR)
class BSTR { ; This type can also be used in a struct.
ptr : uptr
size => DllCall("oleaut32\SysStringByteLen", "ptr", this, "uint")
__value {
get => StrGet(this)
set {
if this.ptr ; In case of use in a struct.
this.__delete()
this.ptr := DllCall("oleaut32\SysAllocStringLen", "wstr", value, "uint", StrLen(value), "ptr")
}
}
__delete => DllCall("oleaut32\SysFreeString", "ptr", this)
}
Return Values
When a return type is a class, an instance is constructed before calling the function. Putting aside the details of the ABI, the instance receives the return value. Prior to returning, DllCall
gets the
__value property. If the property is defined (and not just a setter), whatever it returns becomes the return value of DllCall.
There aren't many standard functions that return a struct by value, but one example is
lldiv (this example is translated from the one on that page):
Code: Select all
class lldiv_t {
quot : i64, rem : i64
}
res := DllCall("ucrtbase\lldiv", "int64", 31558149, "int64", 3600, lldiv_t)
MsgBox Format("Earth orbit: {} hours and {} seconds.", res.quot, res.rem)
Class return types are useful not only with "structs", but with more opaque or abstract values.
Example: Wrap a HWND.
Code: Select all
class Window {
hwnd : uptr
ClassName => WinGetClass(this)
Title => WinGetTitle(this)
;...
}
MsgBox DllCall("GetDesktopWindow", Window).ClassName
MsgBox DllCall("GetForegroundWindow", Window).Title
Also for error-checking.
Example: Wrap a HMODULE and detect failure.
Code: Select all
class LoadedHModule {
ptr : uptr
__value {
get {
if !this.ptr
throw OSError(A_LastError)
return this
}
}
__delete() => DllCall("FreeLibrary", "ptr", this)
}
check
gdip := DllCall("LoadLibrary", "str", "gdiplus", LoadedHModule)
check
gdip := unset
check
DllCall("LoadLibrary", "str", "this is bound to fail", LoadedHModule) ; OSError(126)
check() => MsgBox(DllCall("GetModuleHandle", "str", "gdiplus", "ptr") ? "Loaded" : "Not loaded")
Sometimes the handling for a return value varies by function. Rather than defining a class with the specific behaviour needed for a function, it can be handled with composition. For example:
Code: Select all
class HModule {
ptr : uptr
}
LoadedHModule := FreedWith(DllCall.Bind("FreeLibrary", "ptr", unset),
Checked(ThrowsIf(m => !m.ptr, OSError),
HModule))
check
gdip := DllCall("LoadLibrary", "str", "gdiplus", LoadedHModule)
check
gdip := unset
check
DllCall("LoadLibrary", "str", "this is bound to fail", LoadedHModule) ; OSError(126)
Checked(f, t) {
c := Class()
c.Prototype := {base: t.Prototype}
c.Prototype.DefineProp '__value', {get: f}
return c
}
FreedWith(f, t) {
c := Class()
c.Prototype := {base: t.Prototype}
super_delete := (t.Prototype.__Delete?)
c.Prototype.DefineProp '__delete', {
call: this => (
f(this),
super_delete?.(this)
)
}
return c
}
ThrowsIf(pred, errorClass) {
f(p?) {
if pred(p?)
throw errorClass(, -1)
return (p?)
}
return f
}
check() => MsgBox(DllCall("GetModuleHandle", "str", "gdiplus", "ptr") ? "Loaded" : "Not loaded")
CDecl
In order to permit the return type to be a class (not a string),
CDecl is no longer required. This was already the case on x64, where specifying "CDecl" has no effect because x64 has its own calling convention.
The short version:
- An error is no longer thrown if you pass parameters to an x86 stdcall function which takes no parameters.
- Passing the wrong number/size of parameters to any other x86 stdcall function now throws an error even if you erroneously specify CDecl.
- Specifying CDecl literally has no effect, and all other errors that were previously detected are still detected.
Normally, an x86 stdcall function "removes" its parameters from the stack by adding the total parameter size to the stack pointer (ESP), while a cdecl function leaves ESP at whatever value it had before the call. So in other words, if the call increases ESP by
delta bytes, the function must be stdcall and must have expected a parameter list totalling
delta bytes, unless
delta is zero. For both calling conventions, DllCall restores ESP to whatever value it had at the start. The parameters are prepared on the stack exactly the same for both calling conventions, so the value of ESP is only used to detect errors (sometimes after the damage is done).
If DllCall pushes
p bytes of parameters and the call adjusts ESP by
delta bytes,
- When delta = 0, either the function was stdcall with no parameters, or it was cdecl.
- If p > 0, previous versions threw an error unless CDecl was used. Now no error is thrown regardless of CDecl. This generally isn't an issue because a function which takes no parameters will ignore whatever you put on the stack, and DllCall will restore ESP to its previous value either way.
- If p = 0, there's no difference between stdcall and cdecl, so no error is or was thrown.
Otherwise, the non-zero delta proves the function is stdcall.
- When p = delta, the stdcall function was called with the correct number/size of parameters. In old and new versions, DllCall doesn't care whether you specify CDecl in this case (even though a non-zero delta proves that the function is not CDecl).
- When p > delta, the parameter list was too large. The function would have ignored the excess parameters, but an error is thrown anyway to inform you that the parameters are incorrect.
- When p < delta, the parameter list was too small. The function would have used whatever happened to be on the stack, so the behaviour is effectively "undefined". Generally this doesn't corrupt the program state too badly, and DllCall throws an error to inform you of the mistake.
Note that because
p &&
p !=
delta proves that the function was called incorrectly, an error is now thrown even if CDecl was used. This differs from the previous behaviour, which was to ignore whatever the function does with ESP if CDecl is used.
Again, this only relates to x86. On x64,
delta should always be 0.