An Abstract Specification for Visual Basic Late Binding

In an earlier post, I talk about the implementation details of late bound expressions. That discussion considers the interface between the VB compiler and Late Binder but lacks an overall discussion of late binding: what it is, and how it works. What I present here is an abstract specification for VB late binding.

 

To generalize an earlier statement: late binding is all about figuring out which members to use while the program runs. Once the compiler decides that a particular expression requires late bound evaluation, it changes the expression into a helper call into the Late Binder (located in Microsoft.VisualBasic.dll). It is then up to the Late Binder to evaluate the expression during run time. This raises two important questions: Which expressions become late bound? And what does the Late Binder do at run time to evaluate these expressions?

 

Late Bound Expressions

 

Only certain kinds of Visual Basic expressions can become late bound. The Visual Basic Language Specification offers a definition:

 

11.3 Late-Binding Expressions

When the target of a member access expression or index expression is of type Object, the processing of the expression may be deferred until run time. Deferring processing this way is called late binding. Late binding allows Object variables to be used in a typeless way, where all resolution of members is based on the actual run-time type of the value in the variable. If strict semantics are specified by the compilation environment or by Option Strict, late binding causes a compile-time error. Non-public members are ignored when doing late-binding, including for the purposes of overload resolution. Note that, unlike the early-bound case, invoking or accessing a Shared member late bound will cause the invocation target to be evaluated at run time.

Note   If the expression is an invocation expression for a member defined on System.Object, late binding will not take place.

 

source: Microsoftâ Visual Basicâ .NET Language Specification, Version 7.1, p. 153

 

From the spec's definition, when the target of a member access expression or index expression is of type Object, the expression may become late bound. In other words, using dot "." or parentheses "()" on Object causes late binding (with one exception: using "." to access a member defined on System.Object remains early bound). In fact, there are five distinct categories of late bound expressions:

 

Late Bound Category

Purpose

Late Call

calling a method

Late Index Get

fetching a value using an index

Late Get

fetching the value of a member

Late Index Set

setting a value using an index

Late Set

setting the value of a member

 

Each category represents a usage of either " . " or " ( ) " on Object, and each has a distinct evaluation behavior. The earlier post addressed one of these categories, Late Call, but just scratches the surface as the following scenarios demonstrate:

 

    Sub Run(o As Object, ...)

        'Late Call

        Call o.x() 'Scenario 1: Note the Call keyword is optional

        Call o.x(a) 'Scenario 2: arguments may be supplied

 

        'Late Index Get

        v = o(a) 'Scenario 3

 

        'Late Get

        v = o.x 'Scenario 4

        v = o.x(a) 'Scenario 5: arguments may be supplied

 

        'Late Index Set

        o(a) = v 'Scenario 6

 

        'Late Set

        o.x = v 'Scenario 7

        o.x(a) = v 'Scenario 8: arguments may be supplied

    End Sub

 

Evaluation of Late Bound Expressions

 

Now that we know what kinds of expressions become late bound, we can discuss how they get evaluated at run time. The evaluation of any expression is a two step process: first decide what operation to do and then perform the operation. In the early binding model, the VB compiler makes the decisions and .NET performs the IL-encoded operations. In the late binding model both steps occur at run time, with the Late Binder making the decisions and .NET Reflection performing the operations. The decision is made by applying VB language rules to the expression. These rules depend on the category of the late bound expression and include member lookup, method overload resolution, shadowing and overriding semantics, etc. Once the rules have been applied and the decision made, the resulting operation is performed using Reflection. As currently designed, there are five types of Reflection operations used by the Late Binder:

 

System.Reflection.MethodInfo.Invoke -- perform a function call

System.Array.GetValue -- fetch a value from an array

System.Array.SetValue -- store a value into an array

System.Reflection.FieldInfo.GetValue -- fetch the value of a field

System.Reflection.FieldInfo.SetValue -- store a value into a field

 

