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 currently three 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

Objects use a basic reference-counting mechanism to automatically deallocate the memory they occupy when 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, if a script obtains an unmanaged pointer to an object (for instance, via Object(obj)), the reference count may go out of sync, in which case Bad Things will happen. To prevent this, use the following functions as appropriate:

; Increment the object's reference count to "keep it alive":
ObjAddRef(address)
...
; Decrement the object's reference count to allow it to be freed:
ObjRelease(address)

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

Known Limitations:

Although memory used by the object is reclaimed by the operating system when the program exits, __Delete will not be called unless all references to the object are freed. This can be important if it frees other resources which are not automatically reclaimed by the operating system, such as temporary files.

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 in single-parameter mode.

; 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.

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 limitations:

Object( address | object )

Retrieves an interface pointer from an object reference or vice versa. This is an advanced feature which should be used only if you really know what you're doing; see Reference Counting. One important use is for associating an object with a callback via A_EventInfo.

address := Object(object)
object := Object(address)

In either case the object's reference-count is automatically incremented so that the object is not freed prematurely. Generally each new copy of the address should be treated as an object reference, except that the script is responsible for calling ObjAddRef and/or ObjRelease as appropriate. For example, instead of x := address, consider using ObjAddRef(x := address) and ObjRelease(x) when finished with x if address may be released in the mean-time. This ensures the object is freed when the last reference to it is lost - and not before then.

Note that this function applies equally to objects not created by Object(), such as COM object wrappers or File objects.

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 the key. In most cases obj.func[obj] does not exist and obj.func's __Call meta-function is invoked instead. This may be used to change the behaviour of function calls in an abstract way, 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) {
    ; Call a list of functions.
    Loop % func.MaxIndex()
        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%
}