// NOTE this version is now out of date.

// UPDATED VERSION IS AVAILABLE in the MDBG DISTRIBUTION.

// See http://blogs.msdn.com/jmstall/archive/2005/11/08/mdbg_linkfest.aspx

 
// Sample to load PDB and dump as XML.
// Author: Mike Stall  (http://blogs.msdn.com/jmstall)
// UPDATED on 1/26/05, uses PDB wrappers from MDBG sample.
// must include reference to Mdbg (such as MdbgCore.dll)
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;
using System.Diagnostics;

using System.Runtime.InteropServices;

// For symbol store
using System.Diagnostics.SymbolStore;
using Microsoft.Samples.Debugging.CorSymbolStore;


namespace XmlPdbReader
{
    // Random utility methods.
    static class Util
    {
        // Format a token to a string. Tokens are in hex.
        public static string AsToken(int i)
        {
            return String.Format(System.Globalization.CultureInfo.InvariantCulture, "0x{0:x}", i);
        }

        // Since we're spewing this to XML, spew as a decimal number.
        public static string AsIlOffset(int i)
        {
            return i.ToString();
        }
    }

    /// <summary>
    /// Class to write out XML for a PDB.
    /// </summary>
    class Pdb2XmlConverter
    {
        /// <summary>
        /// Initialize the Pdb to Xml converter. Actual conversion happens in ReadPdbAndWriteToXml.
        /// Passing a filename also makes it easy for us to use reflection to get some information 
        /// (such as enumeration)
        /// </summary>
        /// <param name="writer">XmlWriter to spew to.</param>
        /// <param name="fileName">Filename for exe/dll. This class will find the pdb to match.</param>
        public Pdb2XmlConverter(XmlWriter writer, string fileName)
        {
            m_writer = writer;
            m_fileName = fileName;
        }

        // The filename that the pdb is for.
        string m_fileName;
        XmlWriter m_writer;

        // Keep assembly so we can query metadata on it.
        System.Reflection.Assembly m_assembly;

        // Maps files to ids. 
        Dictionary<string, int> m_fileMapping = new Dictionary<string, int>();

        /// <summary>
        /// Load the PDB given the parameters at the ctor and spew it out to the XmlWriter specified
        /// at the ctor.
        /// </summary>
        public void ReadPdbAndWriteToXml()
        {
            // Actually load the files
            ISymbolReader reader = SymUtil.GetSymbolReaderForFile(m_fileName, null);
            m_assembly = System.Reflection.Assembly.ReflectionOnlyLoadFrom(m_fileName);

            // Begin writing XML.
            m_writer.WriteStartDocument();
            m_writer.WriteComment("This is an XML file representing the PDB for '" + m_fileName + "'");
            m_writer.WriteStartElement("symbols");


            // Record what input file these symbols are for.
            m_writer.WriteAttributeString("file", m_fileName);

            WriteDocList(reader);
            WriteEntryPoint(reader);
            WriteAllMethods(reader);

            m_writer.WriteEndElement(); // "Symbols";
        }

        // Dump all of the methods in the given ISymbolReader to the XmlWriter provided in the ctor.
        void WriteAllMethods(ISymbolReader reader)
        {
            m_writer.WriteComment("This is a list of all methods in the assembly that matches this PDB.");
            m_writer.WriteComment("For each method, we provide the sequence tables that map from IL offsets back to source.");

            m_writer.WriteStartElement("methods");

            // Use reflection to enumerate all methods            
            foreach (Type t in m_assembly.GetTypes())
            {
                foreach (MethodInfo methodReflection in t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly))
                {
                    int token = methodReflection.MetadataToken;
                    ISymbolMethod methodSymbol = null;
                    
                    
                    m_writer.WriteStartElement("method");
                    {
                        m_writer.WriteAttributeString("name", t.FullName + "." + methodReflection.Name);
                        m_writer.WriteAttributeString("token", Util.AsToken(token));
                        try
                        {
                            methodSymbol = reader.GetMethod(new SymbolToken(token));                            
                            WriteSequencePoints(methodSymbol);
                            WriteLocals(methodSymbol);
                        }
                        catch (COMException e)
                        {
                            m_writer.WriteComment("No symbol info");
                        }
                    }
                    m_writer.WriteEndElement(); // method                    
                }
            }
            m_writer.WriteEndElement();
        }

