diff --git a/src/Controls/src/Build.Tasks/Controls.Build.Tasks.csproj b/src/Controls/src/Build.Tasks/Controls.Build.Tasks.csproj index 811be6cbc4ab..579b3db17044 100644 --- a/src/Controls/src/Build.Tasks/Controls.Build.Tasks.csproj +++ b/src/Controls/src/Build.Tasks/Controls.Build.Tasks.csproj @@ -22,6 +22,11 @@ .NET Multi-platform App UI (.NET MAUI) is a cross-platform framework for creating native mobile and desktop apps with C# and XAML. This package only contains the MSBuild tasks and other tooling. Please install the Microsoft.Maui.Controls package to start using .NET MAUI. + + + + + @@ -34,6 +39,7 @@ + @@ -50,6 +56,10 @@ + + + + diff --git a/src/Controls/src/Build.Tasks/MibcProfileGenerator/MibcProfileGenerator.csproj b/src/Controls/src/Build.Tasks/MibcProfileGenerator/MibcProfileGenerator.csproj new file mode 100644 index 000000000000..fd9f0aff080d --- /dev/null +++ b/src/Controls/src/Build.Tasks/MibcProfileGenerator/MibcProfileGenerator.csproj @@ -0,0 +1,32 @@ + + + + Exe + net10.0 + MibcProfileGenerator + Microsoft.Maui.Controls.Build.Tasks + enable + enable + false + false + + + + Generates MIBC profile files listing InitializeComponent methods from compiled MAUI assemblies. + + + + + + + + + <_CopyItems Include="$(TargetDir)MibcProfileGenerator.dll" /> + <_CopyItems Include="$(TargetDir)MibcProfileGenerator.pdb" /> + <_CopyItems Include="$(TargetDir)MibcProfileGenerator.runtimeconfig.json" /> + <_CopyItems Include="$(TargetDir)MibcProfileGenerator.deps.json" /> + + + + + diff --git a/src/Controls/src/Build.Tasks/MibcProfileGenerator/Program.cs b/src/Controls/src/Build.Tasks/MibcProfileGenerator/Program.cs new file mode 100644 index 000000000000..c1c629e70a98 --- /dev/null +++ b/src/Controls/src/Build.Tasks/MibcProfileGenerator/Program.cs @@ -0,0 +1,458 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Compression; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Maui.Controls.Build.Tasks; + +/// +/// Generates a MIBC (Managed Instrumented Binary Code) profile file that lists all +/// InitializeComponent* methods found in one or more input assemblies. MIBC files are +/// consumed by crossgen2 and the .NET AOT compiler to guide profile-guided optimization. +/// +/// The MAUI XAML processing pipeline generates several variants of InitializeComponent: +/// - InitializeComponent — the primary entry point (all inflator modes) +/// - InitializeComponentRuntime — runtime XAML inflation via LoadFromXaml (switch mode / HotReload fallback) +/// - InitializeComponentXamlC — XamlC IL-compiled XAML inflation (switch mode) +/// - InitializeComponentSourceGen — source-generator compiled XAML inflation (switch mode) +/// All of these are included in the generated MIBC profile. +/// +static class MibcProfileGenerator +{ + /// + /// Method name prefixes generated by the MAUI XAML toolchain. + /// See CodeBehindCodeWriter.cs and XamlCTask.cs for where these names are produced. + /// + static readonly string[] InitializeComponentMethodNames = + [ + "InitializeComponent", + "InitializeComponentRuntime", + "InitializeComponentXamlC", + "InitializeComponentSourceGen", + ]; + + static int Main(string[] args) + { + if (args.Length < 2) + { + Console.Error.WriteLine("Usage: MibcProfileGenerator [ ...]"); + Console.Error.WriteLine(); + Console.Error.WriteLine("Scans input assemblies for InitializeComponent* methods and produces"); + Console.Error.WriteLine("a MIBC profile file that can be consumed by crossgen2 (--mibc) or"); + Console.Error.WriteLine("validated with 'dotnet pgo dump '."); + Console.Error.WriteLine(); + Console.Error.WriteLine("Matched method names:"); + foreach (var name in InitializeComponentMethodNames) + Console.Error.WriteLine($" - {name}"); + return 1; + } + + string outputPath = args[0]; + string[] inputPaths = args.Skip(1).ToArray(); + bool uncompressed = string.Equals(Path.GetExtension(outputPath), ".dll", StringComparison.OrdinalIgnoreCase); + + var methods = new List(); + + foreach (string inputPath in inputPaths) + { + if (!File.Exists(inputPath)) + { + Console.Error.WriteLine($"Error: Input file not found: {inputPath}"); + return 1; + } + + try + { + var discovered = DiscoverInitializeComponentMethods(inputPath); + methods.AddRange(discovered); + Console.WriteLine($"Found {discovered.Count} InitializeComponent* method(s) in {Path.GetFileName(inputPath)}"); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error reading {inputPath}: {ex.Message}"); + return 1; + } + } + + if (methods.Count == 0) + { + Console.Error.WriteLine("Warning: No InitializeComponent* methods found in any input assembly."); + } + + Console.WriteLine($"Total: {methods.Count} method(s) to emit into MIBC profile."); + + try + { + EmitMibcFile(outputPath, methods, uncompressed); + Console.WriteLine($"Successfully wrote MIBC profile to {outputPath}"); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error writing MIBC file: {ex.Message}"); + return 1; + } + + return 0; + } + + /// + /// Represents a method discovered in an input assembly that should be included in the MIBC profile. + /// + sealed record DiscoveredMethod( + string AssemblyName, + Version? AssemblyVersion, + byte[]? PublicKeyToken, + string Namespace, + string TypeName, + string MethodName, + byte[] Signature); + + /// + /// Scans an assembly for all InitializeComponent* methods generated by the MAUI XAML toolchain. + /// + static List DiscoverInitializeComponentMethods(string assemblyPath) + { + var results = new List(); + var matchedNames = new HashSet(InitializeComponentMethodNames); + + using var stream = File.OpenRead(assemblyPath); + using var peReader = new PEReader(stream); + var mdReader = peReader.GetMetadataReader(); + + // Get assembly identity + var assemblyDef = mdReader.GetAssemblyDefinition(); + string asmName = mdReader.GetString(assemblyDef.Name); + Version? asmVersion = assemblyDef.Version; + byte[]? publicKeyToken = GetPublicKeyToken(mdReader, assemblyDef); + + foreach (var typeHandle in mdReader.TypeDefinitions) + { + var typeDef = mdReader.GetTypeDefinition(typeHandle); + string typeName = mdReader.GetString(typeDef.Name); + + // In ECMA-335, nested types have an empty Namespace; only the outermost + // declaring type carries the real namespace. Walk up to find it. + string ns = GetDeclaringNamespace(mdReader, typeDef); + + // Handle nested types: walk up to build the full name with '/' separators + string fullTypeName = BuildFullTypeName(mdReader, typeDef, typeName); + + foreach (var methodHandle in typeDef.GetMethods()) + { + var methodDef = mdReader.GetMethodDefinition(methodHandle); + string methodName = mdReader.GetString(methodDef.Name); + + if (!matchedNames.Contains(methodName)) + continue; + + // Read the raw signature blob + var sigBlob = mdReader.GetBlobBytes(methodDef.Signature); + + results.Add(new DiscoveredMethod( + asmName, + asmVersion, + publicKeyToken, + ns, + fullTypeName, + methodName, + sigBlob)); + } + } + + return results; + } + + /// + /// Gets the namespace for a type, walking up to the outermost declaring type for nested types. + /// In ECMA-335 metadata, nested TypeDefs have an empty Namespace field. + /// + static string GetDeclaringNamespace(MetadataReader mdReader, TypeDefinition typeDef) + { + var current = typeDef; + while (current.IsNested) + { + current = mdReader.GetTypeDefinition(current.GetDeclaringType()); + } + return mdReader.GetString(current.Namespace); + } + + /// + /// Builds the full type name including enclosing types for nested types, using '/' as separator. + /// + static string BuildFullTypeName(MetadataReader mdReader, TypeDefinition typeDef, string simpleName) + { + if (!typeDef.IsNested) + return simpleName; + + var parts = new Stack(); + parts.Push(simpleName); + + var current = typeDef; + while (current.IsNested) + { + var declaringHandle = current.GetDeclaringType(); + current = mdReader.GetTypeDefinition(declaringHandle); + parts.Push(mdReader.GetString(current.Name)); + } + + return string.Join("/", parts); + } + + /// + /// Extracts the public key token from an assembly definition. + /// + static byte[]? GetPublicKeyToken(MetadataReader mdReader, AssemblyDefinition asmDef) + { + if (asmDef.PublicKey.IsNil) + return null; + + byte[] publicKey = mdReader.GetBlobBytes(asmDef.PublicKey); + if (publicKey.Length == 0) + return null; + + // If this is a full public key, convert to token via AssemblyName + if (publicKey.Length > 8) + { + var an = new AssemblyName(); + an.SetPublicKey(publicKey); + return an.GetPublicKeyToken(); + } + + return publicKey; + } + + /// + /// Emits a MIBC profile file in the standard format expected by crossgen2 / dotnet-pgo. + /// + /// The MIBC format is a PE assembly containing: + /// 1. A global method "AssemblyDictionary" that indexes groups by assembly name + /// 2. Per-assembly group methods that list profiled methods via ldtoken instructions + /// + /// For each profiled method the group method contains: + /// ldtoken [MemberRef for the method] + /// pop + /// + static void EmitMibcFile(string outputPath, List methods, bool uncompressed) + { + string assemblyName = Path.GetFileNameWithoutExtension(outputPath); + + var mdBuilder = new MetadataBuilder(); + var ilBuilder = new BlobBuilder(); + var methodBodyStream = new MethodBodyStreamEncoder(ilBuilder); + + // Module and assembly definition + var mvidBlob = mdBuilder.ReserveGuid(); + mdBuilder.AddModule( + 0, + mdBuilder.GetOrAddString(assemblyName), + mvidBlob.Handle, + default, default); + + mdBuilder.AddAssembly( + mdBuilder.GetOrAddString(assemblyName), + new Version(1, 0, 0, 0), + default, + default, + default, + AssemblyHashAlgorithm.None); + + // type definition (required for global methods) + mdBuilder.AddTypeDefinition( + default, + default, + mdBuilder.GetOrAddString(""), + baseType: default, + fieldList: MetadataTokens.FieldDefinitionHandle(1), + methodList: MetadataTokens.MethodDefinitionHandle(1)); + + // Build the void() static method signature for global methods + var staticVoidSigBlob = new BlobBuilder(); + new BlobEncoder(staticVoidSigBlob) + .MethodSignature(isInstanceMethod: false) + .Parameters(0, r => r.Void(), _ => { }); + var staticVoidSigHandle = mdBuilder.GetOrAddBlob(staticVoidSigBlob); + + // Cache of assembly references to avoid duplicates + var assemblyRefCache = new Dictionary(); + + // Cache of type references + var typeRefCache = new Dictionary(); + + // Create MemberRefs for all discovered methods and group by assembly + var groupedByAssembly = methods.GroupBy(m => m.AssemblyName).OrderBy(g => g.Key); + var groupMethods = new List<(string GroupName, MethodDefinitionHandle Handle)>(); + + int groupIndex = 0; + foreach (var group in groupedByAssembly) + { + groupIndex++; + string groupName = group.Key + ";"; + + // Emit one group method body with ldtoken/pop for each method + var groupIlBuf = new BlobBuilder(); + var groupIl = new InstructionEncoder(groupIlBuf); + + foreach (var method in group) + { + // Ensure assembly reference exists + if (!assemblyRefCache.TryGetValue(method.AssemblyName, out var asmRef)) + { + AssemblyFlags flags = default; + BlobHandle pktBlob = default; + if (method.PublicKeyToken is { Length: > 0 }) + { + pktBlob = mdBuilder.GetOrAddBlob(method.PublicKeyToken); + } + + asmRef = mdBuilder.AddAssemblyReference( + mdBuilder.GetOrAddString(method.AssemblyName), + method.AssemblyVersion ?? new Version(0, 0, 0, 0), + default, + pktBlob, + flags, + default); + assemblyRefCache[method.AssemblyName] = asmRef; + } + + // Build type reference (handle nested types) + var typeRef = GetOrAddTypeRef(mdBuilder, typeRefCache, asmRef, method.Namespace, method.TypeName); + + // Create MemberRef for the method with its original signature + var memberRef = mdBuilder.AddMemberReference( + typeRef, + mdBuilder.GetOrAddString(method.MethodName), + mdBuilder.GetOrAddBlob(method.Signature)); + + // Emit: ldtoken , pop + groupIl.OpCode(ILOpCode.Ldtoken); + groupIl.Token(memberRef); + groupIl.OpCode(ILOpCode.Pop); + } + + groupIl.OpCode(ILOpCode.Ret); + + // Add the group method as a global method + int bodyOffset = methodBodyStream.AddMethodBody(groupIl, maxStack: 8); + string methodName = $"Assemblies_{group.Key}_{groupIndex}"; + var groupMethodHandle = mdBuilder.AddMethodDefinition( + MethodAttributes.Public | MethodAttributes.Static, + MethodImplAttributes.IL, + mdBuilder.GetOrAddString(methodName), + staticVoidSigHandle, + bodyOffset, + default); + + groupMethods.Add((groupName, groupMethodHandle)); + } + + // Emit AssemblyDictionary global method + var dictIlBuf = new BlobBuilder(); + var dictIl = new InstructionEncoder(dictIlBuf); + + foreach (var (groupName, groupMethodHandle) in groupMethods) + { + dictIl.LoadString(mdBuilder.GetOrAddUserString(groupName)); + dictIl.OpCode(ILOpCode.Ldtoken); + dictIl.Token(groupMethodHandle); + dictIl.OpCode(ILOpCode.Pop); + } + + dictIl.OpCode(ILOpCode.Ret); + + int dictBodyOffset = methodBodyStream.AddMethodBody(dictIl, maxStack: 8); + mdBuilder.AddMethodDefinition( + MethodAttributes.Public | MethodAttributes.Static, + MethodImplAttributes.IL, + mdBuilder.GetOrAddString("AssemblyDictionary"), + staticVoidSigHandle, + dictBodyOffset, + default); + + // Serialize to PE + var peBuilder = new ManagedPEBuilder( + new PEHeaderBuilder(), + new MetadataRootBuilder(mdBuilder), + ilBuilder, + deterministicIdProvider: content => new BlobContentId( + new Guid("97F4DBD4-F6D1-4FAD-91B3-1001F92068E5"), 0x04030201)); + + var peBlob = new BlobBuilder(); + peBuilder.Serialize(peBlob); + + // Write the MVID + new BlobWriter(mvidBlob.Content).WriteGuid( + new Guid("97F4DBD4-F6D1-4FAD-91B3-1001F92068E5")); + + // Write output + if (uncompressed) + { + using var fs = new FileStream(outputPath, FileMode.Create); + peBlob.WriteContentTo(fs); + } + else + { + using var zip = ZipFile.Open(outputPath, ZipArchiveMode.Create); + var entry = zip.CreateEntry(Path.GetFileName(outputPath) + ".dll", CompressionLevel.Optimal); + using var entryStream = entry.Open(); + peBlob.WriteContentTo(entryStream); + } + } + + /// + /// Gets or creates a TypeReference handle, handling nested types (separated by '/'). + /// + static TypeReferenceHandle GetOrAddTypeRef( + MetadataBuilder mdBuilder, + Dictionary cache, + AssemblyReferenceHandle asmRef, + string ns, + string typeName) + { + string cacheKey = $"{MetadataTokens.GetToken(asmRef):X}:{ns}.{typeName}"; + if (cache.TryGetValue(cacheKey, out var existing)) + return existing; + + // Handle nested types: "Outer/Inner" -> TypeRef for Outer, then nested TypeRef for Inner + string[] parts = typeName.Split('/'); + TypeReferenceHandle parentTypeRef = default; + + for (int i = 0; i < parts.Length; i++) + { + string partName = parts[i]; + string partKey; + + if (i == 0) + { + partKey = $"{MetadataTokens.GetToken(asmRef):X}:{ns}.{partName}"; + if (!cache.TryGetValue(partKey, out parentTypeRef)) + { + parentTypeRef = mdBuilder.AddTypeReference( + asmRef, + mdBuilder.GetOrAddString(ns), + mdBuilder.GetOrAddString(partName)); + cache[partKey] = parentTypeRef; + } + } + else + { + partKey = $"{MetadataTokens.GetToken(parentTypeRef):X}/{partName}"; + if (!cache.TryGetValue(partKey, out var nestedRef)) + { + nestedRef = mdBuilder.AddTypeReference( + parentTypeRef, + default, + mdBuilder.GetOrAddString(partName)); + cache[partKey] = nestedRef; + } + parentTypeRef = nestedRef; + } + } + + cache[cacheKey] = parentTypeRef; + return parentTypeRef; + } +} diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets index 0059c276f1a6..c4065314da3b 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets @@ -254,6 +254,33 @@ + + <_MauiMibcProfilePath>$(IntermediateOutputPath)$(TargetName).mibc + + + + + + <_MibcProfileGeneratorPath>$(MSBuildThisFileDirectory)MibcProfileGenerator.dll + + + + + + + + + + + + +