Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dotnet] Integrate class handle rewriting into static registrar process. #18456

Merged
merged 9 commits into from
Jun 23, 2023
134 changes: 0 additions & 134 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/ClassRedirectorTask.cs

This file was deleted.

6 changes: 0 additions & 6 deletions msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj
Original file line number Diff line number Diff line change
@@ -72,12 +72,6 @@
<Compile Include="..\..\tools\common\SdkVersions.cs">
<Link>external\SdkVersions.cs</Link>
</Compile>
<Compile Include="..\..\tools\common\CSToObjCMap.cs">
<Link>external\CSToObjCMap.cs</Link>
</Compile>
<Compile Include="..\..\tools\common\ObjCNameIndex.cs">
<Link>external\ObjCNameIndex.cs</Link>
</Compile>
</ItemGroup>

<ItemGroup>
1 change: 1 addition & 0 deletions src/ObjCRuntime/Runtime.cs
Original file line number Diff line number Diff line change
@@ -206,6 +206,7 @@ public bool IsSimulator {
internal static unsafe InitializationOptions* options;

#if NET
[Preserve (AllMembers = true)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe just add this class + method dynamically in the rewriter? That way it would only be present when needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that's missing is a call from Initialize that will run that code. It needs to be there and it's best if it's just empty.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the method is called from somewhere, then you won't need the Preserve attribute.

public static class ClassHandles
{
internal static unsafe void InitializeClassHandles (MTClassMap* map)
12 changes: 7 additions & 5 deletions tools/common/Application.cs
Original file line number Diff line number Diff line change
@@ -18,6 +18,8 @@

using ObjCRuntime;

using ClassRedirector;

#if MONOTOUCH
using PlatformResolver = MonoTouch.Tuner.MonoTouchResolver;
#elif MMP
@@ -167,7 +169,6 @@ public bool IsDefaultMarshalManagedExceptionMode {
public bool EnableBitCode { get { return BitCodeMode != BitCodeMode.None; } }

public bool SkipMarkingNSObjectsInUserAssemblies { get; set; }
public string ClassMapPath = "";

// assembly_build_targets describes what kind of native code each assembly should be compiled into for mobile targets (iOS, tvOS, watchOS).
// An assembly can be compiled into: static object (.o), dynamic library (.dylib) or a framework (.framework).
@@ -1029,10 +1030,11 @@ public void RunRegistrar ()
}
#endif
var registrar = new Registrar.StaticRegistrar (this);
if (RootAssemblies.Count == 1)
registrar.GenerateSingleAssembly (resolver, resolvedAssemblies.Values, Path.ChangeExtension (registrar_m, "h"), registrar_m, Path.GetFileNameWithoutExtension (RootAssembly), out var _, ClassMapPath);
else
registrar.Generate (resolver, resolvedAssemblies.Values, Path.ChangeExtension (registrar_m, "h"), registrar_m, out var _, ClassMapPath);
if (RootAssemblies.Count == 1) {
registrar.GenerateSingleAssembly (resolver, resolvedAssemblies.Values, Path.ChangeExtension (registrar_m, "h"), registrar_m, Path.GetFileNameWithoutExtension (RootAssembly), out var _);
} else {
registrar.Generate (resolver, resolvedAssemblies.Values, Path.ChangeExtension (registrar_m, "h"), registrar_m, out var _);
}
}

public IEnumerable<Abi> Abis {
37 changes: 0 additions & 37 deletions tools/common/CSToObjCMap.cs
Original file line number Diff line number Diff line change
@@ -7,46 +7,9 @@

namespace ClassRedirector {
public class CSToObjCMap : Dictionary<string, ObjCNameIndex> {
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<XElement> 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<string?, ObjCNameIndex?> (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));
}
}
}

2 changes: 0 additions & 2 deletions tools/common/Driver.cs
Original file line number Diff line number Diff line change
@@ -257,8 +257,6 @@ static bool ParseOptions (Application app, Mono.Options.OptionSet options, strin
options.Add ("skip-marking-nsobjects-in-user-assemblies:", "Don't mark NSObject (and any subclass of NSObject) in user assemblies in the linker. This may break your app, use at own risk.", v => {
app.SkipMarkingNSObjectsInUserAssemblies = ParseBool (v, "--skip-marking-nsobjects-in-user-assemblies");
});
options.Add ("class-map-path=", "Sets the path for an output path to generate a class map XML file used to optimize class handle access when the static registrar has been used.", v => { app.ClassMapPath = v; });


// Keep the ResponseFileSource option at the end.
options.Add (new Mono.Options.ResponseFileSource ());
21 changes: 0 additions & 21 deletions tools/common/ObjCNameIndex.cs
Original file line number Diff line number Diff line change
@@ -6,34 +6,13 @@

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);
}
}
}