        // Write all the locals in the given method out to an XML file.
        // Since the symbol store represents the locals in a recursive scope structure, we need to walk a tree.
        // Although the locals are technically a hierarchy (based off nested scopes), it's easiest for clients
        // if we present them as a linear list. We will provide the range for each local's scope so that somebody
        // could reconstruct an approximation of the scope tree. The reconstruction may not be exact.
        // (Note this would still break down if you had an empty scope nested in another scope.
        void WriteLocals(ISymbolMethod method)
        {
            m_writer.WriteStartElement("locals");
            {
                // If there are no locals, then this element will just be empty.
                WriteLocalsHelper(method.RootScope);
            }
            m_writer.WriteEndElement();
        }

        // Helper method to write the local variables in the given scope.
        // Scopes match an IL range, and also have child scopes.
        void WriteLocalsHelper(ISymbolScope scope)
        {
            foreach (ISymbolVariable l in scope.GetLocals())
            {
                m_writer.WriteStartElement("local");
                {
                    m_writer.WriteAttributeString("name", l.Name);

                    // Each local maps to a unique "IL Index" or "slot" number.
                    // This index is what you pass to ICorDebugILFrame::GetLocalVariable() to get
                    // a specific local variable. 
                    Debug.Assert(l.AddressKind == SymAddressKind.ILOffset);
                    int slot = l.AddressField1;
                    m_writer.WriteAttributeString("il_index", slot.ToString());

                    // Provide scope range
                    m_writer.WriteAttributeString("il_start", Util.AsIlOffset(scope.StartOffset));
                    m_writer.WriteAttributeString("il_end", Util.AsIlOffset(scope.EndOffset));
                }
                m_writer.WriteEndElement(); // local
            }

            foreach (ISymbolScope childScope in scope.GetChildren())
            {
                WriteLocalsHelper(childScope);
            }
        }

        // Write the sequence points for the given method
        // Sequence points are the map between IL offsets and source lines.
        // A single method could span multiple files (use C#'s #line directive to see for yourself).        
        void WriteSequencePoints(ISymbolMethod method)
        {
            m_writer.WriteStartElement("sequencepoints");

            int count = method.SequencePointCount;
            m_writer.WriteAttributeString("total", count.ToString());

            // Get the sequence points from the symbol store. 
            // We could cache these arrays and reuse them.
            int[] offsets = new int[count];
            ISymbolDocument[] docs = new ISymbolDocument[count];
            int[] startColumn = new int[count];
            int[] endColumn = new int[count];
            int[] startRow = new int[count];
            int[] endRow = new int[count];
            method.GetSequencePoints(offsets, docs, startRow, startColumn, endRow, endColumn);

            // Write out sequence points
            for (int i = 0; i < count; i++)
            {
                m_writer.WriteStartElement("entry");
                m_writer.WriteAttributeString("il_offset", Util.AsIlOffset(offsets[i]));

                // If it's a special 0xFeeFee sequence point (eg, "hidden"), 
                // place an attribute on it to make it very easy for tools to recognize.
                // See http://blogs.msdn.com/jmstall/archive/2005/06/19/FeeFee_SequencePoints.aspx
                if (startRow[i] == 0xFeeFee)
                {
                    m_writer.WriteAttributeString("hidden", XmlConvert.ToString(true));
                }
                else
                {
                    m_writer.WriteAttributeString("start_row", startRow[i].ToString());
                    m_writer.WriteAttributeString("start_column", startColumn[i].ToString());
                    m_writer.WriteAttributeString("end_row", endRow[i].ToString());
                    m_writer.WriteAttributeString("end_column", endColumn[i].ToString());
                    m_writer.WriteAttributeString("file_ref", this.m_fileMapping[docs[i].URL].ToString());
                }
                m_writer.WriteEndElement();
            }

            m_writer.WriteEndElement(); // sequencepoints
        }