All late bound expressions eventually turn into a sequence of one or more of these Reflection operations. For example, consider a Late Get expression accessing a member "Position":

 

    Sub Main()

        Dim o As Object = ...

        Console.WriteLine(o.Position())

    End Sub

 

Because the expression belongs to the Late Get category, the Late Binder knows that "Position" must refer to a method, a property or a field. Say an object of type Knight is contained in o. There are at least three declarations of the type Knight that could make the expression o.Position() valid:

 

    'First possibility

    Class Knight

        Public Position As Integer

    End Class

 

    'Second possibility

    Class Knight

        Public Function Position() As Integer

        End Function

    End Class

 

    'Third possibility

    Class Knight

        Public ReadOnly Property Position() As Integer

            Get

            End Get

        End Property

    End Class

 

Also, Position cannot refer to a nested type or a method with parameters (since no arguments have been supplied at the call site). These declarations of type Knight would make the expression o.Position() not valid:

 

    'Fourth possibility

    Class Knight

        Structure Position

            Public Value As Integer

        End Structure

    End Class

 

    'Fifth possibility

    Class Knight

        Public Sub Position(ByRef Value As Integer)

        End Sub

    End Class

 

Upon deciding that Position is a valid member for a Late Get expression, the Late Binder invokes the appropriate Reflection operation. If Position refers to a field, then FieldInfo.GetValue will be called. If it refers to a method, MethodInfo.Invoke will be called instead. If it is not valid, as would be the case for a nested type, an exception is thrown.

 

An Abstract Description of Late Bound Expression Evaluation

 

It's useful to consider that each late bound category is implemented as a unique helper function in the Late Binder. From here, I will explain the rules and operations used by each of these helper functions. Included below are several tables, one per late bound category. In the left column are the VB late binding scenarios indicated above. In the right column are the operations that result after applying the language rules. This column can also include late binding scenarios. Their inclusion means that the resulting operations are identical to that of the scenario indicated.

 

The tables make use of the following definitions:

 

      o is an expression typed as Object.

      x is an identifier

      a is an argument list

      v is an expression, either l-value or r-value

      d is a default property

 

Also, I will use the following shorthand to represent the Reflection operations:

 

      INVOKE o.x(a)

            call MethodInfo.Invoke(o, a) using the MethodInfo object for method x.

      ARRAYGET o[a]

            call CType(o, System.Array).GetValue(a)

      ARRAYSET o[a] = v

            call CType(o, System.Array).SetValue(v, a)

      FIELDGET o.x

            call FieldInfo.GetValue(o) using the FieldInfo for field x.

      FIELDSET o.x = v

            call FieldInfo.SetValue(o, v) using the FieldInfo for field x.

 

Finally, I give a very rough pseudo-code implementation of each helper function. The pseudo-code makes use of a function called PerformMemberResolution(). Consider this the member lookup and resolution process that applies shadowing, overloading, overriding and other various language semantics as described in-depth in the Visual Basic Language Specification. It is dependent upon the instance o, the member name "x" and the supplied arguments a (if any).

 

Late Call

 

A simple method call.

 

Late Bound Scenario

Possible Operations

Comments

Call o.x() 'Scenario 1

INVOKE o.x()

when x is a method

 

INVOKE o.get_x()

when x is a property

Call o.x(a) 'Scenario 2

INVOKE o.x(a)

when x is a method

 

INVOKE o.get_x(a)

when x is a property

 

If x is a sub or function then invoke x. If x is a property, invoke the get_x accessor function. The call may or may not have arguments.

 

Helper function pseudo-code:

 

    Function LateCall(Instance|o, MemberName|"x", Arguments|a)

 

        Dim Result As Reflection.MemberInfo = _

            PerformMemberResolution(Instance, MemberName, Arguments)

 

        If IsMethod(Result) Then

            Return CType(Result, MethodInfo).Invoke(Instance, Arguments)

 

        ElseIf IsProperty(Result) Then

            Return CType(Result, PropertyInfo).GetGetMethod.Invoke(Instance, Arguments)

 

        End If

 

        Throw New Exception("Unable to resolve late call expression")

    End Function

 

