Skip to content

AOT: core module-serializer table (#8626, part 1) - #8645

Merged
Aaronontheweb merged 5 commits into
akkadotnet:devfrom
Aaronontheweb:feature/aot-m2-1-module-serializer-table
Sep 28, 2026
Merged

Aaronontheweb merged 5 commits into
akkadotnet:devfrom
Aaronontheweb:feature/aot-m2-1-module-serializer-table

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

First PR of the #8626 stack. It adds the core half of the module serializer table. No module ships a table yet; the Remote, Cluster family and Persistence PRs each add one.

What changes

  • ModuleSerializers (internal, abstract): a module lists its serializers as ModuleSerializer(Type, Func<ExtendedActorSystem, Config, Serializer>) and its bound types as IReadOnlyList<Type>.
  • ModuleSerializerTable (internal): maps an assembly simple name to a lambda that calls Load("<literal type name>"). Load takes a [DynamicallyAccessedMembers(PublicParameterlessConstructor)] string, the same pattern as CreateProvider in AOT: resolve built-in actor ref providers from constant type names so the trimmer preserves them #8599. Each module gets its own lambda and literal, so the trimmer keeps each table type. The map is empty in this PR.
    • Lazy: a module loads only when a row misses core's built-in tables and the row's assembly half names that module. Rows that hit the built-in tables never probe.
    • Cached per assembly (case-insensitive) in a ConcurrentDictionary, as a lookup keyed by stripped full name that is built once when the module loads, so each row is a dictionary hit.
    • Version skew (TypeLoadException, MissingMemberException, FileNotFoundException or FileLoadException, unwrapped through any depth of TargetInvocationException / TypeInitializationException) makes the module count as absent. Both lists are read inside that guard, so a missing member in the table's constructor, static initializer or list getters is covered.
  • Serialization constructor, per row: built-in table, then module table, then reflection (switch on) or SerializationSetup/NotBuiltIn (switch off). With the switch on, the module arm swaps reflection for direct construction that gives the same Type and serializer class, so JIT behavior does not change.
  • Matching follows what Type.GetType accepts when called from Akka.dll: full type name plus assembly simple name, case-insensitive. A bare name matches only Akka.dll and framework types (so "Akka.Actor.Identify" works with the switch off as it does with it on, per AOT M1-G: one matching rule for every built-in type name #8613); a bare name for a type in another module is rejected, as reflection rejects it. A framework type (System.String) also matches any of the existing FrameworkAssemblyNames.
  • Binding rows whose type lives outside the module (Remote.conf's "System.String", "Akka.Actor.Identify, Akka", "Google.Protobuf.IMessage, Google.Protobuf") resolve against the modules this config's serializer rows loaded. Two modules that list the same name list the same Type, so the order does not matter.
  • One small widening: a binding row whose assembly half names a known module resolves through that module's table even when the module's serializer rows are absent from the config.
  • Internal ctor Serialization(ExtendedActorSystem, ModuleSerializerTable) for tests. The public ctor uses ModuleSerializerTable.Default.

No public API change and no ledger entry.

Tests

ModuleSerializersSpec (in DynamicTypeLoadingCollection) uses a fake module keyed as Akka.Tests:

  • switch off: the module's serializer row and its binding rows (module type, System.String, Akka.Actor.Identify, Akka) resolve, with one probe;
  • an application.conf override of a module type (and of System.String) to bytes wins;
  • a non-module serializer row and a non-module binding row still throw NotBuiltIn;
  • strict matching: the module's type under another assembly, or as a bare name, throws;
  • a bare Akka.dll name ("Akka.Actor.PoisonPill") that a loaded module binds resolves, matching reflection;
  • with no module serializer row, "System.String" = bytes still throws NotBuiltIn with the switch off, and nothing is probed;
  • a SerializationSetup-covered alias is still skipped;
  • switch on: same serializer class and same constructor choice (with and without a settings block) as reflection;
  • version skew: a table whose constructor throws MissingMethodException, one whose static initializer does, and a table type that does not exist all count as absent: reflection works with the switch on, and the switch-off error is the ordinary NotBuiltIn;
  • a default config never probes, with the switch on or off.

Validation

  • dotnet build -c Release -warnaserror for Akka and Akka.Tests
  • Akka.Tests: Serialization + Util + BuiltIn filters, and the full suite
  • Akka.API.Tests: no change
  • AOT canary: unrooted publish and run print [canary] OK with the same 4 IL warnings; the rooted publish passes CheckAotWarnings with no new baseline entries

Serialization now resolves a HOCON serializer or binding row that misses
core's built-in tables through the owning module's ModuleSerializers
table before falling back to reflection (switch on) or NotBuiltIn
(switch off). Modules load lazily by assembly simple name through a
DynamicallyAccessedMembers-annotated literal, are cached per assembly,
and count as absent on version skew. Matching is strict on full name
plus assembly. The first-party map is empty; later PRs add Remote,
the Cluster family and Persistence.
@Aaronontheweb Aaronontheweb added serialization AOT Ahead-of-Time (AOT) Compilation labels Sep 25, 2026
- Unwrap nested TargetInvocationException/TypeInitializationException, so
  a missing member in a module table's static initializer counts as skew.
- Read Serializers and BoundTypes inside the guarded load and precompute
  a per-module lookup keyed by stripped full name.
- Accept a bare Akka.dll type name, as Type.GetType from Akka.dll does.
- Tests: static-initializer skew, bare Akka.dll name, System.String row
  with no module serializer row still throws.
- Precompute an IsAkka flag per entry instead of passing the Type to Accepts.
- FindModuleBoundType: plain loop, owner module first, no LINQ allocation.
- ModuleSerializer doc: null skips the alias as a safety net.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - this just adds the infrastructure for supporting AOT-compatible built-in serializer loading. The actual work on a per-module basis will happen downstream.

internal sealed class ModuleSerializerTable
{
/// <summary>The process-wide table behind the public <see cref="Serialization"/> constructor.</summary>
internal static readonly ModuleSerializerTable Default = new(new Dictionary<string, Func<ModuleSerializers?>>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is getting populated in a follow-up PR

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) September 28, 2026 18:57
@Aaronontheweb
Aaronontheweb merged commit 1e4a82a into akkadotnet:dev Sep 28, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AOT Ahead-of-Time (AOT) Compilation serialization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant