AOT cut 1: consumers with the MessagePack generator on can declare CultRecordRef<T> members and keys
Merges hands/cultlib-aot-record-ref (b7a68884..3d4efc89). cultlib-gaps:cut_report:cut-aot-record-ref-generator.h1, Soul verdict s1 holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
51ca81a1d4
9 changed files with 168 additions and 3 deletions
15
CultLib.sln
15
CultLib.sln
|
|
@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath.Tests", "packages\
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.ConsumerBuild", "tests\GameCult.Caching.ConsumerBuild\GameCult.Caching.ConsumerBuild.csproj", "{D3138F3C-9531-4906-A5D3-74BD857D0685}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.GeneratorConsumer", "tests\GameCult.Caching.GeneratorConsumer\GameCult.Caching.GeneratorConsumer.csproj", "{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -275,6 +277,18 @@ Global
|
|||
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -299,5 +313,6 @@ Global
|
|||
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
|
||||
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
|
||||
{D3138F3C-9531-4906-A5D3-74BD857D0685} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
<PackageReference Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageReference Include="MessagePackAnalyzer" Version="3.1.7" PrivateAssets="all" />
|
||||
<InternalsVisibleTo Include="GameCult.Caching.Tests" />
|
||||
<None Include="buildTransitive\GameCult.Caching.MessagePack.targets" Pack="true" PackagePath="buildTransitive\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@ await cache.PullAllBackingStoresAsync();
|
|||
- The raw CultNet document lane should treat these payload bytes as already
|
||||
blessed, not decode and re-encode them for sport.
|
||||
|
||||
## MessagePack source generator
|
||||
|
||||
NuGet consumers get `[assembly: MessagePackAssumedFormattable(typeof(CultRecordRef<>))]` from the package, so a
|
||||
`CultRecordRef<T>` member or dictionary key compiles with MessagePack's source generator on and is formatted by
|
||||
CultLib's resolver; no per-member `[MessagePackFormatter]` is needed. Unity and `ProjectReference` consumers that turn the generator
|
||||
on write that line themselves (or import `buildTransitive/GameCult.Caching.MessagePack.targets`).
|
||||
|
||||
## CultMath Value Shapes
|
||||
|
||||
Every public CultMath value type has one MessagePack shape (`CultMathResolver`,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<Project>
|
||||
<!-- MessagePack's source generator invents a formatter for CultRecordRef<T> instead of asking the resolver chain (MessagePack-CSharp#2133); assuming it formattable leaves the ref to CultDocumentResolver. -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="MessagePack.MessagePackAssumedFormattableAttribute">
|
||||
<_Parameter1>typeof(global::GameCult.Caching.CultRecordRef<>)</_Parameter1>
|
||||
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -161,7 +161,6 @@ namespace GameCult.Geometry
|
|||
public string RequestId { get; set; } = string.Empty;
|
||||
|
||||
[Key(1)]
|
||||
[MessagePackFormatter(typeof(CultRecordRefFormatter<CultGeometryDomainDocument>))]
|
||||
public CultRecordRef<CultGeometryDomainDocument> DomainKey { get; set; }
|
||||
|
||||
[Key(2)]
|
||||
|
|
@ -236,7 +235,6 @@ namespace GameCult.Geometry
|
|||
public string CutId { get; set; } = string.Empty;
|
||||
|
||||
[Key(1)]
|
||||
[MessagePackFormatter(typeof(CultRecordRefFormatter<CultGeometryBuildRequest>))]
|
||||
public CultRecordRef<CultGeometryBuildRequest> RequestKey { get; set; }
|
||||
|
||||
[Key(2)]
|
||||
|
|
@ -325,7 +323,6 @@ namespace GameCult.Geometry
|
|||
public string ChunkId { get; set; } = string.Empty;
|
||||
|
||||
[Key(1)]
|
||||
[MessagePackFormatter(typeof(CultRecordRefFormatter<CultGeometrySelectedCutManifest>))]
|
||||
public CultRecordRef<CultGeometrySelectedCutManifest> CutKey { get; set; }
|
||||
|
||||
[Key(2)]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
<PackageReference Include="System.Text.Json" Version="9.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\GameCult.Caching.MessagePack\buildTransitive\GameCult.Caching.MessagePack.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="ISOSURFACE-PROVENANCE.md" Pack="true" PackagePath="\" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Aot)' == 'true'">
|
||||
<PublishAot>true</PublishAot>
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Aot)' == 'true'">
|
||||
<TrimmerRootAssembly Include="GameCult.Caching.GeneratorConsumer" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\..\src\GameCult.Caching.MessagePack\buildTransitive\GameCult.Caching.MessagePack.targets" />
|
||||
</Project>
|
||||
105
tests/GameCult.Caching.GeneratorConsumer/Program.cs
Normal file
105
tests/GameCult.Caching.GeneratorConsumer/Program.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using MessagePack;
|
||||
|
||||
// The generator-on twin of ConsumerBuild: this project compiling is the first test, and running it checks that the generated
|
||||
// formatters are the ones in use and that the bytes are the ones the dynamic formatter wrote.
|
||||
var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(RefDoc).Assembly);
|
||||
var target = new CultRecordRef<RefTarget>(new CultRecordKey("p"));
|
||||
var documents = new (Type Type, object Value)[]
|
||||
{
|
||||
(typeof(MemberDoc), new MemberDoc { Name = "r", Target = target }),
|
||||
(typeof(KeyedDoc), new KeyedDoc { Name = "r", Standing = { [target] = 0.5f } }),
|
||||
(typeof(RefDoc), new RefDoc { Name = "r", Target = target, Standing = { [target] = 0.5f } }),
|
||||
};
|
||||
|
||||
foreach (var (type, _) in documents)
|
||||
{
|
||||
var formatter = options.Resolver.GetFormatterDynamic(type)!.GetType();
|
||||
if (formatter.Assembly.IsDynamic || !(formatter.FullName ?? "").Contains("GeneratedMessagePackResolver"))
|
||||
return Fail($"{type.Name} is formatted by {formatter.FullName}, not a generated formatter");
|
||||
}
|
||||
|
||||
var aot = !RuntimeFeature.IsDynamicCodeSupported;
|
||||
if (aot)
|
||||
Console.WriteLine("SKIP ref-keyed dictionary under NativeAOT: AOT cut 3 (map A6)");
|
||||
foreach (var (type, expected) in new[] { (typeof(MemberDoc), "92A172A170"), (typeof(RefDoc), "93A172A17081A170CA3F000000") })
|
||||
{
|
||||
if (aot && type != typeof(MemberDoc))
|
||||
continue;
|
||||
var hex = Convert.ToHexString(MessagePackSerializer.Serialize(type, documents.First(d => d.Type == type).Value, options));
|
||||
Console.WriteLine(hex);
|
||||
if (hex != expected)
|
||||
return Fail($"{type.Name} payload is {hex}");
|
||||
}
|
||||
|
||||
foreach (var (type, value) in documents)
|
||||
{
|
||||
if (aot && type != typeof(MemberDoc))
|
||||
continue;
|
||||
var path = Path.Combine(Path.GetTempPath(), "generator-consumer-" + Guid.NewGuid().ToString("N") + ".cc");
|
||||
try
|
||||
{
|
||||
var registry = CultDocumentRegistry.ForTypes(new[] { type, typeof(RefTarget) });
|
||||
using (var cache = new CultCache(registry))
|
||||
{
|
||||
cache.AddBackingStore(new SingleFileMessagePackBackingStore(path));
|
||||
cache.UpsertAsync(type, value, new CultRecordKey("k")).GetAwaiter().GetResult();
|
||||
cache.FlushAllBackingStores();
|
||||
}
|
||||
using var reopened = new CultCache(registry);
|
||||
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path));
|
||||
reopened.PullAllBackingStoresAsync().GetAwaiter().GetResult();
|
||||
var read = reopened.Get(new CultRecordKey("k"));
|
||||
if (read == null || !Convert.ToHexString(MessagePackSerializer.Serialize(type, read, options)).Equals(Convert.ToHexString(MessagePackSerializer.Serialize(type, value, options))))
|
||||
return Fail($"{type.Name} did not round-trip through the store");
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
static int Fail(string message)
|
||||
{
|
||||
Console.Error.WriteLine(message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
[CultDocument("tests.generator_target", "tests.generator_target.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class RefTarget
|
||||
{
|
||||
[Key(0)] [CultName] public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[CultDocument("tests.generator_member", "tests.generator_member.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class MemberDoc
|
||||
{
|
||||
[Key(0)] [CultName] public string Name { get; set; } = string.Empty;
|
||||
[Key(1)] public CultRecordRef<RefTarget> Target { get; set; }
|
||||
}
|
||||
|
||||
[CultDocument("tests.generator_keyed", "tests.generator_keyed.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class KeyedDoc
|
||||
{
|
||||
[Key(0)] [CultName] public string Name { get; set; } = string.Empty;
|
||||
[Key(1)] public Dictionary<CultRecordRef<RefTarget>, float> Standing { get; set; } = new();
|
||||
}
|
||||
|
||||
[CultDocument("tests.generator_both", "tests.generator_both.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class RefDoc
|
||||
{
|
||||
[Key(0)] [CultName] public string Name { get; set; } = string.Empty;
|
||||
[Key(1)] public CultRecordRef<RefTarget> Target { get; set; }
|
||||
[Key(2)] public Dictionary<CultRecordRef<RefTarget>, float> Standing { get; set; } = new();
|
||||
}
|
||||
|
|
@ -35,6 +35,10 @@ Releases as 3.0.0 (major).
|
|||
- `CultElementIdAttribute` declares an element id (derived from a named member when given), `CultForeignRecord` is a record a store carries untouched because no registered type reads it, and `CultElementIdException`, `CultSchemaConflictException`, `CultStoreUnreadableException` and `CultWriteConflictException` are the refusals named above.
|
||||
- A store written by 1.0.x with a record-reference list in a slot still opens (from 2.0.1).
|
||||
|
||||
### Fixed
|
||||
|
||||
- A consumer with MessagePack's source generator on can declare `CultRecordRef<T>` members and `CultRecordRef<T>` dictionary keys. The NuGet package injects the assembly attribute that makes this compile; Unity consumers add one line (see the MessagePack README). Bytes are unchanged.
|
||||
|
||||
## [2.0.1] - 2026-10-22
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
Loading…
Reference in a new issue