Late Index Get

 

Fetching the value from an Object like an array.

 

VB Scenario

Possible Operations

Comments

v = o(a) 'Scenario 3

ARRAYGET o[a]

where o is an array

 

INVOKE o.get_d(a)

where o is a type with a default property

 

This scenario indexes into o to load a value. This scenario works if the object in o is either an array or a type with a default property. If o is an array, then use the supplied indices a to load the value. If o is a type with a default property d, invoke the get_d accessor function.

 

Note: This scenario requires arguments. If no arguments are supplied, this would be the simple assignment v = o (not a late bound expression).

 

Helper function pseudo-code:

 

    Function LateIndexGet(Instance|o, Arguments|a)

 

        If IsArray(Instance) Then

            Return CType(Instance, System.Array).GetValue(Arguments)

 

        ElseIf HasDefaultProperty(Instance) Then

            GetDefaultProperty(Instance).GetGetMethod.Invoke(Instance, Arguments)

 

        End If

 

        Throw New Exception("Unable to resolve late index get expression")

    End Function

 

Late Get

 

Doing member access on an Object to fetch a value.

 

VB Scenario

Possible Operations

Comments

v = o.x 'Scenario 4

FIELDGET o.x

where x is a field

 

call o.x() 'Scenario 1

 

v = o.x(a) 'Scenario 5

w(a) 'Scenario 3

w := FIELDGET o.x

 

w(a) 'Scenario 3

w := o.x 'Scenario 4

 

call o.x(a) 'Scenario 2

 

 

This looks like Late Call but the requirement for a resulting value changes the kinds of members to which "x" can refer. Note that this is also different from Late Index Get since we're using the member access operator " . " instead of the indexing operator " ( ) ".

 

With no arguments supplied, x can be either a field or a method. When x is a method, the resulting operation is the Late Call expression call o.x() (look up Scenario 1 in the Late Call table to see the possible operations).

 

With arguments supplied, x can be a method but also a field under special circumstances. When x is a field, fetch the value of field x and then use that in a new Late Index Get expression w(a) . In other words, do a Late Index Get into the result of a Late Get. Similarly, if x is a method with no parameters, do a Late Call on that method to get the result w and then use it in Late Index Get w(a) . Here's an example with the two steps explicitly expressed:

                                                                                                                                                    

    'First possibility

    Class Knight

        Public Moves As Integer()

    End Class

 

    'Second possibility

    Class Knight

        Public Function Moves() As Integer()

        End Function

    End Class

 

    Sub Main()

        Dim o As Object = New Knight

        Dim w As Object = o.Moves 'First late bound expression

        Dim v As Integer = w(10) 'Second late bound expression where w contains an array

    End Sub

 

Lastly, if x is a method that takes arguments, then treat it as a Late Call.

 

Helper function pseudo-code:

 

    Function LateGet(Instance|o, MemberName|"x", Arguments|a)

 

        Dim Result As Reflection.MemberInfo = _

            PerformMemberResolution(Instance, MemberName, Arguments)

 

        If Arguments.Count = 0 Then

 

            If IsField(Result) Then

                Return CType(Result, FieldInfo).GetValue(Instance)

 

            ElseIf IsMethod(Result) Then

                Return LateCall(Instance, MemberName, NoArguments)

 

            End If

 

        Else

            If IsField(Result) Then

                Dim w As Object = CType(Result, FieldInfo).GetValue(Instance)

                Return LateIndexGet(w, Arguments)

 

            ElseIf IsParameterlessMethod(Result) Then

                Dim w As Object = CType(Result, MethodInfo).Invoke(Instance, NoArguments)

                Return LateIndexGet(w, Arguments)

 

            ElseIf IsMethod(Result) Then

                Return LateCall(Instance, MemberName, Arguments)

 

            End If

        End If

 

        Throw New Exception("Unable to resolve late get expression")

    End Function

 

Late Index Set

 

Storing a value into an Object like an array.

 

VB Scenario

Possible Operations

