Search code examples
.netvb.netlazy-evaluationlazy-initializationdnlib

How to initialize a Lazy list?


I've read about Lazy Initialization but practically I don't understand how to initialize the objects inside.

This function returns a LazyList which is a kind of custom implementation of a Lazy type

Public Function GetMethods(ByVal Assembly As String,
                           ByVal TypeName As String) 'As List(Of MethodDef) ' As MethodDef()

    Dim methods As LazyList(Of MethodDef) = Nothing

    Using ass As ModuleDefMD = ModuleDefMD.Load(Assembly)

        For Each t As TypeDef In ass.GetTypes

            If t.HasMethods AndAlso t.Name.String.Equals(TypeName, StringComparison.OrdinalIgnoreCase) Then

                methods = t.Methods

                '  MsgBox(t.Methods.GetType.Name) ' Result: LazyList'1
                Exit For

            End If

        Next t

        Return methods

    End Using

End Function

When I try to read a property of any item of the LazyList I get a NullReferenceException exception, I suppose that this is because the object is not initialized? 'cause note that the lazylist item count is "2", and I'm totally sure that the property which I try to read can't be null.

Imports dnlib.DotNet
Imports dnlib.DotNet.Emit
Imports dnlib.Utils

    Dim methods As LazyList(Of MethodDef) = GetMethods("C:\WindowsApplication.exe", "Main")

    MsgBox(methods.IsInitialized(0)) ' Result: False
    MsgBox(methods.Count) ' Result: 2

    For Each method As MethodDef In methods

        ' NullReferenceException exception here:
        MsgBox(method.Name)

        ' NullReferenceException exception here too (reading m.hasbody):
        If method.HasBody Then
            Dim sb As New System.Text.StringBuilder
            With sb
                .AppendLine(String.Format("Method Instructions: {0}", Environment.NewLine &
                                          String.Join(Environment.NewLine, method.Body.Instructions)))
            End With
            Debug.WriteLine(sb.ToString)
        End If

    Next method

If I try to reproduce what I do in the code above (try to read the properties) but inside the function then all goes as expected without any nullreference exception... like in this example:

public function GetMethods (...)

' deleted code...

        If t.HasMethods AndAlso t.Name.String.Equals(TypeName, StringComparison.OrdinalIgnoreCase) Then

           methods = t.Methods
            For Each m In methods
                MsgBox(m.Name) ' no exceptions
                MsgBox(m.HasBody) ' no exceptions
            Next

       End If

' deleted code...

end function

PS: The imports are from dnlib library.

UPDATE:

I would like to explain the problem with more and better examples.

Two thinngs before continue explaining:

I ensured that in all examples the TypeName parameter exists and iis found, I've ensured that the returned collection of objects by the function never is empty, it has an Collection.Count of 2 as I've explained at the start of the question, so any of that is the problem.


Well, the next function returns a List of objects, those objects come with a property named HasBody but this property ALWAYS is empty (throwing a NullReference exception) when it should not be empty, the value should be True for the both items contained in the collection.

Public Function GetMethods(ByVal Assembly As String,
                           ByVal TypeName As String) As List(Of MethodDef)

    Dim methods As List(Of MethodDef) = Nothing

    Using ass As ModuleDefMD = ModuleDefMD.Load(Assembly)

        For Each t As TypeDef In ass.GetTypes

            If t.HasMethods AndAlso t.Name.String.Equals(TypeName, StringComparison.OrdinalIgnoreCase) Then

                methods = t.Methods.ToList
                Exit For

            End If

        Next t

        Return methods

    End Using

End Function

By the other hand, If I perform a ridiculous modification in the function (see the changes related to the tmp object) the function returns a list of objects where the HasBody property is initialized, is not empty, and the 2 items contained in the returned list a HasBody property with a value of True.

Public Function GetMethods(ByVal Assembly As String,
                           ByVal TypeName As String) As List(Of MethodDef)

    Dim methods As List(Of MethodDef) = Nothing

    Dim tmp As Object

    Using ass As ModuleDefMD = ModuleDefMD.Load(Assembly)

        For Each t As TypeDef In ass.GetTypes

            If t.HasMethods AndAlso t.Name.String.Equals(TypeName, StringComparison.OrdinalIgnoreCase) Then

                methods = t.Methods.ToList
                For Each m In methods
                    tmp = m.HasBody
                Next

                Exit For

            End If

        Next t

        Return methods

    End Using

End Function

So where is the problem here and how to fix it?, maybe that is a temporal solution for this situation but the methoddef object that returns the function contains a lot of properties and I need to access to more properties than HasBody in the future so I really can't be "assigning" each property to the tmp object in the function to solve that in a ugly way...

