File: CreateRuntimeRootDescriptorFile.cs
Web Access
Project: src\src\tools\illink\src\ILLink.Tasks\ILLink.Tasks.csproj (ILLink.Tasks)
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.Framework; // ITaskItem
using Microsoft.Build.Utilities; // Task
 
namespace ILLink.Tasks
{
	public class CreateRuntimeRootILLinkDescriptorFile : Task
	{
		/// <summary>
		///   The path to namespace.h.
		/// </summary>
		[Required]
		public ITaskItem NamespaceFilePath { get; set; }
 
		/// <summary>
		///   The path to mscorlib.h.
		/// </summary>
		[Required]
		public ITaskItem MscorlibFilePath { get; set; }
 
		/// <summary>
		///   The path to cortypeinfo.h.
		/// </summary>
		[Required]
		public ITaskItem CortypeFilePath { get; set; }
 
		/// <summary>
		///   The path to rexcep.h.
		/// </summary>
		[Required]
		public ITaskItem RexcepFilePath { get; set; }
 
		/// <summary>
		///   The path to ILLinkTrim.xml.
		/// </summary>
		[Required]
		public ITaskItem ILLinkTrimXmlFilePath { get; set; }
 
		public ITaskItem[] DefineConstants { get; set; }
 
		/// <summary>
		///   The path to the file to generate.
		/// </summary>
		[Required]
		public ITaskItem RuntimeRootDescriptorFilePath { get; set; }
 
		sealed class ClassMembers
		{
			public bool keepAllFields;
			public HashSet<string> methods;
			public HashSet<string> fields;
		}
 
		/// <summary>
		/// Helper utility to track feature switch macros in header file
		/// This type is used in a dictionary as a key
		/// </summary>
		readonly struct FeatureSwitchMembers : IEquatable<FeatureSwitchMembers>
		{
			public string Feature { get; }
			public string FeatureValue { get; }
			public string FeatureDefault { get; }
			// Unique value to track the key
			private readonly string _key;
 
			public FeatureSwitchMembers (string feature, string featureValue, string featureDefault)
			{
				Feature = feature;
				FeatureValue = featureValue;
				FeatureDefault = featureDefault;
				// Use a separator that is not going to be in any of the strings to ensure uniqueness
				_key = feature + ',' + featureValue + ',' + featureDefault;
			}
 
			public override int GetHashCode ()
			{
				return _key.GetHashCode ();
			}
 
			public override bool Equals (object obj)
				=> obj is FeatureSwitchMembers inst && Equals (inst);
 
			public bool Equals (FeatureSwitchMembers fsm)
				=> fsm._key == _key;
		}
 
		readonly Dictionary<string, string> namespaceDictionary = new Dictionary<string, string> ();
		readonly Dictionary<string, string> classIdsToClassNames = new Dictionary<string, string> ();
		readonly Dictionary<string, ClassMembers> classNamesToClassMembers = new Dictionary<string, ClassMembers> ();
		readonly Dictionary<FeatureSwitchMembers, Dictionary<string, ClassMembers>> featureSwitchMembers = new ();
		readonly HashSet<string> defineConstants = new HashSet<string> (StringComparer.Ordinal);
 
		public override bool Execute ()
		{
			var namespaceFilePath = NamespaceFilePath.ItemSpec;
			if (!File.Exists (namespaceFilePath)) {
				Log.LogError ($"File '{namespaceFilePath}' doesn't exist.");
				return false;
			}
 
			var mscorlibFilePath = MscorlibFilePath.ItemSpec;
			if (!File.Exists (mscorlibFilePath)) {
				Log.LogError ($"File '{mscorlibFilePath}' doesn't exist.");
				return false;
			}
 
			var cortypeFilePath = CortypeFilePath.ItemSpec;
			if (!File.Exists (cortypeFilePath)) {
				Log.LogError ($"File '{cortypeFilePath}' doesn't exist.");
				return false;
			}
 
			var rexcepFilePath = RexcepFilePath.ItemSpec;
			if (!File.Exists (rexcepFilePath)) {
				Log.LogError ($"File '{rexcepFilePath}' doesn't exist.");
				return false;
			}
 
			InitializeDefineConstants ();
 
			var iLLinkTrimXmlFilePath = ILLinkTrimXmlFilePath.ItemSpec;
			if (!File.Exists (iLLinkTrimXmlFilePath)) {
				Log.LogError ($"File '{iLLinkTrimXmlFilePath}' doesn't exist.");
				return false;
			}
 
			ProcessNamespaces (namespaceFilePath);
 
			ProcessMscorlib (mscorlibFilePath);
 
			ProcessCoreTypes (cortypeFilePath);
 
			ProcessExceptionTypes (rexcepFilePath);
 
			OutputXml (iLLinkTrimXmlFilePath, RuntimeRootDescriptorFilePath.ItemSpec);
 
			return true;
		}
 