        // Write all docs, and add to the m_fileMapping list.
        // Other references to docs will then just refer to this list.
        void WriteDocList(ISymbolReader reader)
        {
            m_writer.WriteComment("This is a list of all source files referred by the PDB.");

            int id = 0;
            // Write doc list
            m_writer.WriteStartElement("files");
            {
                ISymbolDocument[] docs = reader.GetDocuments();
                foreach (ISymbolDocument doc in docs)
                {
                    string url = doc.URL;

                    // Symbol store may give out duplicate documents. We'll fold them here
                    if (m_fileMapping.ContainsKey(url))
                    {
                        m_writer.WriteComment("There is a duplicate entry for: " + url);
                        continue;
                    }
                    id++;
                    m_fileMapping.Add(doc.URL, id);

                    m_writer.WriteStartElement("file");
                    {
                        m_writer.WriteAttributeString("id", id.ToString());
                        m_writer.WriteAttributeString("name", doc.URL);
                    }
                    m_writer.WriteEndElement(); // file
                }
            }
            m_writer.WriteEndElement(); // files
        }

        // Write out a reference to the entry point method (if one exists)
        void WriteEntryPoint(ISymbolReader reader)
        {
            try
            {
                // If there is no entry point token (such as in a dll), this will throw.
                SymbolToken token = reader.UserEntryPoint;
                ISymbolMethod m = reader.GetMethod(token);

                Debug.Assert(m != null); // would have thrown by now.

                // Should not throw past this point
                m_writer.WriteComment(
                    "This is the token for the 'entry point' method, which is the method that will be called when the assembly is loaded." +
                    " This usually corresponds to 'Main'");

                m_writer.WriteStartElement("EntryPoint");
                WriteMethod(m);
                m_writer.WriteEndElement();
            }
            catch (System.Runtime.InteropServices.COMException)
            {
                // If the Symbol APIs fail when looking for an entry point token, there is no entry point.
                m_writer.WriteComment(
                    "There is no entry point token such as a 'Main' method. This module is probably a '.dll'");
            }
        }

        // Write out XML snippet to refer to the given method.
        void WriteMethod(ISymbolMethod method)
        {
            m_writer.WriteElementString("methodref", Util.AsToken(method.Token.GetToken()));
        }
    }

    // Harness to drive PDB to XML rea
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Test harness for PDB2XML.");

            if (args.Length < 2)
            {
                Console.WriteLine("Usage: Pdb2Xml <input managed exe> <output xml>");
                Console.WriteLine("This will load the pdb for the managed exe, and then spew the pdb contents to an XML file.");
                return;
            }
            string stInputExe = args[0];
            string stOutputXml = args[1];

            // Get a Text Writer to spew the PDB to.
            XmlDocument doc = new XmlDocument();
            XmlWriter xw = doc.CreateNavigator().AppendChild();

            // Do the pdb for the exe into an xml stream. 
            Pdb2XmlConverter p = new Pdb2XmlConverter(xw, stInputExe);
            p.ReadPdbAndWriteToXml();
            xw.Close();

            // Print the XML we just generated and save it to a file for more convenient viewing.
            Console.WriteLine(doc.OuterXml);

            doc.Save(stOutputXml);
            {
                // Proove that it's valid XML by reading it back in...
                XmlDocument d2 = new XmlDocument();
                d2.Load(stOutputXml);
            }

            // Now demonstrate some different queries.
            XmlNode root = doc.DocumentElement;

            DoQuery(QueryForStartRow("Foo.Main", 3), root);
            DoQuery(QueryForEntryName(), root);
            DoQuery(QueryForAllLocalsInMethod("Foo.Main"), root);
        } // end Main

        #region Sample Queries
        // Some sample queries
        static string QueryForEntryName()
        {
            return @"/symbols/methods/method[@token=/symbols/EntryPoint/methodref]/@name";
        }
        static string QueryForAllLocalsInMethod(string stMethod)
        {
            return "/symbols/methods/method[@name=\"" + stMethod + "\"]/locals/local/@name";
        }
        static string QueryForStartRow(string stMethod, int exactILOffset)
        {
            return "/symbols/methods/method[@name=\"" + stMethod + "\"]/sequencepoints/entry[@il_offset=\"" + exactILOffset + "\"]/@start_row";
        }

