Skip to content

Commit

Permalink
[XC] only generate the services that'll be used
Browse files Browse the repository at this point in the history
MarkupExtensions, ValueProviders and some TypeConverters require a
serviceProvider. Only add the services that are actually required. This
will reduce IL size, time to JIT, number of allocations, ...

Also add a warning when users do not attribute their MarkupExctensions

- fixes #19650
  • Loading branch information
StephaneDelcroix committed Jan 19, 2024
1 parent 02406b9 commit 3ba2adc
Show file tree
Hide file tree
Showing 44 changed files with 313 additions and 59 deletions.
1 change: 1 addition & 0 deletions src/Controls/src/Build.Tasks/BuildException.cs
Expand Up @@ -80,6 +80,7 @@ class BuildExceptionCode
public static BuildExceptionCode XStaticSyntax = new BuildExceptionCode("XFC", 0100, nameof(XStaticSyntax), "");
public static BuildExceptionCode XStaticResolution = new BuildExceptionCode("XFC", 0101, nameof(XStaticResolution), "");
public static BuildExceptionCode XDataTypeSyntax = new BuildExceptionCode("XFC", 0102, nameof(XDataTypeSyntax), "");
public static BuildExceptionCode UnattributedMarkupType = new BuildExceptionCode("XC", 0103, nameof(UnattributedMarkupType), ""); //warning

//Style, StyleSheets, Resources
public static BuildExceptionCode StyleSheetSourceOrContent = new BuildExceptionCode("XFC", 0120, nameof(StyleSheetSourceOrContent), "");
Expand Down
Expand Up @@ -38,7 +38,7 @@ public IEnumerable<Instruction> ProvideValue(VariableDefinitionReference vardefr
yield return instruction;

//push the value
foreach (var instruction in ((ValueNode)valueNode).PushConvertedValue(context, bpRef, valueNode.PushServiceProvider(context, bpRef: bpRef), boxValueTypes: true, unboxValueTypes: false))
foreach (var instruction in ((ValueNode)valueNode).PushConvertedValue(context, bpRef, (requiredServices) => valueNode.PushServiceProvider(context, requiredServices, bpRef: bpRef), boxValueTypes: true, unboxValueTypes: false))
yield return instruction;

//set the value
Expand Down
9 changes: 6 additions & 3 deletions src/Controls/src/Build.Tasks/CreateObjectVisitor.cs
Expand Up @@ -172,7 +172,8 @@ public void Visit(ElementNode node, INode parentNode)
{
//<Color>Purple</Color>
Context.IL.Append(vnode.PushConvertedValue(Context, typeref, new ICustomAttributeProvider[] { typedef },
node.PushServiceProvider(Context), false, true));
(requiredServices) => node.PushServiceProvider(Context, requiredServices),
false, true));
Context.IL.Emit(OpCodes.Stloc, vardef);
}
else if (node.CollectionItems.Count == 1 && (vnode = node.CollectionItems.First() as ValueNode) != null &&
Expand Down Expand Up @@ -309,7 +310,8 @@ IEnumerable<Instruction> PushCtorArguments(MethodReference ctorinfo, ElementNode
foreach (var instruction in vnode.PushConvertedValue(Context,
parameter.ParameterType,
new ICustomAttributeProvider[] { parameter, parameter.ParameterType.ResolveCached(Context.Cache) },
enode.PushServiceProvider(Context), false, true))
(requiredServices) => enode.PushServiceProvider(Context, requiredServices),
false, true))
yield return instruction;
}
}
Expand Down Expand Up @@ -346,7 +348,8 @@ IEnumerable<Instruction> PushCtorXArguments(MethodReference factoryCtorInfo, Ele
foreach (var instruction in vnode.PushConvertedValue(Context,
parameter.ParameterType,
new ICustomAttributeProvider[] { parameter, parameter.ParameterType.ResolveCached(Context.Cache) },
enode.PushServiceProvider(Context), false, true))
(requiredServices) => enode.PushServiceProvider(Context, requiredServices),
false, true))
yield return instruction;
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/Controls/src/Build.Tasks/ErrorMessages.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Controls/src/Build.Tasks/ErrorMessages.resx
Expand Up @@ -243,6 +243,11 @@
<data name="XDataTypeSyntax" xml:space="preserve">
<value>x:DataType expects a string literal, an {{x:Type}} markup or {{x:Nul}l}.</value>
</data>
<data name="UnattributedMarkupType" xml:space="preserve">
<value>Consider attributing the markup extension "{0}" with [RequireService] or [AcceptEmptyServiceProvider] if it doesn't require any.</value>
<comment>0 is type name</comment>
</data>

