Objects

In addition to strings and numbers, AutoHotkey_L supports objects. Objects are treated as a distinct type of value which, like other types of values, can be stored in variables, passed to or returned from functions and stored in other objects.

Note that objects are "reference types" - after an assignment such as food := pizza, food will refer to the original pizza object. Similarly, Eat(pizza) will pass the original pizza object to the Eat() function, which is then able to modify the original object. If the parameter is declared ByRef, the function can also change the pizza variable to point to a different object.

IsObject can be used to determine if a value is an object:

Result := IsObject(expression)

There are two primary types of objects:

Syntax

Supported Operators

Operator support for objects is limited to the following:

Invoking an Object

Two primary forms of syntax are available for invoking functionality of an object: Object[Params] and Object.Param. In both cases, Object may be a variable or a more complex expression which evaluates to an object. In each case, the actual result of the operation is defined by the object. Typically a SET operation will return the value being stored if it is stored successfully.

Object[Params]

Brackets are used to imply an object-operation and delimit its parameter list.

Value  := Object[ Params ]             ; GET
Result := Object[ Params ] := Value    ; SET
Result := Object[ Param  ]( Params )   ; CALL

CALL supports only a single Param between the brackets. Typically this is the name of a method to call or a key/index which was used to store a function name within the object. For more information, see Arrays of Functions.

Object.Param

When the first parameter is a known (literal) value consisting purely of alphanumeric characters and/or underscore, alternate syntax is available:

Value  := Object.Param             ; GET
Result := Object.Param := Value    ; SET
Result := Object.Param( Params )   ; CALL

Although only a single literal parameter can be specified this way, any number of additional parameters can be specified:

Value  := Object.Param1[ Param2, Param3, ... ]            ; GET
Result := Object.Param1[ Param2, Param3, ... ] := Value   ; SET
Result := Object.Param1( Param2, Param3, ... )            ; CALL

Parentheses are generally distinct from brackets, but not when immediately followed by the assignment operator (:=):

Result := Object.Param1( Param2, Param3, ... ) := Value   ; SET

If a dot appears at the beginning of an expression or is preceded by an operator such as + or *, it is unconditionally treated as a floating-point literal. If a dot is followed by a space, it is treated as the concatenation operator; if in that case it is not preceded by a space, an error is raised. Any other unquoted dot (excluding .=) is treated as an object-access operator.

Reference-Counting

A basic reference-counting mechanism is used to automatically deallocate an object when it is no longer referenced by the script. Scripts should not typically invoke this mechanism explicitly; it is invoked automatically as the script deals with objects.

However, taking the address of an object (via address := &object) does not increment its reference count. To ensure the address remains valid when all other references to the object are released, follow this example:

; Increment the object's reference count to "keep it alive":
DllCall(NumGet(NumGet(address+0)+4), "uint", address)
...
; Decrement the object's reference count to allow it to be freed:
DllCall(NumGet(NumGet(address+0)+8), "uint", address)

To run code when the last reference to an object is being released, implement the __Delete meta-function.

Known Limitations:

Object()

Creates a scriptable object which is also an associative array.

Object := Object()
Object := Object(Key1, Value1 [, Key2, Value2 ... ])

Parameters must be in pairs of two, except when the only parameter is an object address.

; Explicit initialization:
obj := Object()
obj[a] := b
obj[c] := d

; In-line initialization:
obj := Object(a, b, c, d)

Associative Arrays

Each object contains a list of key-value pairs - each key or value is a string, number or object. Values are stored in or retrieved from an object by key, as shown below:

array := Object()
array["a"] := "alpha"
array["b"] := "bravo"
MsgBox % array["a"] . " " . array["b"]

Of course, dotted syntax may also be used. Adding to the previous example:

array.a := "apple"
MsgBox % array.a . " " . array.b

To remove a key-value pair, use _Remove.

Integer keys are stored in native 32-bit signed form and therefore must be in the range -2147483648 to 2147483647. Integers outside this range will wrap; key << 32 >> 32 achieves a similar effect. Format is ignored, so for instance x[0x10], x[16] and x[00016] are equivalent. Keys do not need to be contiguous - if only x[1] and x[1000] contain values, the object x really contains only two key-value pairs.

Floating-point numbers are not supported as keys - instead they are converted to strings. Note that floating-point literals are preserved exactly as-is, whereas pure floating-point numbers (such as the result of 0+1.0 or Sqrt(y)) are forced into the current float format.

Note: The key "base" has special meaning, except when used with _Insert.