#if false            
            // Here are more sample queries:
            // Get all locals that are active at a given line?
            @"/symbols/methods/method/locals/local[@il_start<=""2"" and @il_end>=""2""]/@name";

            @"/symbols/files/file/@name"; // get all filenames referenced from PDB.

            // ** All methods that have code in a given filename.            
            @"/symbols/methods/method[sequencepoints/entry/@file_ref=/symbols/files/file[@name=""c:\temp\t.cs""]/@id]/@name";
                        
            @"/symbols/files/file[@name=""c:\temp\t.cs""]/@id"; // File ID for a given filename:
            
            @"/symbols/EntryPoint/methodref"; // entry point token.
            @"/symbols/methods/method/@name";  // ** select all names of all methods
            @"/symbols/methods/method[@token=""0x6000001""]";  // entire XML snippet for method with specified token value
            @"/symbols/methods/method[@token=""0x6000001""]/@name"; // ** just the name of method with the given token value
            @"/symbols/methods/method[@token=/symbols/EntryPoint/methodref]/@name";   // *** method name of the entry point token!!
            @"/symbols/methods/method[@name=""Foo.Main""]/locals/local/@name"; // name of all locals in method Foo.Main
            @"/symbols/methods/method/sequencepoints/entry[@il_offset=""0x12""]"; // sp entry for IL offset 12 (in all methods)
            @"/symbols/methods/method[@name=""Foo.Main""]/sequencepoints/entry/@start_row"; // get all source rows for method Foo.Main
            @"/symbols/methods/method[@name=""Foo.Main""]/sequencepoints/entry[@il_offset=""0x4""]/@start_row"; // get start row for for IL offset 4 in method Foo.Main

            // Lookup method name + IL offset given source file + line 
            // Queries only return 1 result, so there's not a good way to get the (name, IL offset) pair back with a single query.
            @"/symbols/methods/method/sequencepoints/entry[@start_row<=""26"" and @end_row>=""26"" and @file_ref=/symbols/files/file[@name=""c:\temp\t.cs""]/@id]/@il_offset";
            @"/symbols/methods/method[sequencepoints/entry[@start_row<=""26"" and @end_row>=""26"" and @file_ref=/symbols/files/file[@name=""c:\temp\t.cs""]/@id]]/@name";
