diff --git a/src/ObjCRuntime/Runtime.cs b/src/ObjCRuntime/Runtime.cs index 6af4a4aab523..69a9964fe2a7 100644 --- a/src/ObjCRuntime/Runtime.cs +++ b/src/ObjCRuntime/Runtime.cs @@ -201,6 +201,15 @@ public bool IsSimulator { internal static unsafe InitializationOptions* options; +#if NET + public static class ClassHandles + { + internal static unsafe void InitializeClassHandles (MTClassMap* map) + { + } + } +#endif + #if NET [BindingImpl (BindingImplOptions.Optimizable)] internal unsafe static bool IsCoreCLR { diff --git a/tools/class-redirector/class-redirector-tests/Usings.cs b/tools/class-redirector/class-redirector-tests/Usings.cs new file mode 100644 index 000000000000..a2ef4115a619 --- /dev/null +++ b/tools/class-redirector/class-redirector-tests/Usings.cs @@ -0,0 +1,2 @@ +global using NUnit.Framework; + diff --git a/tools/class-redirector/class-redirector-tests/XmlTests.cs b/tools/class-redirector/class-redirector-tests/XmlTests.cs new file mode 100644 index 000000000000..db9ea57727d5 --- /dev/null +++ b/tools/class-redirector/class-redirector-tests/XmlTests.cs @@ -0,0 +1,93 @@ +using ClassRedirector; +using System.Xml.Linq; + +namespace ClassRedirectorTests; + +public class XmlTests { + [Test] + public void WritesCorrectXml () + { + var map = new CSToObjCMap () { + ["Foo"] = new ObjCNameIndex ("Bar", 2), + ["Baz"] = new ObjCNameIndex ("Zed", 3), + }; + + var doc = CSToObjCMap.ToXDocument (map); + + var elem = doc.Elements ().Where (el => el.Name == "CSToObjCMap").FirstOrDefault (); + Assert.IsNotNull (elem, "no map"); + Assert.That (elem.Elements ().Count (), Is.EqualTo (2), "Incorrect number of children"); + + var cselem = XElementWithAttribute (elem, "CSName", "Foo"); + Assert.IsNotNull (cselem, "missing Foo elem"); + var nameElem = NameElem (cselem); + Assert.IsNotNull (nameElem, "no name elem1"); + Assert.That (nameElem.Value, Is.EqualTo ("Bar")); + var indexElem = IndexElem (cselem); + Assert.IsNotNull (indexElem, "no value elem1"); + Assert.That ((int) indexElem, Is.EqualTo (2)); + + cselem = XElementWithAttribute (elem, "CSName", "Baz"); + Assert.IsNotNull (cselem, "missing Baz elem"); + nameElem = NameElem (cselem); + Assert.IsNotNull (nameElem, "no name elem2"); + indexElem = IndexElem (cselem); + Assert.That (nameElem.Value, Is.EqualTo ("Zed")); + Assert.IsNotNull (indexElem, "no value elem2"); + Assert.That ((int) indexElem, Is.EqualTo (3)); + } + + [Test] + public void ReadsCorrectXml () + { + var text = @" + + + + Bar + 2 + + + + + Zed + 3 + + +"; + + using var reader = new StringReader (text); + var doc = XDocument.Load (reader); + + var map = CSToObjCMap.FromXDocument (doc); + Assert.IsNotNull (map, "no map"); + Assert.That (map.Count (), Is.EqualTo (2)); + + Assert.True (map.TryGetValue ("Foo", out var nameIndex), "no nameIndex"); + Assert.That (nameIndex.ObjCName, Is.EqualTo ("Bar"), "no bar name"); + Assert.That (nameIndex.MapIndex, Is.EqualTo (2), "no bar index"); + + Assert.True (map.TryGetValue ("Baz", out var nameIndex1)); + Assert.That (nameIndex1.ObjCName, Is.EqualTo ("Zed"), "no bar name"); + Assert.That (nameIndex1.MapIndex, Is.EqualTo (3), "no bar index"); + } + + + static XElement? XElementWithAttribute (XElement el, string attrName, string attrValue) + { + return el.Elements ().Where (e => { + var attr = e.Attribute (attrName); + return attr is not null && attr.Value == attrValue; + }).FirstOrDefault (); + } + + static XElement? NameElem (XElement el) => FirstElementNamed (el, "Name"); + + static XElement? IndexElem (XElement el) => FirstElementNamed (el, "Index"); + + static XElement? FirstElementNamed (XElement el, string name) + { + return el.Descendants ().Where (e => e.Name == name).FirstOrDefault (); + } +} + diff --git a/tools/class-redirector/class-redirector-tests/class-redirector-tests.csproj b/tools/class-redirector/class-redirector-tests/class-redirector-tests.csproj new file mode 100644 index 000000000000..742289b1ec34 --- /dev/null +++ b/tools/class-redirector/class-redirector-tests/class-redirector-tests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + class_redirector_tests + enable + enable + + false + + + + + + + + + + + + + CSToObjCMap.cs + + + ObjCNameIndex.cs + + + StaticRegistrarFile.cs + + + diff --git a/tools/class-redirector/class-redirector.sln b/tools/class-redirector/class-redirector.sln new file mode 100644 index 000000000000..25e29a839df7 --- /dev/null +++ b/tools/class-redirector/class-redirector.sln @@ -0,0 +1,59 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 25.0.1705.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "class-redirector", "class-redirector\class-redirector.csproj", "{0723580E-2C56-4460-BDDD-DEF38E0F543F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "class-redirector-tests", "class-redirector-tests\class-redirector-tests.csproj", "{16FD4B5E-5537-46A2-B573-0424A51472AA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0723580E-2C56-4460-BDDD-DEF38E0F543F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0723580E-2C56-4460-BDDD-DEF38E0F543F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0723580E-2C56-4460-BDDD-DEF38E0F543F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0723580E-2C56-4460-BDDD-DEF38E0F543F}.Release|Any CPU.Build.0 = Release|Any CPU + {16FD4B5E-5537-46A2-B573-0424A51472AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16FD4B5E-5537-46A2-B573-0424A51472AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16FD4B5E-5537-46A2-B573-0424A51472AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16FD4B5E-5537-46A2-B573-0424A51472AA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ECD11F77-045E-4B88-BF92-28ED04BBD631} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.FileWidth = 80 + $1.TabWidth = 8 + $1.IndentWidth = 8 + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = False + $2.NewLinesForBracesInTypes = False + $2.NewLinesForBracesInProperties = False + $2.NewLinesForBracesInAccessors = False + $2.NewLinesForBracesInAnonymousMethods = False + $2.NewLinesForBracesInControlBlocks = False + $2.NewLinesForBracesInAnonymousTypes = False + $2.NewLinesForBracesInObjectCollectionArrayInitializers = False + $2.NewLinesForBracesInLambdaExpressionBody = False + $2.NewLineForElse = False + $2.NewLineForCatch = False + $2.NewLineForFinally = False + $2.NewLineForMembersInObjectInit = False + $2.NewLineForMembersInAnonymousTypes = False + $2.NewLineForClausesInQuery = False + $2.SpacingAfterMethodDeclarationName = True + $2.SpaceAfterMethodCallName = True + $2.SpaceBeforeOpenSquareBracket = True + $2.scope = text/x-csharp + EndGlobalSection +EndGlobal diff --git a/tools/class-redirector/class-redirector/Program.cs b/tools/class-redirector/class-redirector/Program.cs new file mode 100644 index 000000000000..08e4125314a1 --- /dev/null +++ b/tools/class-redirector/class-redirector/Program.cs @@ -0,0 +1,106 @@ +using System.IO; +using Mono.Options; +using System.Xml.Linq; + +#nullable enable + +namespace ClassRedirector { + public class Program { + public static int Main (string [] args) + { + try { + return Main2 (args); + } catch (Exception e) { + Console.Error.WriteLine (e); + return 1; + } + } + + public static int Main2 (string [] args) + { + var doHelp = false; + string inputDirectory = ""; + var options = new OptionSet () { + { "h|?|help", o => doHelp = true }, + { "i=|input-directory=", d => inputDirectory = d }, + }; + + if (doHelp) { + options.WriteOptionDescriptions (Console.Out); + Console.WriteLine ($"This program takes an input directory and looks for the file '{StaticRegistrarFile.Name}'."); + Console.WriteLine ("Upon finding it, it uses that as a map for finding all C# classes defined in dll's in the"); + Console.WriteLine ("directory and rewrites the classes' definition and usage of class_ptr to be more"); + Console.WriteLine ("efficient and, if possible, to no longer need a static constructor."); + Console.WriteLine ("This program also requires that the directory contains one of the Microsoft platform dlls."); + Console.WriteLine ("Classes in the directory are modified in place."); + return 0; + } + + if (String.IsNullOrEmpty (inputDirectory)) { + throw new Exception ($"input-directory is required"); + } + + if (!Directory.Exists (inputDirectory)) { + throw new Exception ($"input-directory {inputDirectory} does not exist."); + } + + if (!DirectoryIsWritable (inputDirectory)) { + throw new Exception ($"input-directory {inputDirectory} is not writable"); + } + + var registrarFile = Path.Combine (inputDirectory, StaticRegistrarFile.Name); + if (!File.Exists (registrarFile)) { + throw new Exception ($"map file {registrarFile} does not exist."); + } + + var dllsToProcess = CollectDlls (inputDirectory); + var xamarinDll = FindXamarinDll (dllsToProcess); + + if (xamarinDll is null) + throw new Exception ($"unable to find platform dll in {inputDirectory}"); + + var map = ReadRegistrarFile (registrarFile); + + var rewriter = new Rewriter (map, xamarinDll, dllsToProcess); + rewriter.Process (); + + return 0; + } + + static bool DirectoryIsWritable (string path) + { + var info = new DirectoryInfo (path); + return !info.Attributes.HasFlag (FileAttributes.ReadOnly); + } + + static string [] CollectDlls (string dir) + { + return Directory.GetFiles (dir, "*.dll"); // GetFiles returns full paths + } + + static string [] xamarinDlls = new string [] { + "Microsoft.iOS.dll", + "Microsoft.macOS.dll", + "Microsoft.tvOS.dll", + }; + + static bool IsXamarinDll (string p) + { + return xamarinDlls.FirstOrDefault (dll => p.EndsWith (dll, StringComparison.Ordinal)) is not null; + } + + static string? FindXamarinDll (string [] paths) + { + return paths.FirstOrDefault (IsXamarinDll); + } + + static CSToObjCMap ReadRegistrarFile (string path) + { + var doc = XDocument.Load (path); + var map = CSToObjCMap.FromXDocument (doc); + if (map is null) + throw new Exception ($"Unable to read static registrar map file {path}"); + return map; + } + } +} diff --git a/tools/class-redirector/class-redirector/Rewriter.cs b/tools/class-redirector/class-redirector/Rewriter.cs new file mode 100644 index 000000000000..1351efac0f54 --- /dev/null +++ b/tools/class-redirector/class-redirector/Rewriter.cs @@ -0,0 +1,258 @@ +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace ClassRedirector { + public class Rewriter { + const string classHandleName = "ObjRuntime.Runtime.ClassHandles"; + const string mtClassMapName = "ObjCRuntime.Runtime.MTClassMap"; + const string nativeHandleName = "ObjCRuntime.NativeHandle"; + const string initClassHandlesName = "InitializeClassHandles"; + const string classPtrName = "class_ptr"; + CSToObjCMap map; + string pathToXamarinAssembly; + string [] assembliesToPatch; + SimpleAssemblyResolver resolver; + Dictionary csTypeToFieldDef = new Dictionary (); + + public Rewriter (CSToObjCMap map, string pathToXamarinAssembly, string [] assembliesToPatch) + { + this.map = map; + this.pathToXamarinAssembly = pathToXamarinAssembly; + this.assembliesToPatch = assembliesToPatch; + resolver = new SimpleAssemblyResolver (assembliesToPatch); + } + + public void Process () + { + var classMap = CreateClassHandles (); + PatchClassPtrUsage (classMap); + } + + Dictionary CreateClassHandles () + { + var classMap = new Dictionary (); + using var module = ModuleDefinition.ReadModule (pathToXamarinAssembly); + + var classHandles = LocateClassHandles (module); + if (classHandles is null) + throw new Exception ($"Unable to find {classHandleName} type in {pathToXamarinAssembly}"); + + var initMethod = classHandles.Methods.FirstOrDefault (m => m.Name == initClassHandlesName); + if (initMethod is null) + throw new Exception ($"Unable to find {initClassHandlesName} method in {classHandles.Name}"); + + var processor = initMethod.Body.GetILProcessor (); + + var mtClassMapDef = LocateMTClassMap (module); + if (mtClassMapDef is null) + throw new Exception ($"Unable to find {mtClassMapName} in {pathToXamarinAssembly}"); + + var nativeHandle = module.Types.FirstOrDefault (t => t.Name == nativeHandleName); + if (nativeHandle is null) + throw new Exception ($"Unable to find {nativeHandleName} in {pathToXamarinAssembly}"); + + var nativeHandleOpImplicit = FindOpImplicit (nativeHandle); + if (nativeHandleOpImplicit is null) + throw new Exception ($"Unable to find implicit cast in {nativeHandleName}"); + + foreach (var (csName, nameIndex) in map) { + var fieldDef = AddPublicStaticField (classHandles, nameIndex.ObjCName, nativeHandle); + AddInitializer (nativeHandleOpImplicit, processor, mtClassMapDef, nameIndex.MapIndex, fieldDef); + classMap [csName] = fieldDef; + } + + module.Write (); + return classMap; + } + + MethodDefinition? FindOpImplicit (TypeDefinition nativeHandle) + { + return nativeHandle.Methods.FirstOrDefault (m => m.Name == "op_Implicit" && m.ReturnType == nativeHandle && + m.Parameters.Count == 1 && m.Parameters [0].ParameterType == nativeHandle.Module.TypeSystem.IntPtr); + } + + void AddInitializer (MethodReference nativeHandleOpImplicit, ILProcessor il, TypeDefinition mtClassMapDef, int index, FieldDefinition fieldDef) + { + // Assuming that we have a method that looks like this: + // internal static unsafe void InitializeClassHandles (MTClassMap* map) + // { + // } + // We should have a compiled method that looks like this: + // nop + // ret + // + // For each handle that we define, we should add the following instructions: + // ldarg.0 + // ldc.i4 index + // conv.i + // sizeof ObjCRuntime.Runtime.MTClassMap + // mul + // add + // ldfld ObjCRuntime.Runtime.MTClassMap.handle + // call ObjCRuntime.NativeHandle ObjCRuntime.NativeHandle::op_Implicit(System.IntPtr) + // stsfld fieldDef + var handleRef = mtClassMapDef.Fields.First (f => f.Name == "handle"); + var last = il.Body.Instructions.Last (); + il.InsertBefore (last, Instruction.Create (OpCodes.Ldarg_0)); + il.InsertBefore (last, Instruction.Create (OpCodes.Ldc_I4, index)); + il.InsertBefore (last, Instruction.Create (OpCodes.Conv_I)); + il.InsertBefore (last, Instruction.Create (OpCodes.Sizeof, mtClassMapDef)); + il.InsertBefore (last, Instruction.Create (OpCodes.Mul)); + il.InsertBefore (last, Instruction.Create (OpCodes.Add)); + il.InsertBefore (last, Instruction.Create (OpCodes.Ldfld, handleRef)); + il.InsertBefore (last, Instruction.Create (OpCodes.Call, nativeHandleOpImplicit)); + il.InsertBefore (last, Instruction.Create (OpCodes.Stsfld, fieldDef)); + } + + FieldDefinition AddPublicStaticField (TypeDefinition inType, string fieldName, TypeReference fieldType) + { + var fieldDef = new FieldDefinition (fieldName, FieldAttributes.Public | FieldAttributes.Static, fieldType); + inType.Fields.Add (fieldDef); + return fieldDef; + } + + TypeDefinition? LocateClassHandles (ModuleDefinition module) + { + return module.GetType (classHandleName); + } + + TypeDefinition? LocateMTClassMap (ModuleDefinition module) + { + return module.GetType (mtClassMapName); + } + + void PatchClassPtrUsage (Dictionary classMap) + { + foreach (var path in assembliesToPatch) { + using var module = ModuleDefinition.ReadModule (path); + PatchClassPtrUsage (classMap, module); + module.Write (); + } + } + + void PatchClassPtrUsage (Dictionary classMap, ModuleDefinition module) + { + foreach (var cl in module.Types) { + if (classMap.TryGetValue (cl.FullName, out var classPtrField)) { + PatchClassPtrUsage (cl, classPtrField); + } + } + } + + void PatchClassPtrUsage (TypeDefinition cl, FieldDefinition classPtrField) + { + var class_ptr = cl.Fields.FirstOrDefault (f => f.Name == classPtrName); + if (class_ptr is null) { + throw new Exception ($"Error processing class {cl.FullName} - no {classPtrName} field."); + } + + // step 1: remove the field + cl.Fields.Remove (class_ptr); + + // step 2: remove init code from cctor + RemoveCCtorInit (cl, class_ptr); + + // step 3: patch every method + PatchMethods (cl, class_ptr, classPtrField); + } + + void PatchMethods (TypeDefinition cl, FieldDefinition classPtr, FieldDefinition classPtrField) + { + foreach (var method in cl.Methods) { + PatchMethod (method, classPtr, classPtrField); + } + } + + void PatchMethod (MethodDefinition method, FieldDefinition classPtr, FieldDefinition classPtrField) + { + foreach (var instr in method.Body.Instructions) { + if (instr.OpCode == OpCodes.Ldsfld && instr.Operand == classPtr) { + instr.Operand = classPtrField; + } + } + } + + void RemoveCCtorInit (TypeDefinition cl, FieldDefinition class_ptr) + { + var cctor = cl.Methods.FirstOrDefault (m => m.Name == ".cctor"); + if (cctor is null) + return; // no static init - should never happen, but we can deal. + + var il = cctor.Body.GetILProcessor (); + Instruction? stsfld = null; + int i = 0; + for (; i < il.Body.Instructions.Count; i++) { + var instr = il.Body.Instructions [i]; + // look for + // stsfld class_ptr + if (instr.OpCode == OpCodes.Stsfld && instr.Operand == class_ptr) { + stsfld = instr; + break; + } + } + if (stsfld is null) + return; + + // if we see: + // ldstr "any" + // call ObjCRuntime.GetClassHandle + // stsfld class_ptr + // Then we can remove all of those instructions + + var isGetClassHandle = IsGetClassHandle (il, i - 1); + var isLdStr = IsLdStr (il, i - 2); + + if (isGetClassHandle && isLdStr) { + il.RemoveAt (i); + il.RemoveAt (i - 1); + il.RemoveAt (i - 2); + } else if (isGetClassHandle) { + // don't know how the string got on the stack, so at least get rid of the + // call to GetClassHandle by nopping it out. This still leaves the string on + // the stack, so pop it. + il.Replace (il.Body.Instructions [i - 1], Instruction.Create (OpCodes.Nop)); + // can't remove all three, so just pop the IntPtr. + il.Replace (il.Body.Instructions [i], Instruction.Create (OpCodes.Pop)); + } else { + // can't remove all three, so just pop the IntPtr. + il.Replace (il.Body.Instructions [i], Instruction.Create (OpCodes.Pop)); + } + + // if we're left with exactly 1 instruction and it's a return, + // then we can get rid of the entire method + if (cctor.Body.Instructions.Count == 1) { + if (cctor.Body.Instructions.Last ().OpCode == OpCodes.Ret) + cl.Methods.Remove (cctor); + } + + // if we're left with exactly 2 instructions and the first is a no-op + // and the last is a return, then we can get rid of the entire method + if (cctor.Body.Instructions.Count == 2) { + if (cctor.Body.Instructions.Last ().OpCode == OpCodes.Ret && + cctor.Body.Instructions.First ().OpCode == OpCodes.Nop) + cl.Methods.Remove (cctor); + } + } + + bool IsGetClassHandle (ILProcessor il, int index) + { + if (index < 0) + return false; + var instr = il.Body.Instructions [index]!; + var operand = instr.Operand?.ToString () ?? ""; + return instr.OpCode == OpCodes.Call && operand.Contains ("Class::GetHandle", StringComparison.Ordinal); + } + + bool IsLdStr (ILProcessor il, int index) + { + if (index < 0) + return false; + var instr = il.Body.Instructions [index]!; + return instr.OpCode == OpCodes.Ldstr; + } + } +} + diff --git a/tools/class-redirector/class-redirector/SimpleAssemblyResolver.cs b/tools/class-redirector/class-redirector/SimpleAssemblyResolver.cs new file mode 100644 index 000000000000..33665dc31afd --- /dev/null +++ b/tools/class-redirector/class-redirector/SimpleAssemblyResolver.cs @@ -0,0 +1,21 @@ +using System.IO; +using Mono.Cecil; + +#nullable enable + +namespace ClassRedirector { + public class SimpleAssemblyResolver : DefaultAssemblyResolver { + public SimpleAssemblyResolver (params string [] filesOrDirectories) + : base () + { + foreach (var fileOrDirectory in filesOrDirectories) { + if (File.Exists (fileOrDirectory)) { + AddSearchDirectory (Path.GetDirectoryName (fileOrDirectory)); + } else if (Directory.Exists (fileOrDirectory)) { + AddSearchDirectory (fileOrDirectory); + } + } + } + } +} + diff --git a/tools/class-redirector/class-redirector/class-redirector.csproj b/tools/class-redirector/class-redirector/class-redirector.csproj new file mode 100644 index 000000000000..2a0530da750a --- /dev/null +++ b/tools/class-redirector/class-redirector/class-redirector.csproj @@ -0,0 +1,26 @@ + + + + Exe + net7.0 + class_redirector + enable + enable + + + + + + + + + CSToObjCMap.cs + + + ObjCNameIndex.cs + + + StaticRegistrarFile.cs + + + diff --git a/tools/common/CSToObjCMap.cs b/tools/common/CSToObjCMap.cs new file mode 100644 index 000000000000..d0dcc1866539 --- /dev/null +++ b/tools/common/CSToObjCMap.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +#nullable enable + +namespace ClassRedirector { + public class CSToObjCMap : Dictionary { + const string objMapName = "CSToObjCMap"; + const string elementName = "Element"; + const string csNameName = "CSName"; + public CSToObjCMap () : base () + { + } + + public static XElement ToXElement (CSToObjCMap map) + { + return new XElement (objMapName, Elements (map)); + } + + static IEnumerable Elements (CSToObjCMap map) + { + return map.Select (kvp => new XElement (elementName, new XAttribute (csNameName, kvp.Key), ObjCNameIndex.ToXElement (kvp.Value))); + } + + public static CSToObjCMap FromXElement (XElement xmap) + { + var map = new CSToObjCMap (); + var elements = from el in xmap.Descendants (elementName) + select new KeyValuePair (el.Attribute (csNameName)?.Value, + ObjCNameIndex.FromXElement (el.Element (ObjCNameIndex.ObjNameIndexName))); + foreach (var elem in elements) { + if (elem.Key is not null && elem.Value is not null) + map.Add (elem.Key, elem.Value); + } + return map; + } + + public static CSToObjCMap? FromXDocument (XDocument doc) + { + var el = doc.Descendants (objMapName).FirstOrDefault (); + return el is null ? null : FromXElement (el); + } + + public static XDocument ToXDocument (CSToObjCMap map) + { + return new XDocument (ToXElement (map)); + } + } +} + diff --git a/tools/common/ObjCNameIndex.cs b/tools/common/ObjCNameIndex.cs new file mode 100644 index 000000000000..cac09237722b --- /dev/null +++ b/tools/common/ObjCNameIndex.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +#nullable enable + +namespace ClassRedirector { + public class ObjCNameIndex { + public const string ObjNameIndexName = "ObjNameIndex"; + const string nameName = "Name"; + const string indexName = "Index"; + public ObjCNameIndex (string objCName, int mapIndex) + { + ObjCName = objCName; + MapIndex = mapIndex; + } + public string ObjCName { get; private set; } + public int MapIndex { get; private set; } + + public static XElement ToXElement (ObjCNameIndex nameIndex) + { + return new XElement (ObjNameIndexName, + new XElement (nameName, nameIndex.ObjCName), + new XElement (indexName, nameIndex.MapIndex)); + } + + public static ObjCNameIndex? FromXElement (XElement? objNameIndex) + { + if (objNameIndex is null) + return null; + var name = (string?) objNameIndex.Element (nameName); + var index = (int?) objNameIndex.Element (indexName); + if (name is null || index is null) + return null; + return new ObjCNameIndex (name, index.Value); + } + } +} + diff --git a/tools/common/StaticRegistrarFile.cs b/tools/common/StaticRegistrarFile.cs new file mode 100644 index 000000000000..35a7801aeed5 --- /dev/null +++ b/tools/common/StaticRegistrarFile.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +#nullable enable + +namespace ClassRedirector { + public class StaticRegistrarFile { + public const string Name = "static-registrar-map.xml"; + } +} +