		void ProcessNamespaces (string namespaceFile)
		{
			string[] namespaces = File.ReadAllLines (namespaceFile);
 
			// Process definitions of the form
			// #define g_SystemNS          "System"
			// from namespace.h
			foreach (string namespaceDef in namespaces) {
				if (namespaceDef.StartsWith ("#define")) {
					char[] separators = { '"', ' ', '\t' };
					string[] namespaceDefElements = namespaceDef.Split (separators, StringSplitOptions.RemoveEmptyEntries);
					int startIndex = "g_".Length;
					// E.g., if namespaceDefElements [1] is "g_RuntimeNS", lhs is "Runtime".
					string lhs = namespaceDefElements[1].Substring (startIndex, namespaceDefElements[1].LastIndexOf ('N') - startIndex);
					if (namespaceDefElements.Length == 3) {
						// E.G., #define g_SystemNS          "System"
						// "System" --> "System"
						namespaceDictionary[lhs] = namespaceDefElements[2];
					} else {
						// E.g., #define g_RuntimeNS         g_SystemNS ".Runtime"
						// "Runtime" --> "System.Runtime"
						string prefix = namespaceDefElements[2].Substring (startIndex, namespaceDefElements[2].LastIndexOf ('N') - startIndex);
						namespaceDictionary[lhs] = namespaceDictionary[prefix] + namespaceDefElements[3];
					}
				}
			}
		}
 
		void ProcessMscorlib (string typeFile)
		{
			string[] types = File.ReadAllLines (typeFile);
			DefineTracker defineTracker = new DefineTracker (defineConstants, Log, typeFile);
			string classId;
			FeatureSwitchMembers? currentFeatureSwitch = null;
 
			foreach (string def in types) {
				if (defineTracker.ProcessLine (def) || !defineTracker.IsActiveSection)
					continue;
 
				//We will handle BEGIN_ILLINK_FEATURE_SWITCH and END_ILLINK_FEATURE_SWITCH
				if (def.StartsWith ("BEGIN_ILLINK_FEATURE_SWITCH")) {
					if (currentFeatureSwitch.HasValue) {
						Log.LogError ($"Could not figure out feature switch status in '{typeFile}' for line {def}.");
					} else {
						char[] separators = { ',', '(', ')', ' ', '\t', '/' };
						string[] featureSwitchElements = def.Split (separators, StringSplitOptions.RemoveEmptyEntries);
						if (featureSwitchElements.Length != 4) {
							Log.LogError ($"BEGIN_ILLINK_FEATURE_SWITCH is not formatted correctly '{typeFile}' for line {def}.");
							return;
						}
						currentFeatureSwitch = new FeatureSwitchMembers (featureSwitchElements[1], featureSwitchElements[2], featureSwitchElements[3]);
						if (!featureSwitchMembers.ContainsKey (currentFeatureSwitch.Value)) {
							featureSwitchMembers.Add (currentFeatureSwitch.Value, new Dictionary<string, ClassMembers> ());
						}
					}
				}
				if (def.StartsWith ("END_ILLINK_FEATURE_SWITCH")) {
					if (!currentFeatureSwitch.HasValue) {
						Log.LogError ($"Could not figure out feature switch status in '{typeFile}' for line {def}.");
					} else {
						currentFeatureSwitch = null;
					}
				}
 
				string[] defElements = null;
				if (def.StartsWith ("DEFINE_") || def.StartsWith ("// DEFINE_")) {
					char[] separators = { ',', '(', ')', ' ', '\t', '/' };
					defElements = def.Split (separators, StringSplitOptions.RemoveEmptyEntries);
				}
 
				if (def.StartsWith ("DEFINE_CLASS(") || def.StartsWith ("// DEFINE_CLASS(")) {
					// E.g., DEFINE_CLASS(APP_DOMAIN,            System,                 AppDomain)
					classId = defElements[1];               // APP_DOMAIN
					string classNamespace = defElements[2]; // System
					string className = defElements[3];      // AppDomain
					AddClass (classNamespace, className, classId, false, currentFeatureSwitch);
				} else if (def.StartsWith ("DEFINE_CLASS_U(")) {
					// E.g., DEFINE_CLASS_U(System,                 AppDomain,      AppDomainBaseObject)
					string classNamespace = defElements[1]; // System
					string className = defElements[2];      // AppDomain
					classId = defElements[3];               // AppDomainBaseObject
															// For these classes the sizes of managed and unmanaged classes and field offsets
															// are compared so we need to preserve all fields.
					const bool keepAllFields = true;
					AddClass (classNamespace, className, classId, keepAllFields, currentFeatureSwitch);
				} else if (def.StartsWith ("DEFINE_FIELD(")) {
					// E.g., DEFINE_FIELD(ACCESS_VIOLATION_EXCEPTION, IP,                _ip)
					classId = defElements[1];          // ACCESS_VIOLATION_EXCEPTION
					string fieldName = defElements[3]; // _ip
					AddField (fieldName, classId, currentFeatureSwitch);
				} else if (def.StartsWith ("DEFINE_METHOD(")) {
					// E.g., DEFINE_METHOD(APP_DOMAIN,           ON_ASSEMBLY_LOAD,       OnAssemblyLoadEvent,        IM_Assembly_RetVoid)
					string methodName = defElements[3]; // OnAssemblyLoadEvent
					classId = defElements[1];           // APP_DOMAIN
					AddMethod (methodName, classId, null, null, currentFeatureSwitch);
				} else if (def.StartsWith ("DEFINE_PROPERTY(") || def.StartsWith ("DEFINE_STATIC_PROPERTY(")) {
					// E.g., DEFINE_PROPERTY(ARRAY,              LENGTH,                 Length,                     Int)
					// or    DEFINE_STATIC_PROPERTY(THREAD,      CURRENT_THREAD,         CurrentThread,              Thread)
					string propertyName = defElements[3];          // Length or CurrentThread
					classId = defElements[1];                      // ARRAY or THREAD
					AddMethod ("get_" + propertyName, classId, null, null, currentFeatureSwitch);
				} else if (def.StartsWith ("DEFINE_SET_PROPERTY(")) {
					// E.g., DEFINE_SET_PROPERTY(THREAD,         UI_CULTURE,             CurrentUICulture,           CultureInfo)
					string propertyName = defElements[3]; // CurrentUICulture
					classId = defElements[1];             // THREAD
					AddMethod ("get_" + propertyName, classId, null, null, currentFeatureSwitch);
					AddMethod ("set_" + propertyName, classId, null, null, currentFeatureSwitch);
				}
			}
		}
 
