Assemblys dynamiques persistants dans .NET
Cet article fournit des remarques supplémentaires à la documentation de référence de cette API.
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é. Nouveauté de .NET 9, la classe PersistedAssemblyBuilder ajoute une implémentation de Reflection.Emit
entièrement managée qui prend en charge la sauvegarde. Cette implémentation n’a aucune dépendance avec l’implémentation Reflection.Emit
spécifique au runtime préexistante. 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
, vous ne pouvez exécuter qu’un assembly généré et ne pas l’enregistrer. Étant donné que l'assemblage était uniquement en mémoire, il était difficile de déboguer. 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 instance PersistedAssemblyBuilder
, utilisez le constructeur PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>). Le paramètre coreAssembly
est utilisé pour résoudre les types d’exécution de base et peut être utilisé pour résoudre le contrôle de version d’assembly de référence :
Si
Reflection.Emit
est utilisé 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 simplement êtretypeof(object).Assembly
. L’exemple suivant montre comment créer et enregistrer un assembly dans un flux et l’exécuter avec l’assembly runtime actuel :public static void CreateSaveAndRunAssembly() { 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 })); }
Si
Reflection.Emit
est utilisé 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 deMetadataLoadContext
et utilisez la valeur de la propriété MetadataLoadContext.CoreAssembly pourcoreAssembly
. 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. Vous devez utiliser des types retournés par l’instanceMetadataLoadContext
lors du référencement des types principaux. Par exemple, au lieu detypeof(int)
, recherchez le type deSystem.Int32
dansMetadataLoadContext.CoreAssembly
par nom :public static void CreatePersistedAssemblyBuilderCoreAssemblyWithMetadataLoadContext(string refAssembliesPath) { PathAssemblyResolver resolver = new PathAssemblyResolver(Directory.GetFiles(refAssembliesPath, "*.dll")); using MetadataLoadContext context = new MetadataLoadContext(resolver); Assembly coreAssembly = context.CoreAssembly; PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyDynamicAssembly"), coreAssembly); TypeBuilder typeBuilder = ab.DefineDynamicModule("MyModule").DefineType("Test", TypeAttributes.Public); MethodBuilder methodBuilder = typeBuilder.DefineMethod("Method", MethodAttributes.Public, coreAssembly.GetType(typeof(int).FullName), Type.EmptyTypes); // .. add members and save the assembly }
Définir le point d’entrée d’un exécutable
Pour définir le point d’entrée d’un exécutable ou pour définir d’autres options pour le fichier d’assembly, vous pouvez appeler la méthode public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData)
et utiliser les métadonnées remplies pour générer l’assembly avec les options souhaitées, par exemple :
public static void SetEntryPoint()
{
PersistedAssemblyBuilder ab = new(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);
ManagedPEBuilder peBuilder = new(
header: PEHeaderBuilder.CreateExecutableHeader(),
metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
ilStream: ilStream,
mappedFieldData: fieldData,
entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken));
BlobBuilder peBlob = new();
peBuilder.Serialize(peBlob);
// Create the executable:
using FileStream fileStream = new("MyAssembly.exe", FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fileStream);
}
Émettre des symboles et générer une base de données PDB
Les métadonnées des symboles sont renseignées dans le paramètre pdbBuilder
out lorsque vous appelez la méthode GenerateMetadata(BlobBuilder, BlobBuilder) sur une instance de PersistedAssemblyBuilder
. Pour créer un assembly avec une base de données PDB portable :
- Créez des instances ISymbolDocumentWriter avec la méthode ModuleBuilder.DefineDocument(String, Guid, Guid, Guid). Lors de l'émission de l'IL de la méthode, émettez également l'information sur le symbole correspondant.
- Créez une instance PortablePdbBuilder à l’aide de l’instance de
pdbBuilder
produite par la méthode GenerateMetadata(BlobBuilder, BlobBuilder). - Sérialisez le
PortablePdbBuilder
dans un Blobet écrivez leBlob
dans un flux de fichiers PDB (uniquement si vous générez une base de données PDB autonome). - Créez une instance DebugDirectoryBuilder et ajoutez un DebugDirectoryBuilder.AddCodeViewEntry (PDB autonome) ou DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
- Définissez l’argument
debugDirectoryBuilder
facultatif lors de la création de l’instance PEBuilder.
L’exemple suivant montre comment émettre des informations de symboles et générer un fichier PDB.
static void GenerateAssemblyWithPdb()
{
PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
ModuleBuilder mb = ab.DefineDynamicModule("MyModule");
TypeBuilder tb = mb.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
MethodBuilder mb1 = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static, typeof(int), [typeof(int), typeof(int)]);
ISymbolDocumentWriter srcDoc = mb.DefineDocument("MySourceFile.cs", SymLanguageType.CSharp);
ILGenerator il = mb1.GetILGenerator();
LocalBuilder local = il.DeclareLocal(typeof(int));
local.SetLocalSymInfo("myLocal");
il.MarkSequencePoint(srcDoc, 7, 0, 7, 11);
...
il.Emit(OpCodes.Ret);
MethodBuilder entryPoint = tb.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static);
ILGenerator il2 = entryPoint.GetILGenerator();
il2.BeginScope();
...
il2.EndScope();
...
tb.CreateType();
MetadataBuilder metadataBuilder = ab.GenerateMetadata(out BlobBuilder ilStream, out _, out MetadataBuilder pdbBuilder);
MethodDefinitionHandle entryPointHandle = MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken);
DebugDirectoryBuilder debugDirectoryBuilder = GeneratePdb(pdbBuilder, metadataBuilder.GetRowCounts(), entryPointHandle);
ManagedPEBuilder peBuilder = new ManagedPEBuilder(
header: new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage, subsystem: Subsystem.WindowsCui),
metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
ilStream: ilStream,
debugDirectoryBuilder: debugDirectoryBuilder,
entryPoint: entryPointHandle);
BlobBuilder peBlob = new BlobBuilder();
peBuilder.Serialize(peBlob);
using var fileStream = new FileStream("MyAssembly.exe", FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fileStream);
}
static DebugDirectoryBuilder GeneratePdb(MetadataBuilder pdbBuilder, ImmutableArray<int> rowCounts, MethodDefinitionHandle entryPointHandle)
{
BlobBuilder portablePdbBlob = new BlobBuilder();
PortablePdbBuilder portablePdbBuilder = new PortablePdbBuilder(pdbBuilder, rowCounts, entryPointHandle);
BlobContentId pdbContentId = portablePdbBuilder.Serialize(portablePdbBlob);
// In case saving PDB to a file
using FileStream fileStream = new FileStream("MyAssemblyEmbeddedSource.pdb", FileMode.Create, FileAccess.Write);
portablePdbBlob.WriteContentTo(fileStream);
DebugDirectoryBuilder debugDirectoryBuilder = new DebugDirectoryBuilder();
debugDirectoryBuilder.AddCodeViewEntry("MyAssemblyEmbeddedSource.pdb", pdbContentId, portablePdbBuilder.FormatVersion);
// In case embedded in PE:
// debugDirectoryBuilder.AddEmbeddedPortablePdbEntry(portablePdbBlob, portablePdbBuilder.FormatVersion);
return debugDirectoryBuilder;
}
En outre, vous pouvez ajouter CustomDebugInformation en appelant la méthode MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) à partir de l’instance pdbBuilder
pour ajouter des informations avancées sur l’incorporation source et l’indexation de sources PDB.
private static void EmbedSource(MetadataBuilder pdbBuilder)
{
byte[] sourceBytes = File.ReadAllBytes("MySourceFile2.cs");
BlobBuilder sourceBlob = new BlobBuilder();
sourceBlob.WriteBytes(sourceBytes);
pdbBuilder.AddCustomDebugInformation(MetadataTokens.DocumentHandle(1),
pdbBuilder.GetOrAddGuid(new Guid("0E8A571B-6926-466E-B4AD-8AB04611F5FE")), pdbBuilder.GetOrAddBlob(sourceBlob));
}
Ajouter des ressources avec PersistedAssemblyBuilder
Vous pouvez appeler MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) pour ajouter autant de ressources que nécessaire. Les flux doivent être concaténés en un seul BlobBuilder que vous passez dans l'argument ManagedPEBuilder. L’exemple suivant montre comment créer des ressources et l’attacher à l’assembly créé.
public static void SetResource()
{
PersistedAssemblyBuilder ab = new(new AssemblyName("MyAssembly"), typeof(object).Assembly);
ab.DefineDynamicModule("MyModule");
MetadataBuilder metadata = ab.GenerateMetadata(out BlobBuilder ilStream, out _);
using MemoryStream stream = new();
ResourceWriter myResourceWriter = new(stream);
myResourceWriter.AddResource("AddResource 1", "First added resource");
myResourceWriter.AddResource("AddResource 2", "Second added resource");
myResourceWriter.AddResource("AddResource 3", "Third added resource");
myResourceWriter.Close();
byte[] data = stream.ToArray();
BlobBuilder resourceBlob = new();
resourceBlob.WriteInt32(data.Length);
resourceBlob.WriteBytes(data);
metadata.AddManifestResource(
ManifestResourceAttributes.Public,
metadata.GetOrAddString("MyResource.resources"),
implementation: default,
offset: 0);
ManagedPEBuilder peBuilder = new(
header: PEHeaderBuilder.CreateLibraryHeader(),
metadataRootBuilder: new MetadataRootBuilder(metadata),
ilStream: ilStream,
managedResources: resourceBlob);
BlobBuilder blob = new();
peBuilder.Serialize(blob);
// Create the assembly:
using FileStream fileStream = new("MyAssemblyWithResource.dll", FileMode.Create, FileAccess.Write);
blob.WriteContentTo(fileStream);
}
L’exemple suivant montre comment lire des ressources à partir de l’assembly créé.
public static void ReadResource()
{
Assembly readAssembly = Assembly.LoadFile(Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"MyAssemblyWithResource.dll"));
// Use ResourceManager.GetString() to read the resources.
ResourceManager rm = new("MyResource", readAssembly);
Console.WriteLine("Using ResourceManager.GetString():");
Console.WriteLine($"{rm.GetString("AddResource 1", CultureInfo.InvariantCulture)}");
Console.WriteLine($"{rm.GetString("AddResource 2", CultureInfo.InvariantCulture)}");
Console.WriteLine($"{rm.GetString("AddResource 3", CultureInfo.InvariantCulture)}");
// Use ResourceSet to enumerate the resources.
Console.WriteLine();
Console.WriteLine("Using ResourceSet:");
ResourceSet resourceSet = rm.GetResourceSet(CultureInfo.InvariantCulture, createIfNotExists: true, tryParents: false);
foreach (DictionaryEntry entry in resourceSet)
{
Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
}
// Use ResourceReader to enumerate the resources.
Console.WriteLine();
Console.WriteLine("Using ResourceReader:");
using Stream stream = readAssembly.GetManifestResourceStream("MyResource.resources")!;
using ResourceReader reader = new(stream);
foreach (DictionaryEntry entry in reader)
{
Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
}
}
Remarque
Les jetons de métadonnées de tous les membres sont remplis lors de l'opération Save. N'utilisez pas les jetons d'un type généré et de ses membres avant de les enregistrer, car ils auront des valeurs par défaut ou lanceront des exceptions. Il est prudent d'utiliser des jetons pour les types qui sont référencés et 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ée. Avec l’implémentation du runtime, vous avez pu utiliser ces API après avoir créé le type. Pour les AssemblyBuilder
persistants, ils lancent NotSupportedException
ou NotImplementedException
. Si vous avez un scénario qui nécessite ces API, soumettez un ticket dans le dépôt dotnet/runtime .
Pour obtenir une autre façon de générer des fichiers d’assembly, consultez MetadataBuilder.