2009-09-01 17 views
11

È possibile passare un'espressione lambda a un AppDomain secondario come un flusso di byte IL e quindi assemblarlo di nuovo utilizzando DynamicMethod in modo che possa essere chiamato?Passare un lambda a un AppDomain secondario come un flusso di IL e riassemblarlo utilizzando DynamicMethod

io non sono troppo sicuro che questo sia il modo giusto per andare in primo luogo, quindi ecco la (dettagliata) motivo che mi chiedo questa domanda ...

Nelle mie applicazioni, ci sono un sacco di casi quando ho bisogno di caricare un paio di assiemi per la riflessione, così posso determinare cosa fare con loro dopo. La parte problematica è che devo essere in grado di scaricare gli assembly dopo che ho finito di rifletterci sopra. Ciò significa che ho bisogno di caricarli utilizzando un altro AppDomain.

Ora, la maggior parte dei miei casi sono simili, tranne che non del tutto. Ad esempio, a volte ho bisogno di restituire una semplice conferma, altre volte ho bisogno di serializzare un flusso di risorse dall'assembly, e altre volte ho bisogno di fare una callback o due.

Così alla fine scrivo più e più volte lo stesso codice di creazione temporaneo AppDomain temporaneo e l'implementazione di proxy personalizzati MarshalByRefObject per comunicare tra il nuovo dominio e quello originale.

Dato che questo non è più davvero accettabile, ho deciso di codificare me una classe AssemblyReflector che potrebbe essere utilizzato in questo modo:

using (var reflector = new AssemblyReflector(@"C:\MyAssembly.dll")) 
{ 
    bool isMyAssembly = reflector.Execute(assembly => 
    { 
     return assembly.GetType("MyAssembly.MyType") != null; 
    }); 
} 

AssemblyReflector sarebbe automatizzare il AppDomain scarico in virtù della IDisposable, e mi permette di eseguire a Func<Assembly,object> - tipo lambda che tiene il codice di riflessione in un altro AppDomain in modo trasparente.

Il problema è che lambda non può essere passato ad altri domini così semplicemente. Quindi, dopo aver cercato in giro, ho trovato quello che sembra un modo per farlo: passare il lambda al nuovo AppDomain come un flusso di IL - e questo mi porta alla domanda originale.

Ecco quello che ho provato, ma non ha funzionato (il problema è stato BadImageFormatException viene generata quando si cerca di chiamare il nuovo delegato):

public delegate object AssemblyReflectorDelegate(Assembly reflectedAssembly); 

public class AssemblyReflector : IDisposable 
{ 
    private AppDomain _domain; 
    private string _assemblyFile; 
    public AssemblyReflector(string fileName) { ... } 
    public void Dispose() { ... } 

    public object Execute(AssemblyReflectorDelegate reflector) 
    { 
     var body = reflector.Method.GetMethodBody(); 
     _domain.SetData("IL", body.GetILAsByteArray()); 
     _domain.SetData("MaxStackSize", body.MaxStackSize); 
     _domain.SetData("FileName", _assemblyFile); 

     _domain.DoCallBack(() => 
     { 
      var il = (byte[])AppDomain.CurrentDomain.GetData("IL"); 
      var stack = (int)AppDomain.CurrentDomain.GetData("MaxStackSize"); 
      var fileName = (string)AppDomain.CurrentDomain.GetData("FileName"); 
      var args = Assembly.ReflectionOnlyLoadFrom(fileName); 
      var pars = new Type[] { typeof(Assembly) }; 

      var dm = new DynamicMethod("", typeof(object), pars, 
       typeof(string).Module); 
      dm.GetDynamicILInfo().SetCode(il, stack); 

      var clone = (AssemblyReflectorDelegate)dm.CreateDelegate(
       typeof(AssemblyReflectorDelegate)); 
      var result = clone(args); // <-- BadImageFormatException thrown. 

      AppDomain.CurrentDomain.SetData("Result", result); 
     }); 

     // Result obviously needs to be serializable for this to work. 
     return _domain.GetData("Result"); 
    } 
} 

sono io nemmeno vicino (cosa manca?), O è questo un esercizio inutile tutto sommato?

NOTA: mi rendo conto che se questo ha funzionato, dovrei ancora fare attenzione a ciò che ho messo in lambda in riferimento ai riferimenti. Questo non è un problema, però.

