Draft: Enable saving dynamic assemblies to disk on .NET 9+ using PersistentAssemblyBuilder #701
Draft: Enable saving dynamic assemblies to disk on .NET 9+ using PersistentAssemblyBuilder #701stakx wants to merge 3 commits intocastleproject:masterfrom
PersistentAssemblyBuilder #701Conversation
| #if !NET9_0_OR_GREATER | ||
| InitializeStaticFields(proxyType); | ||
| #endif |
There was a problem hiding this comment.
This must be skipped here because the generated type cannot be activated while tied to a PersitedAssemblyBuilder.
| lastAssemblyGenerated?.Dispose(); | ||
| var stream = new MemoryStream(); | ||
| persistedAssemblyBuilder.Save(stream); | ||
| stream.Seek(0, SeekOrigin.Begin); | ||
| var assembly = AssemblyLoadContext.Default.LoadFromStream(stream); | ||
| type = assembly.GetType(type.FullName!)!; | ||
| lastAssemblyGenerated = stream; | ||
| lastScope = scope; | ||
| scope = scope.Recycle(); |
There was a problem hiding this comment.
This would likely have to be made thread-safe.
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| #if NET9_0_OR_GREATER |
There was a problem hiding this comment.
Instead of using NET9_0_OR_GREATER we'd probably want a FEATURE_PERSISTEDASSEMBLYBUILDER conditional compilation symbol.
| #if NET9_0_OR_GREATER | ||
| assemblyFilePath = weakModulePath; | ||
| #else |
There was a problem hiding this comment.
This alternative assignment may actually work also for .NET 4.6.2+, as [Weak|Strong]NamedModule.FullyQualifiedName is possibly equal to [weak|strong]ModulePath.
| #elif NET9_0_OR_GREATER | ||
| var persistedAssemblyBuilder = (PersistedAssemblyBuilder)assemblyBuilder; | ||
| persistedAssemblyBuilder.Save(assemblyFilePath); |
There was a problem hiding this comment.
Looks like this is left-over code that (if it worked) would have to be called by the PersistentProxyBuilder on its lastScope. This code has been replaced with code directly in PersistentProxyBuilder however... probably not ideal.
| var assemblyPath = lastScope.WeakNamedModule != null ? lastScope.WeakNamedModuleName : lastScope.StrongNamedModuleName; | ||
| using var file = File.Create(assemblyPath); | ||
| lastAssemblyGenerated.Seek(0, SeekOrigin.Begin); | ||
| lastAssemblyGenerated.CopyTo(file); | ||
| file.Flush(); | ||
| file.Close(); | ||
| return assemblyPath; |
There was a problem hiding this comment.
This should perhaps be replaced with return lastScope.SaveAssembly();.
|
Superseded by #718. |
This is the smallest code change I can think of off the top of my head to enable
PersistentAssemblyBuilder.SaveAssembly()on .NET 9+. It represents only a code exploration to test feasibility, but it is far from an ideal solution and won't be merged in this form.The approach chosen here works using the following approach:
Dynamic types are generated using .NET 9's new
PersistedAssemblyBuilder. The major obstacle here is that it does not support type activation. In order to activate a type, the assembly is emitted into a stream, which can then be loaded into the current assembly load context like any regular assembly.This means that the dynamic assembly is "baked" after every single proxy type activation, and it cannot be reused to emit further types. Because of that, the
ModuleScopegets replaced with a new one after every single proxy type creation.Because of the
ModuleScoperecycling, we essentially lose the type cache functionality. In theory, this functionality could be restored by lifting the type cache out ofModuleScopeintoDefaultProxyBuilder(so that it can span several module scopes). This refactoring would be fairly involved if breaking changes in the public API were to be avoided.Something similar to this might be good enough to run out test suite (including PE / IL verification of generated assemblies) on .NET 9, as we could likely do well without the type cache in this scenario.