Web Log di Adrian Florea

"You know you've achieved perfection in design, not when you have nothing more to add, but when you have nothing more to take away." Antoine de Saint-Exupery
posts - 440, comments - 2715, trackbacks - 3944

My Links

Archives

Post Categories

Image Galleries

.RO Blogs

.RO People

.RO Sites

Blogs

Furls

Links

vinCitori

GetCallingLanguage [la soluzione di un quiz di Claudio Brotto]

Un po' di mesi fa, Claudio Brotto, aveva proposto un simpatico quiz a cui vorrei dare adesso una soluzione (in realta' la soluzione e' per una variante leggermente modificata del quiz). Si tratta praticamente di scrivere un metodo pubblico in un assembly in tal modo che, chiamandolo da un altro assembly, questo metodo produca una stringa diversa in base al linguaggio in cui e' stato scritto l'assembly chiamante. Per esempio, questo metodo dovrebbe ritornare la stringa "C#" se chiamato da un assembly scritto in C#, oppure "Basic" se chiamato da un assembly scritto in VB.NET, etc. Piccolo vincolo della mia soluzione: l'assembly chiamante deve essere compilato in debug mode.

Tutto si basa su un metadato che il compilatore inserisce nell'assembly quando compiliamo in debug mode e che serve ai vari debugger. Cercate nel file "foo.il" ottenuto da un “foo.cs“ qualunque:

csc /debug+ foo.cs
ildasm foo.exe /linenum /out:foo.il/

la direttiva ".language" (vedi C.3, Partition VI, ECMA-335) e troverete 3 GUID che identificano rispettivamente il linguaggio, il vendor (cioe' il compilatore) e il documento. Solo il primo, cioe' il GUID del linguaggio, e' obbligatorio nella direttiva ".language". Per vedere l'elenco dei linguaggi mappati da questi GUID, andate nel file "C:\Program Files\Microsoft.NET\SDK\v2.0\include\corsym.h".

Come possiamo pero' ottenere dal codice, questa informazione? Dopo un po' di giri col Reflector, ho trovato la classe giusta, System.Diagnostics.SymbolStore.SymDocument, nell'assembly "ISymWrapper.dll" (nella cartella del framework). In questa classe notiamo la proprieta' Language di tipo Guid che ci dara' il GUID del linguaggio, incontrato sopra nel file ".il". Il costruttore di questa classe, nonostante pubblico, non ci aiuta tanto, visto il pointer bruttino che non possiamo passarglielo come argomento... Ma sempre coll'aiuto del Reflector, scopriamo chi utilizza questa interessante classe: il metodo GetDocuments della classe System.Diagnostics.SymbolStore.SymReader. E qui la stessa storia per il suo costruttore pubblico, ma indagando che altra classe utilizza la SymReader, arriviamo alla System.Diagnostics.SymbolStore.SymBinder, che finalmente ha un costruttore pubblico senza parametri!

Bene, in questa classe SymBinder, andiamo a vedere un po' il metodo GetReader con il quale potremo ottenere un'istanza di SymReader, e da qui ottenere un'istanza di SymDocument, per arrivare poi al GUID del linguaggio.

In questo metodo GetReader, incontriamo un parametro chiamato "importer", di tipo IntPtr, che punta (dice la documentazione) sulla "metadata import interface". Questa interfaccia, IMetadataImport, si trova, insieme alle altre interfacce unmanaged per i metadati, documentata nel MSDN a questa pagina probabilmente poco visitata. Il giro non e' finito perche', per ottenere un'istanza importer di IMetadataImport, abbiamo bisogno di un "dispenser" a cui chiedere di aprire il file dell'assembly (tramite il metodo OpenState) e trovare l'istanza dell'importer in un parametro out. L'istanza del "dispenser" la otteniamo conoscendo il CLSID_CorMetaDataDispenser (vedi il file "cor.h" - l'header generale del runtime se avete downloadato il SSCLI (Rotor)).

Dobbiamo definirci queste due interfacce unmanaged, decorandole con i loro GUID presi sempre dal file "cor.h", ma senza dover definire necessariamente i metodi che non ci interessano - per questo motivo ho messo dei metodi Dummy, semplicemente per occupare lo slot di un metodo che non ci interessa e per poter cosi' arrivare correttamente al metodo OpenState che e' il secondo metodo dell'interfaccia.

Quello che ho detto sopra, viene implementato cosi' (sicuramente non e' perfetto come codice ma serve solo per illustrare il concetto):

// foo.cs
// csc /t:library /r:ISymWrapper.dll foo.cs
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Diagnostics.SymbolStore;
using System.Diagnostics;

[
    Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    ComVisible(true)
]
public interface IMetadataImport
{
    void Dummy();
}

[
    Guid("809c652e-7396-11d2-9771-00a0c9b4d50c"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    ComVisible(true)
]
interface IMetaDataDispenser
{
    void Dummy(); // DefineScope
    // https://msdn2.microsoft.com/en-us/library/ms231248.aspx
    void OpenScope
        (
            [In, MarshalAs(UnmanagedType.LPWStr)] string szScope,
            [In] int dwOpenFlags,
            [In] ref Guid riid,
            [Out, MarshalAs(UnmanagedType.IUnknown)] out object ppIUnk
        );
}

public class Metadata
{
    public static string GetCallingLanguage()
    {
        Assembly assembly = Assembly.GetCallingAssembly();
        foreach (DebuggableAttribute attribute in assembly.GetCustomAttributes(typeof(DebuggableAttribute), false))
        {
            if (!attribute.IsJITTrackingEnabled)
            {
                throw new NotSupportedException("Calling assembly not built in Debug mode");
            }
        }
        string location = assembly.Location;

        Guid CLSID_CorMetaDataDispenser = new Guid("e5cb7a31-7512-11d2-89ce-0080c792e5d8");
        Guid IID_IMetadataImport = new Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44");

        object importerObj;
        IMetaDataDispenser dispenser = (IMetaDataDispenser)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_CorMetaDataDispenser));
        dispenser.OpenScope(location, 0, ref IID_IMetadataImport, out importerObj);
        IntPtr importer = Marshal.GetComInterfaceForObject(importerObj, typeof(IMetadataImport));
        ISymbolReader reader = new SymBinder().GetReader(importer, location, string.Empty);

        // see LanguageType module in
        // C:\Program Files\Microsoft.NET\SDK\v2.0\include\corsym.h
        Dictionary languages = new Dictionary();
        languages.Add(new Guid("3f5162f8-07c6-11d3-9053-00c04fa302a1"), "C#");
        languages.Add(new Guid("3a12d0b8-c26c-11d0-b442-00a0244a1dd2"), "Basic");
        // ...

        return languages[reader.GetDocuments()[0].Language];
    }
}

Adesso, questo codice:

// bar.cs
// csc /r:foo.dll /debug+ bar.cs
class Test
{
    static void Main()
    {
        System.Console.WriteLine(Metadata.GetCallingLanguage());
    }
}

stampera' C# a console, mentre questo:

' bar.vb
' vbc /r:foo.dll /debug+ bar.vb
Class Test
    Shared Sub Main
        System.Console.WriteLine(Metadata.GetCallingLanguage())
    End Sub
End Class

che e' identico con quello C# ma scritto in VB.NET, stampera' Basic :-)

Print | posted on domenica 8 luglio 2007 21:04 | Filed Under [ Quiz Sharp Carillon .NET ]

Powered by:
Powered By Subtext Powered By ASP.NET