and the code used for loop through the returned List in both cases whas this:

Note: remember that, with the first function I can't parse the method.hasbody property and neither the method.body.instructions property, both throw a NullReference exception.

But with the modified function I can parse the method.hasbody property only if I've assigned it to the tmp variable inside the function before returning the List, and the same for the method.body.instructions property if did the same.

    Dim methods As List(Of MethodDef) =
        GetMethods("C:\WindowsApplication.exe", "Main")

    For Each method As MethodDef In methods

        MsgBox(method.Name)

        If method.HasBody Then
            Dim sb As New System.Text.StringBuilder
            With sb
                .AppendLine(String.Format("Method Instructions: {0}", Environment.NewLine &
                                          String.Join(Environment.NewLine, method.Body.Instructions)))
            End With
            MsgBox(sb.ToString)
        End If

    Next method

Solution

  • I think this is the first problem:

    Public Function GetMethods(ByVal Assembly As String,
                           ByVal TypeName As String) 'As List(Of MethodDef) 
    

    Specify the return type and it may work: As LazyList(Of MethodDef)

    This will also happen when the TypeName is not found in the Assembly. In that case, your LazyList will still be Nothing and result in the NRE. You should check the return - I dont think it has to do with the LazyList (yet):

    Dim methods As LazyList(Of MethodDef) = _
         GetMethods("C:\WindowsApplication.exe", "Main")
    
    If methods IsNot Nothing Then
      ' iterate
    

    TestBed:

    Dim methods As LazyList(Of MethodDef) = Nothing
    

    In a button click:

    methods = Test_Handler("Test2")
    
    For Each meth As MethodDef In methods
        If meth.HasBody Then
            Console.WriteLine("{0} has a body", meth.Name)
        Else
            Console.WriteLine("{0} has NO body", meth.Name)
        End If
    Next
    

    Test handler is essentially what you have &= As LazyList(Of MethodDef). Output:

    .ctor has a body
    testfunction has a body
    Foo has a body
    Bar has a body
    ziggy has a body
    

    The bigger problem may come next: ... I don't understand how to initialize the objects inside

    What you stored in the LazyList are these MethodDef objects -- they are already instantiated. TypeDef.Methods actually returns an IList of MethodDef, the only reason a LazyList is involved is because your code stores them that way.

    If you mean to invoke the method(s) they describe, I am pretty sure you cant do it using a MethodDef as a starting point. They are just metadata describing the characteristics of the method. You already know the TypeName they came from so to invoke one of the methods, you'd have to have an instance of that Type.

    Its unknown what you are trying to do, but a LazyList seems unneeded for a collection of metadata.


    The definition in Object Browser for Methods is IList, as the source also shows:

    public ThreadSafe.IList<MethodDef> Methods
    

    Internally, it uses a LazyList. This is probably so it can create and save a list of Methods for a Type but not bother loading the metadata for them unless and until it is actually asked something about the Methods list. Then they are initialized.

    This works equally well:

    Private Function Test_Handler(typeName As String) As List(Of MethodDef)
    
        Dim modDef As ModuleDefMD = ModuleDefMD.Load("C:\Temp\ConsoleApplication1.exe")
        Dim methods As New List(Of MethodDef)
    
        For Each t As TypeDef In modDef.GetTypes
    
            ' stupid way to drill, but will work for demo purposes
            If t.Name.Equals(typeName) Then
    
                'methods = t.Methods
                methods.AddRange(t.Methods.ToArray)
    
                Exit For
            End If
    
        Next
    
        Return methods
    
    End Function
    

    You can get too clever and try to just do: Return t.Methods and get a cast error about LastList cast to List. The set of method objects you get back is already instanced though, so there might be something else messed up if you are getting NREs.


    This solves the problem:

    Using ass As ModuleDefMD = ModuleDefMD.Load(Assembly)
    

    The List of Method defs are internally a LazyList and wont be instanced until a Property or Method on them is called. To instance them, they (apparently) need access to the thing that created them. So reference something innocuous so you can dispose of ModuleDefMD

    Dim methods As New List(Of MethodDef)
    Dim b As Boolean
    
    Using modDef As ModuleDefMD = ModuleDefMD.Load("C:\Temp\ConsoleApplication1.exe")
        For Each t As TypeDef In modDef.GetTypes
    
            ' stupid way to drill, but will work for demo purposes
            If t.Name.Equals(typeName) Then
    
                For Each m As MethodDef In t.Methods
                    b = m.HasBody
    
                    methods.Add(m)
    
                Next
                Exit For
            End If
    
        Next
    End Using
    Return methods
    

    This is more or less what you Tmp object does. You call it ugly, but since the source is a LazyList, it is either that or work out something with the modDef reference.