Classe System.Reflection.Emit.AssemblyBuilder

Cet article vous offre des remarques complémentaires à la documentation de référence pour cette API.

Un assembly dynamique est un assembly créé à l’aide des API d’émission de Réflexions ion. Un assembly dynamique peut référencer des types définis dans un autre assembly dynamique ou statique. Vous pouvez utiliser AssemblyBuilder pour générer des assemblys dynamiques en mémoire et exécuter leur code pendant la même exécution de l’application. Dans .NET 9, nous avons ajouté une nouvelle PersistedAssemblyBuilder implémentation entièrement gérée de l’émission de réflexion qui vous permet d’enregistrer l’assembly dans un fichier. Dans .NET Framework, vous pouvez effectuer les deux opérations : exécuter l’assembly dynamique et l’enregistrer dans un fichier. L’assembly dynamique créé pour l’enregistrement est appelé assembly persistant , tandis que l’assembly en mémoire seule standard est appelé temporaire ou exécutable. Dans .NET Framework, un assembly dynamique peut se composer d’un ou plusieurs modules dynamiques. Dans .NET Core et .NET 5+, un assembly dynamique ne peut se composer qu’d’un seul module dynamique.

La façon dont vous créez une instance diffère pour chaque implémentation, mais d’autres étapes pour définir un module, un type, une AssemblyBuilder méthode ou une énumération, et pour l’écriture d’il, sont assez similaires.

Assemblys dynamiques exécutables dans .NET

Pour obtenir un objet exécutable AssemblyBuilder , utilisez la AssemblyBuilder.DefineDynamicAssembly méthode. Les assemblys dynamiques peuvent être créés à l’aide de l’un des modes d’accès suivants :

Le mode d’accès doit être spécifié en fournissant la valeur appropriée AssemblyBuilderAccess dans l’appel à la AssemblyBuilder.DefineDynamicAssembly méthode lorsque l’assembly dynamique est défini et ne peut pas être modifié ultérieurement. Le runtime utilise le mode d’accès d’un assembly dynamique pour optimiser la représentation interne de l’assembly.

L’exemple suivant montre comment créer et exécuter un assembly :

public void CreateAndRunAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.Run);
    ModuleBuilder mob = ab.DefineDynamicModule("MyModule");
    TypeBuilder tb = mob.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder mb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
                                                                   typeof(int), new Type[] {typeof(int), typeof(int)});
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    Type type = tb.CreateType();

    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
}

Assemblys dynamiques persistants dans .NET

L’API AssemblyBuilder.Save n’a pas été transférée à l’origine vers .NET (Core), car l’implémentation dépend fortement du code natif spécifique à Windows qui n’a pas non plus été porté. Dans .NET 9, nous avons ajouté une implémentation entièrement managée Reflection.Emit qui prend en charge l’enregistrement. Cette implémentation n’est pas dépendante de l’implémentation préexistante spécifique au runtime Reflection.Emit . Autrement dit, il existe maintenant deux implémentations différentes dans .NET, exécutables et persistants. Pour exécuter l’assembly persistant, enregistrez-le d’abord dans un flux de mémoire ou un fichier, puis chargez-le. Avant PersistedAssemblyBuilder, car vous ne pouviez exécuter qu’un assembly généré et ne pas l’enregistrer, il était difficile de déboguer ces assemblys en mémoire. Les avantages de l’enregistrement d’un assembly dynamique dans un fichier sont les suivants :

  • Vous pouvez vérifier l’assembly généré avec des outils tels que ILVerify ou décompiler et l’examiner manuellement avec des outils tels que ILSpy.
  • L’assembly enregistré peut être chargé directement, sans avoir besoin de compiler à nouveau, ce qui peut réduire le temps de démarrage de l’application.

Pour créer une PersistedAssemblyBuilder instance, utilisez le public PersistedAssemblyBuilder(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null) constructeur. Le coreAssembly paramètre est utilisé pour résoudre les types de runtime de base et peut être utilisé pour résoudre le contrôle de version d’assembly de référence :

  • Si Reflection.Emit elle est utilisée pour générer un assembly qui cible un TFM spécifique, ouvrez les assemblys de référence pour le TFM donné à l’aide MetadataLoadContext et utilisez la valeur de la propriété MetadataLoadContext.CoreAssembly pour coreAssembly. Cette valeur permet au générateur d’exécuter sur une version du runtime .NET et de cibler une autre version du runtime .NET.

  • Si Reflection.Emit elle est utilisée pour générer un assembly qui ne sera exécuté que sur la même version d’exécution que la version du runtime sur laquelle le compilateur s’exécute (généralement in-proc), l’assembly principal peut être typeof(object).Assembly. Les assemblys de référence ne sont pas nécessaires dans ce cas.

L’exemple suivant montre comment créer et enregistrer un assembly dans un flux et l’exécuter :

public void CreateSaveAndRunAssembly(string assemblyPath)
{
    PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
    ModuleBuilder mob = ab.DefineDynamicModule("MyModule");
    TypeBuilder tb = mob.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder meb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
                                                                   typeof(int), new Type[] {typeof(int), typeof(int)});
    ILGenerator il = meb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();

    using var stream = new MemoryStream();
    ab.Save(stream);  // or pass filename to save into a file
    stream.Seek(0, SeekOrigin.Begin);
    Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(stream);
    MethodInfo method = assembly.GetType("MyType").GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
}

