Repository navigation
[API Proposal]: Add PersistedAssemblyBuilder type #97015
Description
Background and motivation
We added a new persisted AssemblyBuilder implementation and related new APIs in .NET 9 preview 1. While discussing adding the SetEntryPoint method questions raised for adding SetEntryPoint(MethodInfo, PEFileKinds) the overload that sets PEFileKinds option.
Further we would need to add more options that used in .NET framework, like PortableExecutableKinds or ImageFileMachine that used to set with Save(String, PortableExecutableKinds, ImageFileMachine) overload, plus would need to add APIs for setting AddResourceFile, DefineResource, DefineUnmanagedResource.
Instead of adding all old .NET framework APIs we prefer to let the customers handle their assembly building process by themselves using the PEHeaderBuilder and System.Reflection.PortableExecutable.PEBuilder implementation ManagedPEBuilder options.
Lines 46 to 65 in 27214a0
| public PEHeaderBuilder( | |
| Machine machine = 0, | |
| int sectionAlignment = 0x2000, | |
| int fileAlignment = 0x200, | |
| ulong imageBase = 0x00400000, | |
| byte majorLinkerVersion = 0x30, // (what is ref.emit using?) | |
| byte minorLinkerVersion = 0, | |
| ushort majorOperatingSystemVersion = 4, | |
| ushort minorOperatingSystemVersion = 0, | |
| ushort majorImageVersion = 0, | |
| ushort minorImageVersion = 0, | |
| ushort majorSubsystemVersion = 4, | |
| ushort minorSubsystemVersion = 0, | |
| Subsystem subsystem = Subsystem.WindowsCui, | |
| DllCharacteristics dllCharacteristics = DllCharacteristics.DynamicBase | DllCharacteristics.NxCompatible | DllCharacteristics.NoSeh | DllCharacteristics.TerminalServerAware, | |
| Characteristics imageCharacteristics = Characteristics.Dll, | |
| ulong sizeOfStackReserve = 0x00100000, | |
| ulong sizeOfStackCommit = 0x1000, | |
| ulong sizeOfHeapReserve = 0x00100000, | |
| ulong sizeOfHeapCommit = 0x1000) |
Lines 37 to 48 in 27214a0
| public ManagedPEBuilder( | |
| PEHeaderBuilder header, | |
| MetadataRootBuilder metadataRootBuilder, | |
| BlobBuilder ilStream, | |
| BlobBuilder? mappedFieldData = null, | |
| BlobBuilder? managedResources = null, | |
| ResourceSectionBuilder? nativeResources = null, | |
| DebugDirectoryBuilder? debugDirectoryBuilder = null, | |
| int strongNameSignatureSize = DefaultStrongNameSignatureSize, | |
| MethodDefinitionHandle entryPoint = default(MethodDefinitionHandle), | |
| CorFlags flags = CorFlags.ILOnly, | |
| Func<IEnumerable<Blob>, BlobContentId>? deterministicIdProvider = null) |
These options are used to produce the assembly and allow users to configure it any way they want. These covers all options that existed in .NET framework, (but not exactly same way), plus provide many other options that not available in .NET framework that customers may want to set on the final binary.
In order to achieve this, we would need to provide all metadata information produced with Reflection.Emit APIs (MetadataBuilder and ILStreams) so that user could embed them into the corresponding section of PEBuidler. But because the MetadataBuilder and BlobBuilder types are not accessible from within CoreLib we decided to make the PersistedAssemblyBuilder type public and reintroduce the related new APIs there, remove the new APIs from the base AssemblyBuilder type.
API Proposal
namespace System.Reflection.Emit;
public abstract partial class AssemblyBuilder
{
// Previously approved new APIs
- public static AssemblyBuilder DefinePersistedAssembly(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null);
- public void Save(Stream stream);
- public void Save(string assemblyFileName);
- protected abstract void SaveCore(Stream stream);
}
+public sealed class PersistedAssemblyBuilder : AssemblyBuilder
{
+ public PersistedAssemblyBuilder(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null);
+ public void Save(Stream stream);
+ public void Save(string assemblyFileName);
+ public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData);
}API Usage
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(
// Header for PEFileKinds.WindowApplication imageCharacteristics and subsystem need to be set
imageBase: 0x00400000, // this is the default value, was not necessarily needed to set.
imageCharacteristics: Characteristics.ExecutableImage,
subsystem: Subsystem.WindowsGui);
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); Old AssemblyBuilder.SetEntryPoint(MethodInfo) Proposal folded below:
Add AssemblyBuilder.SetEntryPoint(MethodInfo) method
### Background and motivationAs a new persist able AssemblyBuilder implementation is being added in .NET 9 it also needs an API for setting the Entry point. We could add similar but virtual API we have in .NET framework.
API Proposal
namespace System.Reflection.Emit;
public abstract partial class AssemblyBuilder
{
// Previously approved new APIs
public static AssemblyBuilder DefinePersistedAssembly(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null);
public void Save(Stream stream);
public void Save(string assemblyFileName);
protected abstract void SaveCore(Stream stream);
+ public virtual void SetEntryPoint(MethodInfo entryMethod);
}API Usage
AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(new AssemblyName("MyAssembly"), typeof(object).Assembly, null);
TypeBuilder tb = ab.DefineDynamicModule("MyModule").DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
MethodBuilder mb1 = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static, typeof(int), [typeof(int), typeof(int)]);
ILGenerator il = mb1.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
MethodBuilder main = tb.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static);
ILGenerator il2 = main.GetILGenerator();
il2.Emit(OpCodes.Ldc_I4_S, 10);
il2.Emit(OpCodes.Ldc_I4_1);
il2.Emit(OpCodes.Call, mb1);
il2.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", [typeof(int)])!);
il2.Emit(OpCodes.Ret);
tb.CreateType();
ab.SetEntryPoint(main);
ab.Save("MyAssembly.exe");Alternative Designs
Or keep the same pattern (have protected virtual *Core method).
namespace System.Reflection.Emit;
public abstract partial class AssemblyBuilder
{
// Previously approved new APIs
public static AssemblyBuilder DefinePersistedAssembly(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null);
public void Save(Stream stream);
public void Save(string assemblyFileName);
protected abstract void SaveCore(Stream stream);
+ public void SetEntryPoint(MethodInfo entryMethod);
+ protected virtual void SetEntryPointCore(MethodInfo entryMethod);
}
Activity
jkotas commentedon Jan 16, 2024
Does it need to be virtual?
buyaa-n commentedon Jan 16, 2024
Yes, it needs to be virtual, thank you. Updated the proposal to
public virtual void SetEntryPoint(...), or can beprotected virtual SetEntryPointCore(...)that will be called from thepublic void SetEntryPoint(...).reflectronic commentedon Jan 30, 2024
Is there a reason why
SetEntryPoint(MethodInfo, PEFileKinds)from .NET Framework is not also being added? I notice thatSave(string, PortableExecutableKinds, ImageFileMachine)is missing from theAssemblyBuilderproposal too. These overloads are needed for building certain kinds of .NET Framework assemblies. Is it expected thatAssemblyBuilder.Savein .NET 9 will not support this?buyaa-n commentedon Jan 31, 2024
PEFileKindsdoesn't exist in .NET core and doesn't really have anything useful.For now, the assembly is DLL only if no entry point set, if user needs to set more than this we can consider adding API for setting PEHeaderBuilder in the future, but that option doesn't have to be associated with
SetEntryPointorSavemethodPlease try out the default implementation and let us know what exact scenarios is not covered, and also check PEHeaderBuilder if there is an option that could cover your scenarios and let us know if there is anything not covered.
bartonjs commentedon Feb 6, 2024
Video
10 remaining items
jkotas commentedon Feb 20, 2024
I think
PersistedAssemblyBuildershould be a sealed type with no extensibility.buyaa-n commentedon Feb 20, 2024
API Proposal
namespace System.Reflection.Emit; public abstract partial class AssemblyBuilder { // Previously approved new APIs - public static AssemblyBuilder DefinePersistedAssembly(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null); - public void Save(Stream stream); - public void Save(string assemblyFileName); - protected abstract void SaveCore(Stream stream); } +public sealed class PersistedAssemblyBuilder : AssemblyBuilder { + public PersistedAssemblyBuilder(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null); + public void Save(Stream stream); + public void Save(string assemblyFileName); + public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData); }API Usage
jkotas commentedon Feb 20, 2024
This can be a regular constructor. I do not think that a factory method serves any purpose here.
[-][API Proposal]: Add AssemblyBuilder.SetEntryPoint(MethodInfo) method[/-][+][API Proposal]: Add PersistedAssemblyBuilder type[/+]buyaa-n commentedon Feb 22, 2024
I have moved the API proposal to the description as it became completely different proposal than the original one, folded the old proposal in the description. Added blocking to get it reviewed ASAP.
masonwheeler commentedon Feb 23, 2024
@buyaa-n
mainhas not been defined in this sample code. Is it supposed to be the same thing asentryPoint?buyaa-n commentedon Feb 23, 2024
Right, in the new example I changed the
main->entryPoint, forgot to update inILGenerator il2 = main.GetILGenerator();, thanks!bartonjs commentedon Mar 12, 2024
Video
Breaking API compatibility from .NET Framework (by having Save somewhere other than on AssemblyBuilder) is unfortunate; but seems tolerable based on expected usage.
Given that, looks good as proposed.
namespace System.Reflection.Emit; public abstract partial class AssemblyBuilder { // Previously approved new APIs - public static AssemblyBuilder DefinePersistedAssembly(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null); - public void Save(Stream stream); - public void Save(string assemblyFileName); - protected abstract void SaveCore(Stream stream); } +public sealed class PersistedAssemblyBuilder : AssemblyBuilder { + public PersistedAssemblyBuilder(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null); + public void Save(Stream stream); + public void Save(string assemblyFileName); + public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData); }