<data name="XmlnsUndeclared" xml:space="preserve">
<value>Undeclared xmlns prefix "{0}".</value>
<comment>0 is the xmlns prefix</comment>
Expand Down
103 changes: 66 additions & 37 deletions src/Controls/src/Build.Tasks/NodeILExtensions.cs
Expand Up @@ -98,7 +98,7 @@ public static bool CanConvertValue(this ValueNode node, ILContext context, TypeR

public static IEnumerable<Instruction> PushConvertedValue(this ValueNode node, ILContext context,
TypeReference targetTypeRef, IEnumerable<ICustomAttributeProvider> attributeProviders,
IEnumerable<Instruction> pushServiceProvider, bool boxValueTypes, bool unboxValueTypes)
Func<TypeReference[],IEnumerable<Instruction>> pushServiceProvider, bool boxValueTypes, bool unboxValueTypes)
{
TypeReference typeConverter = null;
foreach (var attributeProvider in attributeProviders)
Expand All @@ -119,7 +119,7 @@ public static bool CanConvertValue(this ValueNode node, ILContext context, TypeR
}

public static IEnumerable<Instruction> PushConvertedValue(this ValueNode node, ILContext context, FieldReference bpRef,
IEnumerable<Instruction> pushServiceProvider, bool boxValueTypes, bool unboxValueTypes)
Func<TypeReference[],IEnumerable<Instruction>> pushServiceProvider, bool boxValueTypes, bool unboxValueTypes)
{
var module = context.Body.Method.Module;
var targetTypeRef = bpRef.GetBindablePropertyType(context.Cache, node, module);
Expand All @@ -142,7 +142,7 @@ static T TryFormat<T>(Func<string, T> func, IXmlLineInfo lineInfo, string str)
}

public static IEnumerable<Instruction> PushConvertedValue(this ValueNode node, ILContext context,
TypeReference targetTypeRef, TypeReference typeConverter, IEnumerable<Instruction> pushServiceProvider,
TypeReference targetTypeRef, TypeReference typeConverter, Func<TypeReference[],IEnumerable<Instruction>> pushServiceProvider,
bool boxValueTypes, bool unboxValueTypes)
{
var module = context.Body.Method.Module;
Expand Down Expand Up @@ -197,7 +197,10 @@ static T TryFormat<T>(Func<string, T> func, IXmlLineInfo lineInfo, string str)

if (isExtendedConverter)
{
foreach (var instruction in pushServiceProvider)
var requireServiceAttribute = typeConverter.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "RequireServiceAttribute"));
var requiredServiceType = (requireServiceAttribute?.ConstructorArguments[0].Value as CustomAttributeArgument[])?.Select(ca => ca.Value as TypeReference).ToArray();

foreach (var instruction in pushServiceProvider(requiredServiceType))
yield return instruction;
}

Expand Down Expand Up @@ -583,14 +586,12 @@ static IEnumerable<Instruction> PushNamescopes(INode node, ILContext context, Mo
}
}