Built-in Methods

See Object Methods.

Arrays of Arrays

Since objects can contain other objects, arrays of arrays are also possible. For instance, if table is an array of rows and each row is itself an array of columns, the content of column y of row x can be set using either of the methods below:

table[x][y] := content  ; A
table[x, y] := content  ; B

If table[x] does not exist, A and B differ in two ways:

Arrays of Functions

Although true function references aren't yet supported, arrays of functions may be simulated by storing function names in objects. If array[index] contains a function name, the following two examples are equivalent:

array[index](param)
n := array[index]
%n%(param)

If index contains the name of a built-in method, the name of a method defined in a base object, or an object representing a function, only the first example will work.

Known limitation: If an object-call exists on the stack of running functions and subroutines, Exit behaves as though the object-call created a new thread; that is, it returns immediately to the caller of the object-call. However, Exit still causes the script to terminate when appropriate.

Object(address)

To cast an address previously retrieved via address := &object back into a usable object reference, use Object(address). Note that this also works for objects not created by Object(). This is an advanced feature which should be used only when absolutely necessary; see Reference Counting. It may be useful for associating an object with a callback via A_EventInfo.

Extensibility

An object's behaviour can be modified or extended via the special base mechanism. When the key of an operation (the first or only parameter) has no pre-existing value, the object's base is invoked. When a standard base object is invoked, it does the following:

If (and only if) no base object handles the operation, processing will continue as normal:

Example: Simple value inheritance.

Color := Object("R", 0, "G", 0, "B", 0)
blue := Object("B", 255, "base", Color)
cyan := Object("G", 255, "base", blue)
MsgBox % "blue: " blue.R "," blue.G "," blue.B
MsgBox % "cyan: " cyan.R "," cyan.G "," cyan.B

Example: Adding to the previous example, define a method to retrieve the combined RGB value.

Color.GetRGB := "Color_GetRGB"

MsgBox % cyan.GetRGB()  ; Displays 65535.

Color_GetRGB(clr) {
    return clr.R << 16 | clr.G << 8 | clr.B
}

Meta-Functions

Meta-functions allow script to define exactly how an object should act. When called, the parameter list contains a reference to the target object, followed by the parameters of the get, set or call operation. If the function returns a value, it is used as the result of the operation and no further processing occurs. If return is not encountered, processing continues as per the rules described above.

Example: Reimplement the examples above to store only a single RGB value.

Color := Object("RGB", 0
                , "__Set", "Color_Set"
                , "__Get", "Color_Get")

blue := Object("base", Color, "B", 255)
cyan := Object("base", Color, "RGB", 0x00ffff)

MsgBox % "blue: " blue.R "," blue.G "," blue.B " = " blue.RGB
MsgBox % "cyan: " cyan.R "," cyan.G "," cyan.B " = " cyan.RGB

Color_Get(clr, name)
{
    if name = R
        return (clr.RGB >> 16) & 255
    if name = G
        return (clr.RGB >> 8) & 255
    if name = B
        return clr.RGB & 255
}

Color_Set(clr, name, val)
{
    if name in R,G,B
    {
        val &= 255
        
        if      name = R
            clr.rgb := (val << 16) | (clr.rgb & ~0xff0000)
        else if name = G
            clr.rgb := (val << 8)  | (clr.rgb & ~0x00ff00)
        else  ; name = B
            clr.rgb :=  val        | (clr.rgb & ~0x0000ff)
        
        ; 'Return' must be used to indicate a new key-value pair should not be created.
        ; This also defines what will be stored in the 'x' in 'x := clr[name] := val':
        return val
    }
}

It is important to note that when Object() is called with parameters, the key-value pairs are set in left to right order. Since "B" is set before "base" in the example below, a new key-value pair is created and "RGB" is not updated:

c := Object("B", 255, "base", Color)

This behaviour can be used to bypass a meta-function, or for performance in cases where the base is not required for that particular key.

Known limitations:

Cleanup

If an object has associated resources such as file or network handles or pointers to memory allocated with DllCall, these resources may be managed automatically by implementing the __Delete meta-function. For example:

; Create an object with a __delete meta-function:
obj := Object("base", Object("__Delete", "MemoryObject_Delete"))
; Allocate a resource and store it in the object:
obj.ptr := DllCall("GlobalAlloc", "uint", 0, "uint", 1024)

MsgBox % "Memory allocated: " obj.ptr

