Skip to content

Commit

Permalink
Replace wrappers with proxies (#205)
Browse files Browse the repository at this point in the history
The conception of wrappers was that an instance of a type could be
wrapped and the serialize/deserialize implementation would be placed on
the wrapper. Unfortunately the fact that deserialize requires a static
method and serialize does not makes the two significantly different in
implementation. There are also performance issues with relying on static
interfaces.

The new model is structured around "proxy objects" which implement the
driving interfaces, which are now all regular instance methods. A proxy
object can be acquired throught ISerializeProvider and
IDeserializeProvider, which are interfaces with static methods. All user
types now implement ISerializeProvider/IDeserializeProvider and generate
private proxy objects. A user can write the same implementations
manually and achieve the same results.

Attributes for wrappers have also been renamed to "proxy."
  • Loading branch information
agocke authored Dec 14, 2024
1 parent d8bfc43 commit 3b0b7f9
Show file tree
Hide file tree
Showing 328 changed files with 6,858 additions and 5,286 deletions.
2 changes: 1 addition & 1 deletion perf/bench/DataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Benchmarks
{
internal static class DataGenerator
{
public static T GenerateSerialize<T>() where T : Serde.ISerialize<T>
public static T GenerateSerialize<T>() where T : Serde.ISerializeProvider<T>
{
if (typeof(T) == typeof(LoginViewModel))
return (T)(object)CreateLoginViewModel();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@

using System.Text.Json;
using BenchmarkDotNet.Attributes;
using Serde;

namespace Benchmarks
{
[GenericTypeArguments(typeof(LoginViewModel), typeof(LoginViewModel))]
[GenericTypeArguments(typeof(Location), typeof(LocationWrap))]
public class DeserializeFromString<T, U>
where T : Serde.IDeserialize<T>
where U : Serde.IDeserialize<T>
where T : Serde.IDeserializeProvider<T>
where U : Serde.IDeserializeProvider<T>
{
private JsonSerializerOptions _options = null!;
private string value = null!;

private readonly IDeserialize<T> _proxy = T.DeserializeInstance;
private readonly IDeserialize<T> _manualProxy = U.DeserializeInstance;

[GlobalSetup]
public void Setup()
{
Expand All @@ -37,10 +41,10 @@ public void Setup()
}

[Benchmark]
public T SerdeJson() => Serde.Json.JsonSerializer.Deserialize<T>(value);
public T SerdeJson() => Serde.Json.JsonSerializer.Deserialize<T, IDeserialize<T>>(value, _proxy);

[Benchmark]
public T SerdeManual() => Serde.Json.JsonSerializer.Deserialize<T, U>(value);
public T SerdeManual() => Serde.Json.JsonSerializer.Deserialize<T, IDeserialize<T>>(value, _manualProxy);

// DataContractJsonSerializer does not provide an API to serialize to string
// so it's not included here (apples vs apples thing)
Expand Down
2 changes: 1 addition & 1 deletion perf/bench/JsonToString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Benchmarks
[GenericTypeArguments(typeof(LoginViewModel))]
[GenericTypeArguments(typeof(Location))]
[GenericTypeArguments(typeof(Serde.Test.AllInOne))]
public class SerializeToString<T> where T : Serde.ISerialize<T>
public class SerializeToString<T> where T : Serde.ISerializeProvider<T>
{
private JsonSerializerOptions _options = null!;
private T value = default!;
Expand Down
26 changes: 15 additions & 11 deletions perf/bench/SampleTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,28 @@ public partial record Location
};
}

public partial record LocationWrap : IDeserialize<Location>
public partial record LocationWrap : IDeserialize<Location>, IDeserializeProvider<Location>
{
public static LocationWrap Instance { get; } = new();
static IDeserialize<Location> IDeserializeProvider<Location>.DeserializeInstance => Instance;
private LocationWrap() { }

public static ISerdeInfo SerdeInfo { get; } = Serde.SerdeInfo.MakeCustom(
"Location",
typeof(Location).GetCustomAttributesData(),
[
("id", Int32Wrap.SerdeInfo, typeof(Location).GetProperty("Id")!),
("address1", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address1")!),
("address2", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address2")!),
("city", StringWrap.SerdeInfo, typeof(Location).GetProperty("City")!),
("state", StringWrap.SerdeInfo, typeof(Location).GetProperty("State")!),
("postalCode", StringWrap.SerdeInfo, typeof(Location).GetProperty("PostalCode")!),
("name", StringWrap.SerdeInfo, typeof(Location).GetProperty("Name")!),
("phoneNumber", StringWrap.SerdeInfo, typeof(Location).GetProperty("PhoneNumber")!),
("country", StringWrap.SerdeInfo, typeof(Location).GetProperty("Country")!)
("id", Int32Proxy.SerdeInfo, typeof(Location).GetProperty("Id")!),
("address1", StringProxy.SerdeInfo, typeof(Location).GetProperty("Address1")!),
("address2", StringProxy.SerdeInfo, typeof(Location).GetProperty("Address2")!),
("city", StringProxy.SerdeInfo, typeof(Location).GetProperty("City")!),
("state", StringProxy.SerdeInfo, typeof(Location).GetProperty("State")!),
("postalCode", StringProxy.SerdeInfo, typeof(Location).GetProperty("PostalCode")!),
("name", StringProxy.SerdeInfo, typeof(Location).GetProperty("Name")!),
("phoneNumber", StringProxy.SerdeInfo, typeof(Location).GetProperty("PhoneNumber")!),
("country", StringProxy.SerdeInfo, typeof(Location).GetProperty("Country")!)
]);

static Benchmarks.Location Serde.IDeserialize<Benchmarks.Location>.Deserialize(IDeserializer deserializer)
Benchmarks.Location Serde.IDeserialize<Benchmarks.Location>.Deserialize(IDeserializer deserializer)
{
int _l_id = default !;
string _l_address1 = default !;
Expand Down
52 changes: 31 additions & 21 deletions src/generator/Generator.Deserialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,44 @@ namespace Serde
{
internal class DeserializeImplGenerator
{
internal static (MemberDeclarationSyntax[], BaseListSyntax) GenerateDeserializeImpl(
internal static (List<MemberDeclarationSyntax>, BaseListSyntax) GenerateDeserializeImpl(
TypeDeclContext typeDeclContext,
GeneratorExecutionContext context,
ITypeSymbol receiverType,
ImmutableList<ITypeSymbol> inProgress)
ImmutableList<(ITypeSymbol Receiver, ITypeSymbol Containing)> inProgress)
{
var typeFqn = receiverType.ToDisplayString();
TypeSyntax typeSyntax = ParseTypeName(typeFqn);

// `Serde.IDeserialize<'typeName'>
var interfaceSyntax = QualifiedName(IdentifierName("Serde"), GenericName(
Identifier("IDeserialize"),
TypeArgumentList(SeparatedList(new[] { typeSyntax }))
));

// Generate members for ISerialize.Deserialize implementation
MemberDeclarationSyntax[] members;
// Generate members for IDeserialize.Deserialize implementation
var members = new List<MemberDeclarationSyntax>();
List<BaseTypeSyntax> bases = [
// `Serde.IDeserialize<'typeName'>
SimpleBaseType(QualifiedName(IdentifierName("Serde"), GenericName(
Identifier("IDeserialize"),
TypeArgumentList(SeparatedList(new[] { typeSyntax }))
))),
];
if (receiverType.TypeKind == TypeKind.Enum)
{
var method = GenerateEnumDeserializeMethod(receiverType, typeSyntax);
members = [ method ];
// `Serde.IDeserializeProvider<'typeName'>. Enums generate a proxy
bases.Add(SimpleBaseType(ParseTypeName($"Serde.IDeserializeProvider<{typeFqn}>")));

var deserialize = GenerateEnumDeserializeMethod(receiverType, typeSyntax);
members.Add(deserialize);

var deserializeInstance = ParseMemberDeclaration($"""
static IDeserialize<{typeFqn}> IDeserializeProvider<{typeFqn}>.DeserializeInstance
=> {typeFqn}Proxy.Instance;
""")!;
members.Add(deserializeInstance);
}
else
{
var method = GenerateCustomDeserializeMethod(typeDeclContext, context, receiverType, typeSyntax, inProgress);
members = [ method ];
members.Add(method);
}
var baseList = BaseList(SeparatedList(new BaseTypeSyntax[] { SimpleBaseType(interfaceSyntax) }));
var baseList = BaseList(SeparatedList(bases));
return (members, baseList);
}

Expand Down Expand Up @@ -85,9 +95,9 @@ private static MethodDeclarationSyntax GenerateEnumDeserializeMethod(
_ => throw new InvalidOperationException("Too many members in type")
};
var src = $$"""
static {{typeFqn}} IDeserialize<{{typeFqn}}>.Deserialize(IDeserializer deserializer)
{{typeFqn}} IDeserialize<{{typeFqn}}>.Deserialize(IDeserializer deserializer)
{
var serdeInfo = global::Serde.SerdeInfoProvider.GetInfo<{{typeFqn}}Wrap>();
var serdeInfo = global::Serde.SerdeInfoProvider.GetInfo<{{typeFqn}}Proxy>();
var de = deserializer.ReadType(serdeInfo);
int index;
if ((index = de.TryReadIndex(serdeInfo, out var errorName)) == IDeserializeType.IndexNotFound)
Expand Down Expand Up @@ -130,7 +140,7 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
GeneratorExecutionContext context,
ITypeSymbol type,
TypeSyntax typeSyntax,
ImmutableList<ITypeSymbol> inProgress)
ImmutableList<(ITypeSymbol Receiver, ITypeSymbol Containing)> inProgress)
{
Debug.Assert(type.TypeKind != TypeKind.Enum);

Expand All @@ -155,7 +165,7 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
: "_";

var methodText = $$"""
static {{typeFqn}} Serde.IDeserialize<{{typeFqn}}>.Deserialize(IDeserializer deserializer)
{{typeFqn}} Serde.IDeserialize<{{typeFqn}}>.Deserialize(IDeserializer deserializer)
{
{{locals}}
{{assignedVarType}} {{AssignedVarName}} = 0;
Expand Down Expand Up @@ -193,15 +203,15 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
var m = members[fieldIndex];
string wrapperName;
var memberType = m.Type.WithNullableAnnotation(m.NullableAnnotation).ToDisplayString();
if (Wrappers.TryGetExplicitWrapper(m, context, SerdeUsage.Deserialize, inProgress) is { } explicitWrap)
if (Proxies.TryGetExplicitWrapper(m, context, SerdeUsage.Deserialize, inProgress) is { } explicitWrap)
{
wrapperName = explicitWrap.ToString();
}
else if (ImplementsSerde(m.Type, m.Type, context, SerdeUsage.Deserialize))
{
wrapperName = memberType;
}
else if (Wrappers.TryGetImplicitWrapper(m.Type, context, SerdeUsage.Deserialize, inProgress) is { Wrapper: { } wrap })
else if (Proxies.TryGetImplicitWrapper(m.Type, context, SerdeUsage.Deserialize, inProgress) is { Proxy: { } wrap })
{
wrapperName = wrap.ToString();
}
Expand All @@ -213,7 +223,7 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
m.Locations[0],
m.Symbol,
memberType,
"Serde.IDeserialize"));
"Serde.IDeserializeProvider<T>"));
wrapperName = memberType;
}
var localName = GetLocalName(m);
Expand Down
63 changes: 49 additions & 14 deletions src/generator/Generator.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal static void GenerateImpl(
TypeDeclContext typeDeclContext,
ITypeSymbol receiverType,
GeneratorExecutionContext context,
ImmutableList<ITypeSymbol> inProgress)
ImmutableList<(ITypeSymbol Receiver, ITypeSymbol Containing)> inProgress)
{
var typeName = typeDeclContext.Name;

Expand All @@ -41,25 +41,56 @@ internal static void GenerateImpl(

var typeKind = typeDeclContext.Kind;
MemberDeclarationSyntax newType;
var newTypes = new List<MemberDeclarationSyntax>();
if (typeKind == SyntaxKind.EnumDeclaration)
{
var wrapperName = Wrappers.GetWrapperName(typeName);
newType = StructDeclaration(
var proxyName = Proxies.GetProxyName(typeName);
newType = ClassDeclaration(
attributeLists: default,
modifiers: TokenList(Token(SyntaxKind.PartialKeyword)),
keyword: Token(SyntaxKind.StructKeyword),
identifier: Identifier(wrapperName),
modifiers: TokenList(Token(SyntaxKind.SealedKeyword), Token(SyntaxKind.PartialKeyword)),
keyword: Token(SyntaxKind.ClassKeyword),
identifier: Identifier(proxyName),
typeParameterList: default,
baseList: baseList,
constraintClauses: default,
openBraceToken: Token(SyntaxKind.OpenBraceToken),
members: List(implMembers),
closeBraceToken: Token(SyntaxKind.CloseBraceToken),
semicolonToken: default);
typeName = wrapperName;
typeName = proxyName;
}
else
{
var suffix = usage == SerdeUsage.Serialize ? "Serialize" : "Deserialize";
var proxyName = typeName + suffix + "Proxy";

implMembers.Add(ParseMemberDeclaration($$"""
public static readonly {{proxyName}} Instance = new();
""")!
);
implMembers.Add(ParseMemberDeclaration($$"""
private {{proxyName}}() { }
""")!
);

var interfaceName = usage.GetProxyInterfaceName();
MemberDeclarationSyntax proxyType = ClassDeclaration(
attributeLists: default,
modifiers: TokenList([ Token(SyntaxKind.SealedKeyword) ]),
keyword: Token(SyntaxKind.ClassKeyword),
identifier: Identifier(proxyName),
typeParameterList: default,
baseList: baseList,
constraintClauses: default,
openBraceToken: Token(SyntaxKind.OpenBraceToken),
members: List(implMembers),
closeBraceToken: Token(SyntaxKind.CloseBraceToken),
semicolonToken: default);

var member = ParseMemberDeclaration($$"""
static {{interfaceName}}<{{receiverType.ToDisplayString()}}> {{interfaceName}}Provider<{{receiverType.ToDisplayString()}}>.{{suffix}}Instance
=> {{proxyName}}.Instance;
""")!;
newType = TypeDeclaration(
typeKind,
attributes: default,
Expand All @@ -74,26 +105,30 @@ internal static void GenerateImpl(
}),
identifier: Identifier(typeName),
typeParameterList: typeDeclContext.TypeParameterList,
baseList: baseList,
baseList: BaseList(SeparatedList(new BaseTypeSyntax[] {
SimpleBaseType(ParseTypeName($"Serde.{interfaceName}Provider<{receiverType.ToDisplayString()}>"))
})),
constraintClauses: default,
openBraceToken: Token(SyntaxKind.OpenBraceToken),
members: List(implMembers),
members: List([ member, proxyType ]),
closeBraceToken: Token(SyntaxKind.CloseBraceToken),
semicolonToken: default);
}
string fullTypeName = string.Join(".", typeDeclContext.NamespaceNames
.Concat(typeDeclContext.ParentTypeInfo.Select(x => x.Name))
.Concat(new[] { typeName }));

var srcName = fullTypeName + "." + usage.GetInterfaceName();
var srcName = fullTypeName + "." + usage.GetProxyInterfaceName();

newType = typeDeclContext.WrapNewType(newType);

newTypes.Insert(0, newType);

var tree = CompilationUnit(
externs: default,
usings: List(new[] { UsingDirective(IdentifierName("System")), UsingDirective(IdentifierName("Serde")) }),
attributeLists: default,
members: List<MemberDeclarationSyntax>(new[] { newType }));
members: List(newTypes));
tree = tree.NormalizeWhitespace(eol: Utilities.NewLine);

context.AddSource(srcName,
Expand All @@ -110,7 +145,7 @@ internal static (string FileName, string Body) MakePartialDecl(
var typeKind = typeDeclContext.Kind;
string declKeywords;
(typeName, declKeywords) = typeKind == SyntaxKind.EnumDeclaration
? (Wrappers.GetWrapperName(typeName), "struct")
? (Proxies.GetProxyName(typeName), "class")
: (typeName, TypeDeclContext.TypeKindToString(typeKind));
var newType = $$"""
partial {{declKeywords}} {{typeName}}{{typeDeclContext.TypeParameterList}} : Serde.ISerdeInfoProvider
Expand Down Expand Up @@ -150,8 +185,8 @@ internal static bool ImplementsSerde(ITypeSymbol targetType, ITypeSymbol argType
}

var mdName = usage switch {
SerdeUsage.Serialize => "Serde.ISerialize`1",
SerdeUsage.Deserialize => "Serde.IDeserialize`1",
SerdeUsage.Serialize => "Serde.ISerializeProvider`1",
SerdeUsage.Deserialize => "Serde.IDeserializeProvider`1",
_ => throw new ArgumentException("Invalid SerdeUsage", nameof(usage))
};
var serdeSymbol = context.Compilation.GetTypeByMetadataName(mdName)?.Construct(argType);
Expand Down
Loading

0 comments on commit 3b0b7f9

Please sign in to comment.