Comments

o(a) = v 'Scenario 6

ARRAYSET o[a] = v

where o is an array

 

INVOKE o.set_d(v)

where o is a type with a default property

 

This scenario indexes into o to store a value. This scenario works if the object in o is either an array or a type with a default property. If o is an array, then use the supplied indices a to store the value. If o is a type with a default property d, invoke the set_d accessor function.

 

Note: This scenario requires arguments. If no arguments are supplied, this would be the simple assignment o = v (not a late bound expression).

 

Helper function pseudo-code:

 

    Sub LateIndexSet(Instance|o, Arguments|a, Value|v)

 

        If IsArray(Instance) Then

            Instance.SetValue(Value, Arguments)

            Return

 

        ElseIf HasDefaultProperty(Instance) Then

            GetDefaultProperty(Instance).GetSetMethod.Invoke(Instance, Arguments + Value)

            Return

 

        End If

 

        Throw New Exception("Unable to resolve late index set expression")

    End Sub

 

Late Set

 

Doing member access on an Object to set a value.

 

VB Scenario

Possible Operations

Comments

o.x = v 'Scenario 7

FIELDSET o.x = v

where x is a field

 

INVOKE o.set_x(v)

 

o.x(a) = v 'Scenario 8

w(a) 'Scenario 3

w := FIELDGET o.x

 

INVOKE o.set_x(a, v)

 

 

w(a) = v 'Scenario 3

w := o.x 'Scenario 4

 

Again, the existence of arguments changes the situation. The member x can either be a field, method, or property. If x is a field and no arguments are supplied, perform a field set. If arguments were supplied, fetch the value w in field x and then do a Late Index Set w(a) .

 

If x is a property, this is a simple invocation of the set_x accessor. If x is a parameterless method, then invoke x to get the resulting value w and perform a Late Index Set w(a) .

 

Helper function pseudo-code:

 

    Sub LateSet(Instance|o, MemberName|"x", Arguments|a, Value|v)

 

        Dim Result As Reflection.MemberInfo = _

            PerformMemberResolution(Instance, MemberName, Arguments)

 

        If Arguments.Count = 0 Then

 

            If IsField(Result) Then

                CType(Result, FieldInfo).SetValue(Instance, Value)

                Return

 

            ElseIf IsProperty(Result) Then

                CType(Result, PropertyInfo).GetSetMethod.Invoke(Instance, Value)

                Return

 

            End If

 

        Else

            If IsField(Result) Then

                Dim w As Object = CType(Result, FieldInfo).GetValue(Instance)

                LateIndexSet(w, Arguments, Value)

                Return

 

            ElseIf IsProperty(Result) Then

                CType(Result, PropertyInfo).GetSetMethod.Invoke(Instance, Arguments + Value)

                Return

 

            ElseIf IsParameterlessMethod(Result) Then

                Dim w As Object = CType(Result, MethodInfo).Invoke(Instance, NoArguments)

                LateIndexSet(w, Arguments, Value)

                Return

 

            End If

        End If

 

        Throw New Exception("Unable to resolve late set expression")

    End Sub

 

In Closing

 

We've seen that only some expressions become late bound, and that these expressions are grouped into five major categories. Each category uses a unique set of VB language rules to decide the expression's validity, and that after this decision process is made, a series of Reflection operations are used to perform the expression. Each category can be thought of as a helper function with specific behavior, and these behaviors have been described as a set of scenarios and resulting operations.

 

A final note: the Visual Basic runtime contains a function CallByName. The CallByName function performs a late bound evaluation on the given object, member name, and arguments. There is also an enum parameter UseCallType of type Microsoft.VisualBasic.CallType. Essentially, this enum parameter allows the programmer to choose the late binding category. CallType.Get, CallType.Set, and CallType.Method correspond to Late Get, Late Set, and Late Call respectively (a fourth value, CallType.Let, is used only for COM interop). In fact, the CallByName function is simply a Select Case statement that uses the CallType argument to choose the appropriate late binding helper function.