Pour définir le point d’entrée d’un exécutable et/ou définir d’autres options pour le fichier d’assembly, vous pouvez appeler la public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) méthode et utiliser les métadonnées remplies pour générer l’assembly avec les options souhaitées, par exemple :

PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
TypeBuilder tb = ab.DefineDynamicModule("MyModule").DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
// ...
MethodBuilder entryPoint = tb.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static);
ILGenerator il2 = entryPoint.GetILGenerator();
// ...
il2.Emit(OpCodes.Ret);
tb.CreateType();

MetadataBuilder metadataBuilder = ab.GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder fieldData);
PEHeaderBuilder peHeaderBuilder = new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage);

ManagedPEBuilder peBuilder = new ManagedPEBuilder(
                header: peHeaderBuilder,
                metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
                ilStream: ilStream,
                mappedFieldData: fieldData,
                entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken));

BlobBuilder peBlob = new BlobBuilder();
peBuilder.Serialize(peBlob);

// in case saving to a file:
using var fileStream = new FileStream("MyAssembly.exe", FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fileStream); 

Remarque

Les jetons de métadonnées pour tous les membres sont renseignés sur l’opération Save . N’utilisez pas les jetons d’un type généré et de ses membres avant l’enregistrement, car ils auront des valeurs par défaut ou lèveront des exceptions. Il est sûr d’utiliser des jetons pour les types référencés, non générés.

Certaines API qui ne sont pas importantes pour l’émission d’un assembly ne sont pas implémentées ; par exemple, GetCustomAttributes() n’est pas implémenté. Avec l’implémentation du runtime, vous avez pu utiliser ces API après avoir créé le type. Pour les persistants AssemblyBuilder, ils lèvent NotSupportedException ou NotImplementedException. Si vous avez un scénario qui nécessite ces API, créez un problème dans le dépôt dotnet/runtime.

Pour obtenir une autre façon de générer des fichiers d’assembly, consultez MetadataBuilder.

Assemblys dynamiques persistants dans .NET Framework

Dans .NET Framework, les assemblys dynamiques et les modules peuvent être enregistrés dans des fichiers. Pour prendre en charge cette fonctionnalité, l’énumération AssemblyBuilderAccess déclare deux champs supplémentaires : Save et RunAndSave.

Les modules dynamiques de l’assembly dynamique persistant sont enregistrés lorsque l’assembly dynamique est enregistré à l’aide de la Save méthode. Pour générer un exécutable, la SetEntryPoint méthode doit être appelée pour identifier la méthode qui est le point d’entrée de l’assembly. Les assemblys sont enregistrés sous forme de DLL par défaut, sauf si la SetEntryPoint méthode demande la génération d’une application console ou d’une application Windows.

L’exemple suivant montre comment créer, enregistrer et exécuter un assembly à l’aide de .NET Framework.

public void CreateRunAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.RunAndSave);
    ModuleBuilder mob = ab.DefineDynamicModule("MyAssembly.dll");
    TypeBuilder tb = mob.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder meb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
                                                                   typeof(int), new Type[] {typeof(int), typeof(int)});
    ILGenerator il = meb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    Type type = tb.CreateType();

    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
    ab.Save("MyAssembly.dll");
}

Certaines méthodes de la classe de base Assembly , telles que GetModules et GetLoadedModules, ne fonctionnent pas correctement quand elles sont appelées à partir d’objets AssemblyBuilder . Vous pouvez charger l’assembly dynamique défini et appeler les méthodes sur l’assembly chargé. Par exemple, pour vous assurer que les modules de ressources sont inclus dans la liste des modules retournés, appelez GetModules l’objet chargé Assembly . Si un assembly dynamique contient plusieurs modules dynamiques, le nom du fichier manifeste de l’assembly doit correspondre au nom du module spécifié comme premier argument de la DefineDynamicModule méthode.

La signature d’un assembly dynamique à l’aide KeyPair de n’est pas effective tant que l’assembly n’est pas enregistré sur le disque. Par conséquent, les noms forts ne fonctionnent pas avec les assemblys dynamiques temporaires.

Les assemblys dynamiques peuvent référencer des types définis dans un autre assembly. Un assembly dynamique temporaire peut référencer en toute sécurité des types définis dans un autre assembly dynamique temporaire, un assembly dynamique persistant ou un assembly statique. Toutefois, le Common Language Runtime n’autorise pas un module dynamique persistant à référencer un type défini dans un module dynamique temporaire. Cela est dû au fait que lorsque le module dynamique persistant est chargé après avoir été enregistré sur le disque, le runtime ne peut pas résoudre les références aux types définis dans le module dynamique temporaire.

Restrictions sur l’émission vers des domaines d’application distants

Certains scénarios nécessitent la création et l’exécution d’un assembly dynamique dans un domaine d’application distant. Réflexions’émission d’émission n’autorise pas l’émission d’un assembly dynamique directement vers un domaine d’application distant. La solution consiste à émettre l’assembly dynamique dans le domaine d’application actuel, à enregistrer l’assembly dynamique émis sur le disque, puis à charger l’assembly dynamique dans le domaine d’application distant. Les domaines de communication à distance et d’application sont pris en charge uniquement dans .NET Framework.