AGGIORNAMENTO: sono riuscito a ottenere un po 'di più. Sembra che chiamare semplicemente SetCode(...) non sia abbastanza per ricostruire il metodo. Ecco ciò che è necessario:

// Build a method signature. Since we know which delegate this is, this simply 
// means adding its argument types together. 
var builder = SignatureHelper.GetLocalVarSigHelper(); 
builder.AddArgument(typeof(Assembly), false); 
var signature = builder.GetSignature(); 

// This is the tricky part... See explanation below. 
di.SetCode(ILTokenResolver.Resolve(il, di, module), stack); 
dm.InitLocals = initLocals; // Value gotten from original method's MethodInfo. 
di.SetLocalSignature(signature); 

Il trucco è il seguente. L'IL originale contiene determinati token di metadati validi solo nel contesto del metodo originale. Avevo bisogno di analizzare l'IL e sostituire quei token con quelli che sono validi nel nuovo contesto. L'ho fatto utilizzando una classe speciale, ILTokenResolver, che ho adattato da queste due fonti: Drew Wilson e Haibo Luo.

C'è ancora un piccolo problema con questo - il nuovo IL non sembra essere esattamente valido.A seconda del contenuto esatto del lambda, può o meno lanciare un InvalidProgramException in fase di esecuzione.

Come semplice esempio, questo funziona:

reflector.Execute(a => { return 5; }); 

mentre questo non:

reflector.Execute(a => { int a = 5; return a; }); 

Ci sono anche esempi più complessi che sono o funziona o non, secondo alcuni ancora- differenza da determinare. Potrei aver perso qualche piccolo ma importante dettaglio. Ma sono abbastanza sicuro che lo troverò dopo un confronto più dettagliato delle uscite ildasm. Pubblicherò i miei risultati qui, quando lo farò.

MODIFICA: Oh, amico. Ho completamente dimenticato che questa domanda era ancora aperta. Ma come probabilmente è diventato ovvio in sé, ho rinunciato a risolvere questo. Non ne sono felice, questo è sicuro. È davvero un peccato, ma suppongo che aspetterò un miglior supporto dal framework e/o CLR prima di tentare di nuovo. Ci sono solo molti hack che devi fare per farlo funzionare, e anche in questo caso non è affidabile. Scuse a tutti gli interessati.

risposta

1

Probabilmente no, perché un lambda è più di un'espressione nel codice sorgente. le espressioni lambda creano anche chiusure che catturano/sollevano variabili nelle proprie classi nascoste. Il programma viene modificato dal compilatore, quindi ovunque tu usi quelle variabili che stai effettivamente parlando alla classe. Quindi dovresti non solo passare il codice per il lambda, ma anche eventuali modifiche alle variabili di chiusura nel tempo.

+0

Ne ero consapevole. Ma pensavo che non sarebbe stato un problema se non avessi usato variabili esterne in lambda? E se usassi un vecchio delegato semplice invece di lambda? – aoven

+0

Parte del punto è che un lambda non è solo il codice nell'espressione. Trasforma anche altri codici nella tua app in fase di compilazione. È possibile trasferire le modifiche in fase di compilazione a un altro assembly già compilato. –

2

Non ho ottenuto esattamente il problema che stai cercando di risolvere, ma in passato ho creato un componente che potrebbe risolverlo.

Fondamentalmente, il suo scopo era generare un'espressione Lambda da un string. Utilizza uno AppDomain separato per eseguire il compilatore CodeDOM. L'IL di un metodo compilato viene serializzato all'originale AppDomain e quindi ricostruito a un delegato con DynamicMethod. Quindi, il delegato viene chiamato e viene restituita un'espressione lambda.

Ho pubblicato una spiegazione completa sul mio blog. Naturalmente, è open source. Quindi, se lo usi, ti prego di inviarmi qualsiasi commento che ritieni ragionevole.

+0

Ho voluto iniettare il codice dal mio AppDomain nella mia estensione VS AppDomain principale e questo ha funzionato magnificamente! FYI a chiunque voglia utilizzare questo codice per il codice esistente, sarà necessario apportare modifiche per ricostruire il DynamicMethod dal MethodIL passando il modulo che contiene tutti i tipi per evitare eccezioni di accesso/sicurezza. – mstrange

+0

@mstrange Sono felice di sapere che questo è ancora utile dopo più di 6 anni. – jpbochi

Problemi correlati