From 14f75f06dc9ee8f088928e7ad6e79043626e9bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Mon, 7 Aug 2023 15:41:07 +0200 Subject: [PATCH] MSR: Generate additional constructors to reduce usage of reflection (#18529) Closes #18357 This PR implements the idea described in the linked issue: Whenever there's an NSObject constructor that we call from a registrar callback, we need to create a separate constructor that will first set the `handle` and `flags` values of the NSObject before calling the original constructor. Here's an example of the code we generate: ```csharp // The original constructor: public .ctor (T0 p0, T1 p1, ...) { /* ... */ } // The generated constructor with pre-initialization: public .ctor (T0 p0, T1 p1, ..., IntPtr handle, IManagedRegistrar dummy) { this.handle = (NativeHandle)handle; this.flags = 2; // Flags.NativeRef == 2 this..ctor (p0, p1, ...); } ``` - This code can't be expressed in C# and it can only be expressed directly in IL. - The reason we need to do this is because the base NSObject parameterless constructor would allocate a new Objective-C object if `handle` is a zero pointer. - The `IManagedRegistrar` type is added only to make the constructor signature unique (IManagedRegistrar is not a public class and customers can't use it in their apps directly) --- src/Foundation/NSObject2.cs | 14 ++- tools/dotnet-linker/AppBundleRewriter.cs | 55 ++++++++--- tools/dotnet-linker/CecilExtensions.cs | 8 ++ .../Steps/ManagedRegistrarStep.cs | 93 +++++++++++++++++-- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/src/Foundation/NSObject2.cs b/src/Foundation/NSObject2.cs index f88f1a0492c2..a54e741e9f14 100644 --- a/src/Foundation/NSObject2.cs +++ b/src/Foundation/NSObject2.cs @@ -231,16 +231,14 @@ public void Dispose () GC.SuppressFinalize (this); } - static T AllocateNSObject (IntPtr handle) where T : NSObject - { - var obj = (T) RuntimeHelpers.GetUninitializedObject (typeof (T)); - obj.handle = handle; - obj.flags = Flags.NativeRef; - return obj; - } - internal static IntPtr CreateNSObject (IntPtr type_gchandle, IntPtr handle, Flags flags) { +#if NET + if (Runtime.IsManagedStaticRegistrar) { + throw new System.Diagnostics.UnreachableException (); + } +#endif + // This function is called from native code before any constructors have executed. var type = (Type) Runtime.GetGCHandleTarget (type_gchandle); try { diff --git a/tools/dotnet-linker/AppBundleRewriter.cs b/tools/dotnet-linker/AppBundleRewriter.cs index edd7dc2ace5c..92f6384e547b 100644 --- a/tools/dotnet-linker/AppBundleRewriter.cs +++ b/tools/dotnet-linker/AppBundleRewriter.cs @@ -51,6 +51,7 @@ public AssemblyDefinition PlatformAssembly { Dictionary> type_map = new (); Dictionary method_map = new (); + Dictionary field_map = new (); public AppBundleRewriter (LinkerConfiguration configuration) { @@ -176,6 +177,23 @@ static string GetMethodSignature (MethodDefinition method) return $"{method?.ReturnType?.FullName ?? "(null)"} {method?.DeclaringType?.FullName ?? "(null)"}::{method?.Name ?? "(null)"} ({string.Join (", ", method?.Parameters?.Select (v => v?.ParameterType?.FullName + " " + v?.Name) ?? Array.Empty ())})"; } + public FieldReference GetFieldReference (AssemblyDefinition assembly, TypeReference tr, string name, string key, out FieldDefinition field) + { + if (!field_map.TryGetValue (key, out var tuple)) { + var td = tr.Resolve (); + var fd = td.Fields.SingleOrDefault (v => v.Name == name); + if (fd is null) + throw new InvalidOperationException ($"Unable to find the field '{tr.FullName}::{name}' (for key '{key}') in {assembly.Name.Name}. Fields in type:\n\t{string.Join ("\n\t", td.Fields.Select (f => f.Name).OrderBy (v => v))}"); + + tuple.Item1 = fd; + tuple.Item2 = CurrentAssembly.MainModule.ImportReference (fd); + field_map.Add (key, tuple); + } + + field = tuple.Item1; + return tuple.Item2; + } + /* Types */ public TypeReference System_Boolean { @@ -267,6 +285,12 @@ public TypeReference System_Void { } } + public TypeReference System_ValueType { + get { + return GetTypeReference (CorlibAssembly, "System.ValueType", out var _); + } + } + public TypeReference System_Collections_Generic_Dictionary2 { get { return GetTypeReference (CorlibAssembly, "System.Collections.Generic.Dictionary`2", out var _); @@ -321,6 +345,26 @@ public TypeReference Foundation_NSObject { } } + public FieldReference Foundation_NSObject_HandleField { + get { + return GetFieldReference (PlatformAssembly, Foundation_NSObject, "handle", "Foundation.NSObject::handle", out var _); + } + } + +#if NET + public MethodReference Foundation_NSObject_FlagsSetterMethod { + get { + return GetMethodReference (PlatformAssembly, Foundation_NSObject, "set_flags", "Foundation.NSObject::set_flags", predicate: null, out var _); + } + } +#else + public FieldReference Foundation_NSObject_FlagsField { + get { + return GetFieldReference (PlatformAssembly, Foundation_NSObject, "flags", "Foundation.NSObject::flags", out var _); + } + } +#endif + public TypeReference ObjCRuntime_BindAs { get { return GetTypeReference (PlatformAssembly, "ObjCRuntime.BindAs", out var _); @@ -449,17 +493,6 @@ public MethodReference MethodBase_GetMethodFromHandle__RuntimeMethodHandle { } } - public MethodReference NSObject_AllocateNSObject { - get { - return GetMethodReference (PlatformAssembly, - Foundation_NSObject, "AllocateNSObject", - nameof (NSObject_AllocateNSObject), - isStatic: true, - genericParameterCount: 1, - System_IntPtr); - } - } - public MethodReference BindAs_ConvertNSArrayToManagedArray { get { return GetMethodReference (PlatformAssembly, ObjCRuntime_BindAs, "ConvertNSArrayToManagedArray", (v) => diff --git a/tools/dotnet-linker/CecilExtensions.cs b/tools/dotnet-linker/CecilExtensions.cs index e4b0dcd34e26..0176e16d995d 100644 --- a/tools/dotnet-linker/CecilExtensions.cs +++ b/tools/dotnet-linker/CecilExtensions.cs @@ -30,6 +30,14 @@ public static MethodDefinition AddMethod (this TypeDefinition self, string name, return rv; } + public static FieldDefinition AddField (this TypeDefinition self, string name, FieldAttributes attributes, TypeReference type) + { + var rv = new FieldDefinition (name, attributes, type); + rv.DeclaringType = self; + self.Fields.Add (rv); + return rv; + } + public static MethodBody CreateBody (this MethodDefinition self, out ILProcessor il) { var body = new MethodBody (self); diff --git a/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs b/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs index 9f8811fbde4a..0e89314fa655 100644 --- a/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs +++ b/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs @@ -401,6 +401,7 @@ public void EmitCallToExportedMethod (MethodDefinition method, MethodDefinition var placeholderType = abr.System_IntPtr; ParameterDefinition? callSuperParameter = null; VariableDefinition? returnVariable = null; + MethodReference? ctor = null; var leaveTryInstructions = new List (); var isVoid = method.ReturnType.Is ("System", "Void"); @@ -425,6 +426,7 @@ public void EmitCallToExportedMethod (MethodDefinition method, MethodDefinition // and later on remove everything after this instruction. Maybe at a later point I'll figure out a way to make the code emission conditional without // littering the logic with conditional statements. Instruction? skipEverythingAfter = null; + if (isInstanceCategory) { il.Emit (OpCodes.Ldarg_0); EmitConversion (method, il, method.Parameters [0].ParameterType, true, 0, out var nativeType, postProcessing); @@ -459,12 +461,32 @@ public void EmitCallToExportedMethod (MethodDefinition method, MethodDefinition // We're throwing an exception, so there's no need for any more code. skipEverythingAfter = il.Body.Instructions.Last (); } else { - il.Emit (OpCodes.Ldarg_0); + // Whenever there's an NSObject constructor that we call from a registrar callback, we need to create + // a separate constructor that will first set the `handle` and `flags` values of the NSObject before + // calling the original constructor. Here's an example of the code we generate: + // + // // The original constructor: + // public .ctor (T0 p0, T1 p1, ...) { /* ... */ } + // + // // The generated constructor with pre-initialization: + // public .ctor (T0 p0, T1 p1, ..., IntPtr nativeHandle, IManagedRegistrar dummy) { + // this.handle = (NativeHandle)nativeHandle; + // this.flags = 2; // Flags.NativeRef == 2 + // this..ctor (p0, p1, ...); + // } + // + // - This code can't be expressed in C# and it can only be expressed directly in IL. + // - The reason we need to do this is because the base NSObject parameterless constructor + // would allocate a new Objective-C object if `handle` is a zero pointer. + // - The `IManagedRegistrar` dummy parameter is used only to make sure that the signature + // is unique and there aren't any conflicts. The IManagedRegistrar type is internal and + // we only make it public through a custom linker step. + + ctor = CloneConstructorWithNativeHandle (method); + method.DeclaringType.Methods.Add (ctor.Resolve ()); + + il.Emit (OpCodes.Nop); postLeaveBranch.Operand = il.Body.Instructions.Last (); - var git = new GenericInstanceMethod (abr.NSObject_AllocateNSObject); - git.GenericArguments.Add (method.DeclaringType); - il.Emit (OpCodes.Call, git); - il.Emit (OpCodes.Dup); // this is for the call to ObjCRuntime.NativeObjectExtensions::GetHandle after the call to the constructor } } else if (isGeneric) { // this is a proxy method and we can simply use `this` without any conversion @@ -509,7 +531,13 @@ public void EmitCallToExportedMethod (MethodDefinition method, MethodDefinition callback.AddParameter ("exception_gchandle", new PointerType (abr.System_IntPtr)); - if (isGeneric && !method.IsConstructor) { + if (ctor is not null) { + // in addition to the params of the original ctor we pass also the native handle and a null + // value for the dummy (de-duplication) parameter + il.Emit (OpCodes.Ldarg_0); + il.Emit (OpCodes.Ldnull); + il.Emit (OpCodes.Newobj, ctor); + } else if (isGeneric && !method.IsConstructor) { var targetMethod = method.DeclaringType.CreateMethodReferenceOnGenericType (method, method.DeclaringType.GenericParameters.ToArray ()); il.Emit (OpCodes.Call, targetMethod); } else if (method.IsStatic) { @@ -1270,5 +1298,58 @@ void GenerateConversionToNative (MethodDefinition method, ILProcessor il, TypeRe il.Append (endTarget); } } + + MethodDefinition CloneConstructorWithNativeHandle (MethodDefinition ctor) + { + var clonedCtor = new MethodDefinition (ctor.Name, ctor.Attributes, ctor.ReturnType); + clonedCtor.IsPublic = false; + + // clone the original parameters firsts + foreach (var parameter in ctor.Parameters) { + clonedCtor.AddParameter (parameter.Name, parameter.ParameterType); + } + + // add a native handle param + a dummy parameter that we know for a fact won't be used anywhere + // to make the signature of the new constructor unique + var handleParameter = clonedCtor.AddParameter ("nativeHandle", abr.System_IntPtr); + var dummyParameter = clonedCtor.AddParameter ("dummy", abr.ObjCRuntime_IManagedRegistrar); + + var body = clonedCtor.CreateBody (out var il); + + // ensure visible + abr.Foundation_NSObject_HandleField.Resolve ().IsFamily = true; +#if NET + abr.Foundation_NSObject_FlagsSetterMethod.Resolve ().IsFamily = true; +#else + abr.Foundation_NSObject_FlagsField.Resolve ().IsFamily = true; +#endif + + // store the handle and flags first + il.Emit (OpCodes.Ldarg_0); + il.Emit (OpCodes.Ldarg, handleParameter); +#if NET + il.Emit (OpCodes.Call, abr.NativeObject_op_Implicit_NativeHandle); +#endif + il.Emit (OpCodes.Stfld, abr.CurrentAssembly.MainModule.ImportReference (abr.Foundation_NSObject_HandleField)); + + il.Emit (OpCodes.Ldarg_0); + il.Emit (OpCodes.Ldc_I4_2); // Flags.NativeRef == 2 +#if NET + il.Emit (OpCodes.Call, abr.Foundation_NSObject_FlagsSetterMethod); +#else + il.Emit (OpCodes.Stfld, abr.Foundation_NSObject_FlagsField); +#endif + + // call the original constructor with all of the original parameters + il.Emit (OpCodes.Ldarg_0); + foreach (var parameter in clonedCtor.Parameters.SkipLast (2)) { + il.Emit (OpCodes.Ldarg, parameter); + } + + il.Emit (OpCodes.Call, ctor); + il.Emit (OpCodes.Ret); + + return clonedCtor; + } } }