Add generated serializer contexts for collectible AssemblyLoadContext#454
Draft
SunSi12138 wants to merge 4 commits into
Draft
Add generated serializer contexts for collectible AssemblyLoadContext#454SunSi12138 wants to merge 4 commits into
SunSi12138 wants to merge 4 commits into
Conversation
Author
|
Hi @neuecc, when you have time, I’d really appreciate any early feedback on whether this source-generated context direction fits MemoryPack. Thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #453.
Summary
This adds a .NET 7+ source-generated, instance-scoped serializer context for types that must remain unloadable with a collectible
AssemblyLoadContext.Each context instance owns its formatter registry, closed formatter graph, type map, and generated materializer delegates. The generated
PluginMemoryPackContext.Defaultis a read-only static on the plugin's context type, so it belongs to that assembly's lifetime. There is no process-wide contextCurrent, baseDefault,AsyncLocal, or options-based mutable resolver.The existing no-context APIs and
MemoryPackFormatterProviderremain in place. The old serializer, writer, reader, and provider hot paths are not modified. A type included in a context stops eager global registration, but its existingIMemoryPackFormatterRegister.RegisterFormatter()implementation remains available and the legacy static API registers it lazily when explicitly used.Lifetime model
The legacy retention chain is:
Plugin-owned static fields only form references within the collectible lifetime and do not create this reverse root.
GenerateType.NoGenerateis also not sufficient by itself: the manual workaround succeeds only when generated eager registration is avoided,RegisterFormatter()does not register globally, and every used packable/container path avoids the static provider.The context path builds a separate generated graph with injected dependencies and no provider fallback. It covers objects, version-tolerant and circular-reference types, generated collections, unions,
Type, nullable values, arrays through rank four, list/dictionary and array-like shapes, tuples, interface/concurrent/immutable collections, and .NET 8 frozen collections. Closed generic MemoryPackable types are expanded from their concrete context arguments.Cross-assembly MemoryPackable dependencies require a generated context factory in their declaring assembly. A bridge context can then declare shapes such as
Dictionary<PluginA, PluginB>without runtime context composition. The bridge and both referenced plugins intentionally form one lifetime group.Typeuses a context-local wire-name map. A type not present in the generated graph fails instead of falling back toType.GetTypeor the global provider.FormatterTypecan provide an external leaf or nested dependency, and an advanced formatter can implementIMemoryPackSerializerContextFormatterFactory<T>.Compatibility
Serialize<T>/Deserialize<T>overloads and wire format remain compatible.nulloptions calls and method-group conversions have compile tests.TContextand static-abstract generated entry point; it does not addTypelookup, reflection, or boxing to the hot path.Type/objectAPIs are available on the context for tooling and may box by design.IDisposable; removing all strong references releases ownership.Stateless Core/BCL leaf formatters that cannot reference a collectible type can still be shared. A global
Typekey, reflected member, plugin formatter instance, delegate, or container formatter closed over a plugin type is not safe to share. A user formatter that explicitly callsMemoryPackFormatterProvider.Registeropts out of context isolation.Tests
net7.0and round-tripped successfully.netstandard2.1,net7.0, andnet8.0; Unity shims build for the same existing targets.Unload(), repeated collection/finalizer cycles, and separate weak references for the ALC, assembly, and plugin type.Default, nestedType, exception paths, non-generic context APIs, arrays/lists/dictionaries, a cross-ALC object reference, and a bridge context that holds then releases both ALCs.Performance validation
The PR includes BenchmarkDotNet coverage for simple DTOs, deep graphs, arrays, lists, dictionaries, unions, byte arrays,
IBufferWriter, sequence deserialization, streams, and context construction. It also includes a context-free static-path baseline benchmark.The established static serializer/provider/writer/reader implementations are unchanged, and a generator regression test verifies byte-for-byte identical generated source for a type unrelated to a context. Short local smoke runs showed identical steady-state allocation counts, but their timing variance is too large to support a 0.1% claim. Repeated-launch equivalence measurements can be attached separately; no noisy shared-CI hard gate is introduced here.
Intentional limitations
Deserializespells both generic arguments because C# cannot infer the result type, and this preserves existingDeserialize<T>(bytes, null)binding.