MemoryObject_Delete(obj)
{   ; Retrieve the resource.
    if p := obj.ptr
    {   ; It hasn't already been freed, so free it.
        DllCall("GlobalFree", "uint", p)
        ; Set to zero so we know it has been freed.
        obj.ptr := 0
    }
    MsgBox % "Memory freed: " p
}

When the last reference to obj is about to be released, obj.base.__Delete is called with obj as the first parameter. Note that at the time __Delete is called, all other references to the object have been released. If there are still references to the object after the function returns (for instance, because the function copied a reference into a static or global variable), the object is not deleted and __Delete may be called again at a later time.

Arrays of Arrays

When a multi-parameter assignment such as table[x, y] := content implicitly causes a new object to be created, the new object ordinarily has no base and therefore no custom methods or special behaviour. __Set may be used to initialize these objects, as demonstrated below.

x := Object("base", Object("addr", "x_Addr", "__Set", "x_Setter"))

; Assign value, implicitly calling x_Setter to create sub-objects.
x[1,2,3] := "..."

; Retrieve value and call example method.
MsgBox % x[1,2,3] "`n" x.addr() "`n" x[1].addr() "`n" x[1,2].addr()

x_Setter(x, p1, p2, p3) {
    x[p1] := Object("base", x.base)
}

x_Addr(x) {
    return &x
}

Since x_Setter has four mandatory parameters, it will only be called when there are two or more key parameters. When the assignment above occurs, the following takes place:

Objects as Functions

When a call such as obj.func(param) is made, obj.func may contain a function name or an object. If obj.func contains an object, it is invoked using obj as a key; for example, the following may be equivalent:

ObjCall(obj.func, obj, param)

In most cases obj.func[obj] does not exist and the __Call meta-function is invoked instead, with the same parameters as shown above (or with more or less parameters). This may be used to change the behaviour of a function-call, as shown in the example below:

FuncType := Object("__Call", "FuncType_Call")
func := Object("base", FuncType, 1, "One", 2, "Two")
obj := Object("func", func, "name", "foo")
obj.func("bar")  ; Shows "One foo bar", "Two foo bar"

FuncType_Call(func, obj, param) {
    Loop % func._MaxIndex()
        ObjCall(func, A_Index, obj, param)
}

One(obj, param) {
    MsgBox % A_ThisFunc " " obj.name " " param
}
Two(obj, param) {
    MsgBox % A_ThisFunc " " obj.name " " param
}

Default Base

When a non-object value is used with object syntax or passed to ObjGet, ObjSet or ObjCall, the default base object is invoked. This can be used for debugging or to globally define object-like behaviour for strings, numbers and/or variables. The default base may be accessed by using .base with any non-object value; for instance, "".base. Although the default base may not be set as in "".base := Object(), the default base may itself have a base as in "".base.base := Object().

Example 1: Automatic Var Init

When an empty variable is used as the target of a set operation, it is passed directly to the __Set meta-function, giving it opportunity to insert a new object into the variable. Limitations: Since the first parameter must be declared ByRef, the meta-function will not be invoked if the target is not a variable. This example does not support multiple parameters.

"".base.__Set := "Default_Set_AutomaticVarInit"

empty_var.foo := "bar"
MsgBox % empty_var.foo

Default_Set_AutomaticVarInit(ByRef var, key, value)
{
    if var =
        var := Object(key, value)
}

Example 2: Pseudo-Properties

Object "syntax sugar" can be applied to strings and numbers.

"".base.__Get := "Default_Get_PseudoProperty"
"".base.is  := "Default_is"

MsgBox % A_AhkPath.length " == " StrLen(A_AhkPath)
MsgBox % A_AhkPath.length.is("integer")

Default_Get_PseudoProperty(nonobj, key)
{
    if key = length
        return StrLen(nonobj)
}

Default_is(nonobj, type)
{
    if nonobj is %type%
        return true
    return false
}

Note that built-in functions may also be used, but in this case the parentheses cannot be omitted:

"".base.length := "StrLen"
MsgBox % A_AhkPath.length() " == " StrLen(A_AhkPath)

Example 3: Debug

If allowing a value to be treated as an object is undesirable, a warning may be shown whenever a non-object value is invoked:

"".base.__Call := "Default__Warn"
"".base.__Set  := "Default__Warn"
"".base.__Get  := "Default__Warn"

empty_var.foo := "bar"
x := (1 + 1).is("integer")

Default__Warn(nonobj, p1="", p2="", p3="", p4="")
{
    ListLines
    MsgBox A non-object value was improperly invoked.`n`nSpecifically: %nonobj%
}