		public void ProcessCoreTypes (string corTypeFile)
		{
			string[] corTypes = File.ReadAllLines (corTypeFile);
			DefineTracker defineTracker = new DefineTracker (defineConstants, Log, corTypeFile);
 
			foreach (string def in corTypes) {
				if (defineTracker.ProcessLine (def) || !defineTracker.IsActiveSection)
					continue;
 
				// E.g., TYPEINFO(ELEMENT_TYPE_VOID,         "System", "Void",          0,              TYPE_GC_NONE,   false,  true,   false,  false,  false) // 0x01
				if (def.StartsWith ("TYPEINFO(")) {
					char[] separators = { ',', '(', ')', '"', ' ', '\t' };
					string[] defElements = def.Split (separators, StringSplitOptions.RemoveEmptyEntries);
					string classId = null;
					string classNamespace = defElements[2]; // System
					string className = defElements[3];      // Void
					AddClass (classNamespace, className, classId);
				}
			}
		}
 
		public void ProcessExceptionTypes (string excTypeFile)
		{
			string[] excTypes = File.ReadAllLines (excTypeFile);
			DefineTracker defineTracker = new DefineTracker (defineConstants, Log, excTypeFile);
 
			foreach (string def in excTypes) {
				if (defineTracker.ProcessLine (def) || !defineTracker.IsActiveSection)
					continue;
 
				// E.g., DEFINE_EXCEPTION(g_InteropNS,          MarshalDirectiveException,      false,  COR_E_MARSHALDIRECTIVE)
				if (def.StartsWith ("DEFINE_EXCEPTION(")) {
					char[] separators = { ',', '(', ')', ' ', '\t' };
					string[] defElements = def.Split (separators, StringSplitOptions.RemoveEmptyEntries);
					string classId = null;
					string classNamespace = defElements[1]; // g_InteropNS
					string className = defElements[2];      // MarshalDirectiveException
					AddClass (classNamespace, className, classId);
					AddMethod (".ctor", classId, classNamespace, className);
				}
			}
		}
 
