Skip to content

Commit

Permalink
MSR: Generate additional constructors to reduce usage of reflection (#…
Browse files Browse the repository at this point in the history
…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)
  • Loading branch information
simonrozsival authored Aug 7, 2023
1 parent 1e7e084 commit 14f75f0
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 25 deletions.
14 changes: 6 additions & 8 deletions src/Foundation/NSObject2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,16 +231,14 @@ public void Dispose ()
GC.SuppressFinalize (this);
}

static T AllocateNSObject<T> (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 {
Expand Down
55 changes: 44 additions & 11 deletions tools/dotnet-linker/AppBundleRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public AssemblyDefinition PlatformAssembly {

Dictionary<AssemblyDefinition, Dictionary<string, (TypeDefinition, TypeReference)>> type_map = new ();
Dictionary<string, (MethodDefinition, MethodReference)> method_map = new ();
Dictionary<string, (FieldDefinition, FieldReference)> field_map = new ();

public AppBundleRewriter (LinkerConfiguration configuration)
{
Expand Down Expand Up @@ -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<string> ())})";
}

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 {
Expand Down Expand Up @@ -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 _);
Expand Down Expand Up @@ -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 _);
Expand Down Expand Up @@ -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) =>
Expand Down
8 changes: 8 additions & 0 deletions tools/dotnet-linker/CecilExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
93 changes: 87 additions & 6 deletions tools/dotnet-linker/Steps/ManagedRegistrarStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Instruction> ();
var isVoid = method.ReturnType.Is ("System", "Void");

Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
}

6 comments on commit 14f75f0

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.