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";
+ }
+}
+