		void OutputXml (string iLLinkTrimXmlFilePath, string outputFileName)
		{
			XmlDocument doc = new XmlDocument ();
			using (var sr = new StreamReader (iLLinkTrimXmlFilePath)) {
				using XmlReader reader = XmlReader.Create (sr, new XmlReaderSettings () { XmlResolver = null });
				doc.Load (reader);
			}
 
			XmlElement linkerNode = doc["linker"];
 
			if (featureSwitchMembers.Count > 0) {
				foreach ((var fs, var members) in featureSwitchMembers.Select (kv => (kv.Key, kv.Value))) {
					if (members.Count == 0)
						continue;
 
					// <assembly fullname="System.Private.CoreLib" feature="System.Diagnostics.Tracing.EventSource.IsSupported" featurevalue="true" featuredefault="true">
					XmlElement featureAssemblyNode = doc.CreateElement ("assembly");
					XmlAttribute featureAssemblyFullName = doc.CreateAttribute ("fullname");
					featureAssemblyFullName.Value = "System.Private.CoreLib";
					featureAssemblyNode.Attributes.Append (featureAssemblyFullName);
 
					XmlAttribute featureName = doc.CreateAttribute ("feature");
					featureName.Value = fs.Feature;
					featureAssemblyNode.Attributes.Append (featureName);
 
					XmlAttribute featureValue = doc.CreateAttribute ("featurevalue");
					featureValue.Value = fs.FeatureValue;
					featureAssemblyNode.Attributes.Append (featureValue);
 
					XmlAttribute featureDefault = doc.CreateAttribute ("featuredefault");
					featureDefault.Value = fs.FeatureDefault;
					featureAssemblyNode.Attributes.Append (featureDefault);
 
					foreach (var type in members) {
						AddXmlTypeNode (doc, featureAssemblyNode, type.Key, type.Value);
					}
					linkerNode.AppendChild (featureAssemblyNode);
				}
			}
 
			XmlNode assemblyNode = linkerNode["assembly"];
 
			foreach (var type in classNamesToClassMembers) {
				AddXmlTypeNode (doc, assemblyNode, type.Key, type.Value);
			}
			doc.Save (outputFileName);
		}
 
		static void AddXmlTypeNode (XmlDocument doc, XmlNode assemblyNode, string typeName, ClassMembers members)
		{
			XmlElement typeNode = doc.CreateElement ("type");
			XmlAttribute typeFullName = doc.CreateAttribute ("fullname");
			typeFullName.Value = typeName;
			typeNode.Attributes.Append (typeFullName);
 
			// We need to keep everyting in System.Runtime.InteropServices.WindowsRuntime and
			// System.Threading.Volatile.
			if (!typeName.StartsWith ("System.Runtime.InteropServices.WindowsRuntime") &&
				!typeName.StartsWith ("System.Threading.Volatile")) {
				if (members.keepAllFields) {
					XmlAttribute preserve = doc.CreateAttribute ("preserve");
					preserve.Value = "fields";
					typeNode.Attributes.Append (preserve);
				} else if ((members.fields == null) && (members.methods == null)) {
					XmlAttribute preserve = doc.CreateAttribute ("preserve");
					preserve.Value = "nothing";
					typeNode.Attributes.Append (preserve);
				}
 
				if (!members.keepAllFields && (members.fields != null)) {
					foreach (string field in members.fields) {
						XmlElement fieldNode = doc.CreateElement ("field");
						XmlAttribute fieldName = doc.CreateAttribute ("name");
						fieldName.Value = field;
						fieldNode.Attributes.Append (fieldName);
						typeNode.AppendChild (fieldNode);
					}
				}
 
				if (members.methods != null) {
					foreach (string method in members.methods) {
						XmlElement methodNode = doc.CreateElement ("method");
						XmlAttribute methodName = doc.CreateAttribute ("name");
						methodName.Value = method;
						methodNode.Attributes.Append (methodName);
						typeNode.AppendChild (methodNode);
					}
				}
			}
			assemblyNode.AppendChild (typeNode);
		}
 
 
		void AddClass (string classNamespace, string className, string classId, bool keepAllFields = false, FeatureSwitchMembers? featureSwitch = null)
		{
			string fullClassName = GetFullClassName (classNamespace, className);
			if (fullClassName != null) {
				if ((classId != null) && (classId != "NoClass")) {
					classIdsToClassNames[classId] = fullClassName;
				}
 
				ClassMembers members;
				if (!featureSwitch.HasValue) {
					if (!classNamesToClassMembers.TryGetValue (fullClassName, out members)) {
						members = new ClassMembers ();
						classNamesToClassMembers[fullClassName] = members;
					}
				} else {
					Dictionary<string, ClassMembers> currentFeatureSwitchMembers = featureSwitchMembers[featureSwitch.Value];
					if (!currentFeatureSwitchMembers.TryGetValue (fullClassName, out members)) {
						members = new ClassMembers ();
						currentFeatureSwitchMembers[fullClassName] = members;
					}
				}
				members.keepAllFields |= keepAllFields;
			}
		}
 