Original file line number Diff line number Diff line change
@@ -5,10 +5,12 @@
using Mono.Cecil;
using Mono.Cecil.Cil;
using ClassRedirector;
using Mono.Linker;

#nullable enable

namespace Xamarin.MacDev.Tasks {
namespace ClassRedirector {
#if NET
public class Rewriter {
const string runtimeName = "ObjCRuntime.Runtime";
const string classHandleName = "ObjCRuntime.Runtime/ClassHandles";
@@ -18,35 +20,54 @@ public class Rewriter {
const string classPtrName = "class_ptr";
CSToObjCMap map;
string pathToXamarinAssembly;
string [] assembliesToPatch;
string outputDirectory;
SimpleAssemblyResolver resolver;
string? outputDirectory = null;
Dictionary<string, FieldDefinition> csTypeToFieldDef = new Dictionary<string, FieldDefinition> ();
IEnumerable<AssemblyDefinition> assemblies;
AssemblyDefinition xamarinAssembly;
Xamarin.Tuner.DerivedLinkContext linkContext;

public Rewriter (CSToObjCMap map, string pathToXamarinAssembly, string [] assembliesToPatch, string outputDirectory)
public Rewriter (CSToObjCMap map, IEnumerable<AssemblyDefinition> assembliesToPatch, Xamarin.Tuner.DerivedLinkContext? linkContext)
{
this.map = map;
this.pathToXamarinAssembly = pathToXamarinAssembly;
this.assembliesToPatch = assembliesToPatch;
this.outputDirectory = outputDirectory;
resolver = new SimpleAssemblyResolver (assembliesToPatch);
this.assemblies = assembliesToPatch;
var xasm = assembliesToPatch.Select (assem => assem.MainModule).FirstOrDefault (ContainsNativeHandle)?.Assembly;
if (xasm is null) {
throw new Exception ("Unable to find Xamarin assembly.");
} else {
xamarinAssembly = xasm;
pathToXamarinAssembly = xamarinAssembly.MainModule.FileName;
}
if (linkContext is null) {
throw new Exception ("Rewriter needs a valid link context.");
} else {
this.linkContext = linkContext;
}

}

public void Process ()
public string Process ()
{
var classMap = CreateClassHandles ();
Dictionary<string, FieldDefinition> classMap;
try {
classMap = CreateClassHandles ();
} catch (Exception e) {
// if this throws, no changes are made to the assemblies
// so it's safe to log it on the far side.
return e.Message;
}
Comment on lines +53 to +57
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason it's not a failure to fail to apply the class handle optimization? It should be easy enough for customers to turn it off if need be, and just printing a log message if something goes wrong feels like nobody will notice if it ever breaks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple reasons: if this throws an exception, it means that we've done something horribly, horribly wrong. It means that someone removed the ClassHandles types or its InitializeClassHandles method, or NativeHandle or any of a number of things that need to be in place for the rest of the code to work. And while this seems like a "Holy cow, we should stop this immediately!" error - it actually isn't. The rest of the process won't run and no changes will have been made to the assemblies and the user is left with a build without NativeHandle.

PatchClassPtrUsage (classMap);
return "";
}

Dictionary<string, FieldDefinition> CreateClassHandles ()
{
var classMap = new Dictionary<string, FieldDefinition> ();
using var assemblyStm = new FileStream (pathToXamarinAssembly, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
using var module = ModuleDefinition.ReadModule (assemblyStm);
var module = xamarinAssembly.MainModule;

var classHandles = LocateClassHandles (module);
if (classHandles is null)
throw new Exception ($"Unable to find {classHandleName} type in {pathToXamarinAssembly}");
if (classHandles is null) {
throw new Exception ($"Unable to find {classHandleName} type in Module {module.Name} File {module.FileName}, assembly {xamarinAssembly.Name}");
}

var initMethod = classHandles.Methods.FirstOrDefault (m => m.Name == initClassHandlesName);
if (initMethod is null)
@@ -56,16 +77,19 @@ Dictionary<string, FieldDefinition> CreateClassHandles ()

var mtClassMapDef = LocateMTClassMap (module);
if (mtClassMapDef is null)
throw new Exception ($"Unable to find {mtClassMapName} in {pathToXamarinAssembly}");
throw new Exception ($"Unable to find {mtClassMapName} in Module {module.Name} File {module.FileName}, assembly {xamarinAssembly.Name}");

var nativeHandle = module.Types.FirstOrDefault (t => t.FullName == nativeHandleName);
var nativeHandle = LocateNativeHandle (module);
if (nativeHandle is null)
throw new Exception ($"Unable to find {nativeHandleName} in {pathToXamarinAssembly}");
throw new Exception ($"Unable to find {nativeHandleName} in Module {module.Name} File {module.FileName}, assembly {xamarinAssembly.Name}");

var nativeHandleOpImplicit = FindOpImplicit (nativeHandle);
if (nativeHandleOpImplicit is null)
throw new Exception ($"Unable to find implicit cast in {nativeHandleName}");

if (map.Count () == 0)
return classMap;

foreach (var nameIndexPair in map) {
var csName = nameIndexPair.Key;
var nameIndex = nameIndexPair.Value;
@@ -74,7 +98,7 @@ Dictionary<string, FieldDefinition> CreateClassHandles ()
classMap [csName] = fieldDef;
}

module.Write (ToOutputFileName (pathToXamarinAssembly));
MarkForSave (xamarinAssembly);
return classMap;
}

@@ -124,6 +148,16 @@ FieldDefinition AddPublicStaticField (TypeDefinition inType, string fieldName, T
return fieldDef;
}

bool ContainsNativeHandle (ModuleDefinition module)
{
return LocateNativeHandle (module) is not null;
}

TypeDefinition? LocateNativeHandle (ModuleDefinition module)
{
return AllTypes (module).FirstOrDefault (t => t.FullName == nativeHandleName);
}

TypeDefinition? LocateClassHandles (ModuleDefinition module)
{
return AllTypes (module).FirstOrDefault (t => t.FullName == classHandleName);
@@ -136,21 +170,28 @@ FieldDefinition AddPublicStaticField (TypeDefinition inType, string fieldName, T

void PatchClassPtrUsage (Dictionary<string, FieldDefinition> classMap)
{
foreach (var path in assembliesToPatch) {
using var stm = new FileStream (path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
using var module = ModuleDefinition.ReadModule (stm);
PatchClassPtrUsage (classMap, module);
module.Write (ToOutputFileName (path));
foreach (var assem in assemblies) {
var module = assem.MainModule;
if (PatchClassPtrUsage (classMap, module)) {
MarkForSave (assem);
}
}
}

void PatchClassPtrUsage (Dictionary<string, FieldDefinition> classMap, ModuleDefinition module)
// returns true if the assembly was changed.
bool PatchClassPtrUsage (Dictionary<string, FieldDefinition> classMap, ModuleDefinition module)
{
var dirty = false;
foreach (var cl in AllTypes (module)) {
if (classMap.TryGetValue (cl.FullName, out var classPtrField)) {
dirty = true;
// if this doesn't throw, it will
// always change the contents of an
// assembly
PatchClassPtrUsage (cl, classPtrField);
}
}
return dirty;
}

void PatchClassPtrUsage (TypeDefinition cl, FieldDefinition classPtrField)
@@ -291,6 +332,15 @@ string ToOutputFileName (string pathToInputFileName)
{
return Path.Combine (outputDirectory, Path.GetFileName (pathToInputFileName));
}

void MarkForSave (AssemblyDefinition assembly)
{
var annotations = linkContext.Annotations;
var action = annotations.GetAction (assembly);
if (action == AssemblyAction.Copy)
annotations.SetAction (assembly, AssemblyAction.Save);
}
}
#endif
}

31 changes: 17 additions & 14 deletions tools/common/StaticRegistrar.cs
Original file line number Diff line number Diff line change
@@ -2843,7 +2843,7 @@ public string GetInitializationMethodName (string single_assembly)
}
}

void Specialize (AutoIndentStringBuilder sb, out string initialization_method, string type_map_path)
void Specialize (AutoIndentStringBuilder sb, out string initialization_method)
{
List<Exception> exceptions = new List<Exception> ();
List<ObjCMember> skip = new List<ObjCMember> ();
@@ -3295,12 +3295,15 @@ void Specialize (AutoIndentStringBuilder sb, out string initialization_method, s

sb.WriteLine (map.ToString ());
sb.WriteLine (map_init.ToString ());

if (!string.IsNullOrEmpty (type_map_path)) {
var doc = CSToObjCMap.ToXDocument (map_dict);
doc.Save (type_map_path);
#if NET
if (App.Optimizations.RedirectClassHandles == true) {
var rewriter = new Rewriter (map_dict, GetAssemblies (), LinkContext);
var result = rewriter.Process ();
if (!string.IsNullOrEmpty (result)) {
Driver.Log (5, $"Not redirecting class handles because {result}");
}
}

#endif
ErrorHelper.ThrowIfErrors (exceptions);
}

@@ -5504,24 +5507,24 @@ public void FilterTrimmedApi (AnnotationStore annotations)
}
}

public void GenerateSingleAssembly (PlatformResolver resolver, IEnumerable<AssemblyDefinition> assemblies, string header_path, string source_path, string assembly, out string initialization_method, string type_map_path)
public void GenerateSingleAssembly (PlatformResolver resolver, IEnumerable<AssemblyDefinition> assemblies, string header_path, string source_path, string assembly, out string initialization_method)
{
single_assembly = assembly;
Generate (resolver, assemblies, header_path, source_path, out initialization_method, type_map_path);
Generate (resolver, assemblies, header_path, source_path, out initialization_method);
}

public void Generate (IEnumerable<AssemblyDefinition> assemblies, string header_path, string source_path, out string initialization_method, string type_map_path)
public void Generate (IEnumerable<AssemblyDefinition> assemblies, string header_path, string source_path, out string initialization_method)
{
Generate (null, assemblies, header_path, source_path, out initialization_method, type_map_path);
Generate (null, assemblies, header_path, source_path, out initialization_method);
}

public void Generate (PlatformResolver resolver, IEnumerable<AssemblyDefinition> assemblies, string header_path, string source_path, out string initialization_method, string type_map_path)
public void Generate (PlatformResolver resolver, IEnumerable<AssemblyDefinition> assemblies, string header_path, string source_path, out string initialization_method)
{
Register (resolver, assemblies);
Generate (header_path, source_path, out initialization_method, type_map_path);
Generate (header_path, source_path, out initialization_method);
}

public void Generate (string header_path, string source_path, out string initialization_method, string type_map_path)
public void Generate (string header_path, string source_path, out string initialization_method)
{
var sb = new AutoIndentStringBuilder ();
header = new AutoIndentStringBuilder ();
@@ -5556,7 +5559,7 @@ public void Generate (string header_path, string source_path, out string initial
if (App.Embeddinator)
methods.WriteLine ("void xamarin_embeddinator_initialize ();");

Specialize (sb, out initialization_method, type_map_path);
Specialize (sb, out initialization_method);

methods.WriteLine ();
methods.AppendLine ();
2 changes: 1 addition & 1 deletion tools/dotnet-linker/Steps/RegistrarStep.cs
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ protected override void TryEndProcess ()
// so we need to remove those that were later trimmed away by the trimmer.
Configuration.Target.StaticRegistrar.FilterTrimmedApi (Annotations);
}
Configuration.Target.StaticRegistrar.Generate (header, code, out var initialization_method, app.ClassMapPath);
Configuration.Target.StaticRegistrar.Generate (header, code, out var initialization_method);

var items = new List<MSBuildItem> ();
foreach (var abi in Configuration.Abis) {
3 changes: 3 additions & 0 deletions tools/dotnet-linker/dotnet-linker.csproj
Original file line number Diff line number Diff line change
@@ -107,6 +107,9 @@
<Compile Include="..\common\CSToObjCMap.cs">
<Link>tools\common\CSToObjCMap.cs</Link>
</Compile>
<Compile Include="..\common\Rewriter.cs">
<Link>tools\common\Rewriter.cs</Link>
</Compile>
<Compile Include="..\common\ObjCNameIndex.cs">
<Link>tools\common\ObjCNameIndex.cs</Link>
</Compile>
3 changes: 2 additions & 1 deletion tools/mmp/driver.cs
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@
using Xamarin.Utils;
using Xamarin.Linker;
using Registrar;
using ClassRedirector;
using ObjCRuntime;

namespace Xamarin.Bundler {
@@ -794,7 +795,7 @@ static void Compile ()
if (App.Registrar == RegistrarMode.Static) {
registrarPath = Path.Combine (App.Cache.Location, "registrar.m");
var registrarH = Path.Combine (App.Cache.Location, "registrar.h");
BuildTarget.StaticRegistrar.Generate (BuildTarget.Resolver.ResolverCache.Values, registrarH, registrarPath, out initialization_method, App.ClassMapPath);
BuildTarget.StaticRegistrar.Generate (BuildTarget.Resolver.ResolverCache.Values, registrarH, registrarPath, out initialization_method);

var platform_assembly = BuildTarget.Resolver.ResolverCache.First ((v) => v.Value.Name.Name == BuildTarget.StaticRegistrar.PlatformAssembly).Value;
Frameworks.Gather (App, platform_assembly, BuildTarget.Frameworks, BuildTarget.WeakFrameworks);
3 changes: 3 additions & 0 deletions tools/mmp/mmp.csproj
Original file line number Diff line number Diff line change
@@ -364,6 +364,9 @@
<Compile Include="..\common\CSToObjCMap.cs">
<Link>tools\common\CSToObjCMap.cs</Link>
</Compile>
<Compile Include="..\common\Rewriter.cs">
<Link>tools\common\Rewriter.cs</Link>
</Compile>
<Compile Include="..\common\ObjCNameIndex.cs">
<Link>tools\common\ObjCNameIndex.cs</Link>
</Compile>
4 changes: 3 additions & 1 deletion tools/mtouch/BuildTasks.mtouch.cs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@

using Xamarin.MacDev;
using Xamarin.Utils;
using ClassRedirector;

namespace Xamarin.Bundler {
public abstract class ProcessTask : BuildTask {
@@ -120,7 +121,8 @@ public override IEnumerable<string> Outputs {

protected override void Execute ()
{
Target.StaticRegistrar.Generate (Target.Assemblies.Select ((a) => a.AssemblyDefinition), RegistrarHeaderPath, RegistrarCodePath, out var initialization_name, Target.App.ClassMapPath);
var assemblies = Target.Assemblies.Select ((a) => a.AssemblyDefinition);
Target.StaticRegistrar.Generate (assemblies, RegistrarHeaderPath, RegistrarCodePath, out var initialization_name);
RegistrationMethods.Add (initialization_name);
}
}
3 changes: 3 additions & 0 deletions tools/mtouch/mtouch.csproj
Original file line number Diff line number Diff line change
@@ -338,6 +338,9 @@
<Compile Include="..\common\OSPlatformAttributeExtensions.cs">
<Link>tools\common\OSPlatformAttributeExtensions.cs</Link>
</Compile>
<Compile Include="..\common\Rewriter.cs">
<Link>tools\common\Rewriter.cs</Link>
</Compile>
<Compile Include="..\common\CSToObjCMap.cs">
<Link>tools\common\CSToObjCMap.cs</Link>
</Compile>