public static IEnumerable<Instruction> PushServiceProvider(this INode node, ILContext context, FieldReference bpRef = null, PropertyReference propertyRef = null, TypeReference declaringTypeReference = null)
public static IEnumerable<Instruction> PushServiceProvider(this INode node, ILContext context, TypeReference[] requiredServices, FieldReference bpRef = null, PropertyReference propertyRef = null, TypeReference declaringTypeReference = null)
{
var module = context.Body.Method.Module;

#if NOSERVICEPROVIDER
yield return Instruction.Create (OpCodes.Ldnull);
yield break;
#endif
var createAllServices = requiredServices is null;
var alreadyContainsProvideValueTarget = false;

var addService = module.ImportMethodReference(context.Cache, ("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml.Internals", "XamlServiceProvider"),
methodName: "Add",
Expand All @@ -601,43 +602,69 @@ public static IEnumerable<Instruction> PushServiceProvider(this INode node, ILCo

yield return Create(Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml.Internals", "XamlServiceProvider"), parameterTypes: null));

//Add a SimpleValueTargetProvider and register it as IProvideValueTarget and IReferenceProvider
var pushParentIl = node.PushParentObjectsArray(context).ToList();
if (pushParentIl[pushParentIl.Count - 1].OpCode != Ldnull)
//Add a SimpleValueTargetProvider and register it as IProvideValueTarget, IReferenceProvider and IProvideParentValues
if ( createAllServices
|| requiredServices.Contains(module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IProvideParentValues")), TypeRefComparer.Default)
|| requiredServices.Contains(module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IReferenceProvider")), TypeRefComparer.Default))
{
yield return Create(Dup); //Keep the serviceProvider on the stack
yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IProvideValueTarget")));
yield return Create(Call, module.ImportMethodReference(context.Cache, ("mscorlib", "System", "Type"), methodName: "GetTypeFromHandle", parameterTypes: new[] { ("mscorlib", "System", "RuntimeTypeHandle") }, isStatic: true));

foreach (var instruction in pushParentIl)
yield return instruction;
alreadyContainsProvideValueTarget = true;
var pushParentIl = node.PushParentObjectsArray(context).ToList();
if (pushParentIl[pushParentIl.Count - 1].OpCode != Ldnull)
{
yield return Create(Dup); //Keep the serviceProvider on the stack
yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IProvideValueTarget")));
yield return Create(Call, module.ImportMethodReference(context.Cache, ("mscorlib", "System", "Type"), methodName: "GetTypeFromHandle", parameterTypes: new[] { ("mscorlib", "System", "RuntimeTypeHandle") }, isStatic: true));

foreach (var instruction in PushTargetProperty(context, bpRef, propertyRef, declaringTypeReference, module))
yield return instruction;
foreach (var instruction in pushParentIl)
yield return instruction;

foreach (var instruction in PushNamescopes(node, context, module))
yield return instruction;
foreach (var instruction in PushTargetProperty(context, bpRef, propertyRef, declaringTypeReference, module))
yield return instruction;

yield return Create(Ldc_I4_0); //don't ask
yield return Create(Newobj, module.ImportCtorReference(context.Cache,
("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml.Internals", "SimpleValueTargetProvider"), paramCount: 4));
foreach (var instruction in PushNamescopes(node, context, module))
yield return instruction;

//store the provider so we can register it again with a different key
yield return Create(Dup);
var refProvider = new VariableDefinition(module.ImportReference(context.Cache, ("mscorlib", "System", "Object")));
context.Body.Variables.Add(refProvider);
yield return Create(Stloc, refProvider);
yield return Create(Callvirt, addService);
yield return Create(Ldc_I4_0); //don't ask
yield return Create(Newobj, module.ImportCtorReference(context.Cache,
("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml.Internals", "SimpleValueTargetProvider"), paramCount: 4));

//store the provider so we can register it again with a different key
yield return Create(Dup);
var refProvider = new VariableDefinition(module.ImportReference(context.Cache, ("mscorlib", "System", "Object")));
context.Body.Variables.Add(refProvider);
yield return Create(Stloc, refProvider);
yield return Create(Callvirt, addService);

yield return Create(Dup); //Keep the serviceProvider on the stack
yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IReferenceProvider")));
yield return Create(Call, module.ImportMethodReference(context.Cache, ("mscorlib", "System", "Type"), methodName: "GetTypeFromHandle", parameterTypes: new[] { ("mscorlib", "System", "RuntimeTypeHandle") }, isStatic: true));
yield return Create(Ldloc, refProvider);
yield return Create(Callvirt, addService);
}
}

//Add an even simpler ValueTargetProvider and register it as IProvideValueTarget
if (!alreadyContainsProvideValueTarget && requiredServices.Contains(module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IProvideValueTarget")), TypeRefComparer.Default))
{
yield return Create(Dup); //Keep the serviceProvider on the stack
yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IReferenceProvider")));
yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IProvideValueTarget")));
yield return Create(Call, module.ImportMethodReference(context.Cache, ("mscorlib", "System", "Type"), methodName: "GetTypeFromHandle", parameterTypes: new[] { ("mscorlib", "System", "RuntimeTypeHandle") }, isStatic: true));
yield return Create(Ldloc, refProvider);

if (node.Parent is IElementNode elementNode)
foreach (var instruction in context.Variables[elementNode].LoadAs(context.Cache, module.TypeSystem.Object, module))
yield return instruction;
else
yield return Create(Ldnull);

foreach (var instruction in PushTargetProperty(context, bpRef, propertyRef, declaringTypeReference, module))
yield return instruction;

yield return Create(Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml.Internals", "ValueTargetProvider"), paramCount: 2));
yield return Create(Callvirt, addService);
}

//Add a XamlTypeResolver
if (node.NamespaceResolver != null)
//Add a IXamlTypeResolver
if (node.NamespaceResolver != null && createAllServices || requiredServices.Contains(module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IXamlTypeResolver")), TypeRefComparer.Default))
{
yield return Create(Dup); //Duplicate the serviceProvider
yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IXamlTypeResolver")));
Expand All @@ -662,7 +689,7 @@ public static IEnumerable<Instruction> PushServiceProvider(this INode node, ILCo
yield return Create(Callvirt, addService);
}

if (node is IXmlLineInfo)
if (node is IXmlLineInfo && createAllServices || requiredServices.Contains(module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IXmlLineInfoProvider")), TypeRefComparer.Default))
{
yield return Create(Dup); //Duplicate the serviceProvider
yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IXmlLineInfoProvider")));
Expand All @@ -672,6 +699,8 @@ public static IEnumerable<Instruction> PushServiceProvider(this INode node, ILCo
yield return Create(Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml.Internals", "XmlLineInfoProvider"), parameterTypes: new[] { ("System.Xml.ReaderWriter", "System.Xml", "IXmlLineInfo") }));
yield return Create(Callvirt, addService);
}

//and... we end up with the serviceProvider on the stack, as expected
}
}
}
}

0 comments on commit 3ba2adc

Please sign in to comment.