		void AddField (string fieldName, string classId, FeatureSwitchMembers? featureSwitch = null)
		{
			string className = classIdsToClassNames[classId];
			ClassMembers members;
 
			if (!featureSwitch.HasValue) {
				members = classNamesToClassMembers[className];
			} else {
				members = featureSwitchMembers[featureSwitch.Value][className];
			}
 
			members.fields ??= new HashSet<string> ();
			members.fields.Add (fieldName);
		}
 
		void AddMethod (string methodName, string classId, string classNamespace = null, string className = null, FeatureSwitchMembers? featureSwitch = null)
		{
			string fullClassName;
			if (classId != null) {
				fullClassName = classIdsToClassNames[classId];
			} else {
				fullClassName = GetFullClassName (classNamespace, className);
			}
 
			ClassMembers members;
 
			if (!featureSwitch.HasValue) {
				members = classNamesToClassMembers[fullClassName];
			} else {
				members = featureSwitchMembers[featureSwitch.Value][fullClassName];
			}
 
			members.methods ??= new HashSet<string> ();
			members.methods.Add (methodName);
		}
 
		string GetFullClassName (string classNamespace, string className)
		{
			string prefixToRemove = "g_";
			if (classNamespace.StartsWith (prefixToRemove)) {
				classNamespace = classNamespace.Substring (prefixToRemove.Length);
			}
			string suffixToRemove = "NS";
			if (classNamespace.EndsWith (suffixToRemove)) {
				classNamespace = classNamespace.Substring (0, classNamespace.Length - suffixToRemove.Length);
			}
 
			if ((classNamespace == "NULL") && (className == "NULL")) {
				return null;
			}
 
			if (!namespaceDictionary.ContainsKey (classNamespace)) {
				Log.LogError ($"Unknown namespace '{classNamespace}'.");
			}
 
			// Convert from the System.Reflection/CoreCLR nested type name format to the IL/Cecil format.
			string classNameWithCecilNestedFormat = className.Replace ('+', '/');
 
			return namespaceDictionary[classNamespace] + "." + classNameWithCecilNestedFormat;
		}
 
		void InitializeDefineConstants ()
		{
			if (DefineConstants is null)
				return;
 
			foreach (var item in DefineConstants)
				defineConstants.Add (item.ItemSpec.Trim ());
		}
 
		sealed class DefineTracker
		{
			readonly HashSet<string> defineConstants;
			readonly TaskLoggingHelper log;
			readonly string filePath;
			readonly Stack<bool?> activeSections = new Stack<bool?> ();
			int linenum;
 
			public DefineTracker (HashSet<string> defineConstants, TaskLoggingHelper log, string filePath)
			{
				this.defineConstants = defineConstants;
				this.log = log;
				this.filePath = filePath;
			}
 
			public bool ProcessLine (string line)
			{
				linenum++;
 
				int ifdefLength = 0;
				bool negativeIfDef = false;
				if (line.StartsWith ("#ifdef ")) {
					ifdefLength = 7;
				} else if (line.StartsWith ("#ifndef ")) {
					ifdefLength = 8;
					negativeIfDef = true;
				} else if (line.StartsWith ("#if ")) {
					ifdefLength = 4;
				}
 
				if (ifdefLength > 0) {
					if (activeSections.Count > 0 && activeSections.Peek () != true) {
						activeSections.Push (null);
					} else {
						string defineName = line.Substring (ifdefLength);
						int commentIndex = defineName.IndexOf ('/');
						if (commentIndex >= 0)
							defineName = defineName.Substring (0, commentIndex);
 
						defineName = defineName.Trim ();
						if (defineConstants.Contains (defineName))
							activeSections.Push (!negativeIfDef);
						else
							activeSections.Push (negativeIfDef);
					}
 
					return true;
				}
 
				if (line.StartsWith ("#else")) {
					if (activeSections.Count == 0) {
						log.LogError ($"Could not figure out ifdefs in '{filePath}' around line {linenum}.");
					} else {
						bool? activeSection = activeSections.Pop ();
						if (activeSection == null)
							activeSections.Push (null);
						else
							activeSections.Push (!activeSection.Value);
					}
 
					return true;
				}
 
				if (line.StartsWith ("#endif")) {
					if (activeSections.Count == 0)
						log.LogError ($"Could not figure out ifdefs in '{filePath}' around line {linenum}.");
					else
						activeSections.Pop ();
 
					return true;
				}
 
				return false;
			}
 
			public bool IsActiveSection {
				get => activeSections.Count == 0 || activeSections.Peek () == true;
			}
		}
	}
}