#endif

        #endregion


        // Helper to execute a query and print out to console.
        static void DoQuery(string stQuery, XmlNode root)
        {
            Console.WriteLine("Query:{0}", stQuery);

            XmlNodeList nodeList = root.SelectNodes(stQuery);
            Console.WriteLine("Found {0} item(s) in query.", nodeList.Count);
            Console.WriteLine("(outer)-----------");
            foreach (XmlNode x in nodeList)
            {
                Console.WriteLine(x.OuterXml);
            }
            Console.WriteLine("(inner)-----------");
            foreach (XmlNode x in nodeList)
            {
                Console.WriteLine(x.InnerText);
            }
            Console.WriteLine("------------------");
        }
    }


    #region Get a symbol reader for the given module
    // Encapsulate a set of helper classes to get a symbol reader from a file.
    // The symbol interfaces require an unmanaged metadata interface.
    static class SymUtil
    {
        static class NativeMethods
        {
            [DllImport("ole32.dll")]
            public static extern int CoCreateInstance([In] ref Guid rclsid,
                                                       [In, MarshalAs(UnmanagedType.IUnknown)] Object pUnkOuter,
                                                       [In] uint dwClsContext,
                                                       [In] ref Guid riid,
                                                       [Out, MarshalAs(UnmanagedType.Interface)] out Object ppv);
        }

        // Wrapper.
        public static ISymbolReader GetSymbolReaderForFile(string pathModule, string searchPath)
        {
            return SymUtil.GetSymbolReaderForFile(new SymbolBinder(), pathModule, searchPath);
        }

        // We demand Unmanaged code permissions because we're reading from the file system and calling out to the Symbol Reader
        // @TODO - make this more specific.
        [System.Security.Permissions.SecurityPermission(
            System.Security.Permissions.SecurityAction.Demand,
            Flags = System.Security.Permissions.SecurityPermissionFlag.UnmanagedCode)]
        public static ISymbolReader GetSymbolReaderForFile(SymbolBinder binder, string pathModule, string searchPath)
        {
            // Guids for imported metadata interfaces.
            Guid dispenserClassID = new Guid(0xe5cb7a31, 0x7512, 0x11d2, 0x89, 0xce, 0x00, 0x80, 0xc7, 0x92, 0xe5, 0xd8); // CLSID_CorMetaDataDispenser
            Guid dispenserIID = new Guid(0x809c652e, 0x7396, 0x11d2, 0x97, 0x71, 0x00, 0xa0, 0xc9, 0xb4, 0xd5, 0x0c); // IID_IMetaDataDispenser
            Guid importerIID = new Guid(0x7dac8207, 0xd3ae, 0x4c75, 0x9b, 0x67, 0x92, 0x80, 0x1a, 0x49, 0x7d, 0x44); // IID_IMetaDataImport

            // First create the Metadata dispenser.
            object objDispenser;
            NativeMethods.CoCreateInstance(ref dispenserClassID, null, 1, ref dispenserIID, out objDispenser);

            // Now open an Importer on the given filename. We'll end up passing this importer straight
            // through to the Binder.
            object objImporter;
            IMetaDataDispenser dispenser = (IMetaDataDispenser)objDispenser;
            dispenser.OpenScope(pathModule, 0, ref importerIID, out objImporter);

            IntPtr importerPtr = IntPtr.Zero;
            ISymbolReader reader;
            try
            {
                // This will manually AddRef the underlying object, so we need to be very careful to Release it.
                importerPtr = Marshal.GetComInterfaceForObject(objImporter, typeof(IMetadataImport));

                reader = binder.GetReader(importerPtr, pathModule, searchPath);
            }
            finally
            {
                if (importerPtr != IntPtr.Zero)
                {
                    Marshal.Release(importerPtr);
                }
            }
            return reader;
        }
    }
    #region Metadata Imports

    // We can use reflection-only load context to use reflection to query for metadata information rather
    // than painfully import the com-classic metadata interfaces.
    [Guid("809c652e-7396-11d2-9771-00a0c9b4d50c"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComVisible(true)]
    interface IMetaDataDispenser
    {
        // We need to be able to call OpenScope, which is the 2nd vtable slot.
        // Thus we need this one placeholder here to occupy the first slot..
        void DefineScope_Placeholder();

        //STDMETHOD(OpenScope)(                   // Return code.
        //LPCWSTR     szScope,                // [in] The scope to open.
        //  DWORD       dwOpenFlags,            // [in] Open mode flags.
        //  REFIID      riid,                   // [in] The interface desired.
        //  IUnknown    **ppIUnk) PURE;         // [out] Return interface on success.
        void OpenScope([In, MarshalAs(UnmanagedType.LPWStr)] String szScope, [In] Int32 dwOpenFlags, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.IUnknown)] out Object punk);

        // Don't need any other methods.
    }

    // Since we're just blindly passing this interface through managed code to the Symbinder, we don't care about actually
    // importing the specific methods.
    // This needs to be public so that we can call Marshal.GetComInterfaceForObject() on it to get the
    // underlying metadata pointer.
    [Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComVisible(true)]
    [CLSCompliant(true)]
    public interface IMetadataImport
    {
        // Just need a single placeholder method so that it doesn't complain about an empty interface.
        void Placeholder();
    }
    #endregion

    #endregion Get a symbol reader for the given module

} // XmlPdbReader