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

Research improving finding the constructor to call to create a managed NSObject instance in MSR #18358

Closed
Tracked by #18584
rolfbjarne opened this issue May 26, 2023 · 1 comment · Fixed by #18519
Closed
Tracked by #18584
Assignees
Labels
performance If an issue or pull request is related to performance
Milestone

Comments

@rolfbjarne
Copy link
Member

Ref: last part of dotnet/runtime#86649 (comment)

When instantiating a managed NSObject instance (or subclass) for an existing Objective-C object

Say we have the following managed class:

class MyObject : NSObject {
    [Export ("doSomething:")]
    public static void DoSomething (NSString something) {}
}

class NSString : NSObject {
    protected NSString (IntPtr handle) : base (handle) {}
}

In this case we do something like:

partial class MyObject {
    class __RegistrarHelper_ {
        [UnmanagedCallersOnly ("__MyObject_DoSomething__")]
        static void DoSomething (IntPtr handle, IntPtr selector, IntPtr arg1)
        {
             // Runtime.GetNSObject:
             // https://github.com/xamarin/xamarin-macios/blob/3dd412daa6b5522029014b0be298f27d751c5f45/src/ObjCRuntime/Runtime.cs#L1516-L1519
             // basically something like this:
             //     if we already have a managed instance for the handle 'handle', return that
             //     otherwise find the constructor that takes an IntPtr (using reflection), and call that constructor
            var arg1Obj = Runtime.GetNSObject ();
                
            MyObject.DoSomething (arg1Obj);
        }
    }
}

This is in some ways very similar to Filip's INativeObject/Selector case from above, except a bit more complex, because Runtime.GetNSObject will look up the actual runtime type of the Objective-C handle, find the closest matching C# type, and create an instance of that type. This might very well be a subclass of the type the caller expects (and this is why we can't just do a new NSString (handle) in the generated code).

Potential solution: we already generate a table to map Objective-C class -> managed type (which we use to find the closest matching C# type for a given Objective-C classr), we could add the token for the IntPtr ctor to table, so that when we find a managed type, we'll find the ctor to call too (or maybe even generate code to call the ctor directly, because calling a method given a token in C# is rather obnoxious)

@simonrozsival
Copy link
Contributor

simonrozsival commented Aug 10, 2023

I experimented with a C#-only solution that could potentially work with roslyn source generators in the future. I came up with a PoC which isn't very different from the current solution that uses reflection but uses the recently introduced zero-overhead unsafe accessors and a "trick" to make it impossible for NativeAOT to generate extra metadata:

partial class CustomClass : NSObject {
    private int _x;
    [Export ("initWithX:")]
    public CustomClass (int x)  => _x = x;
}

// generated:
partial class CustomClass {
    static class __Registrar_Callbacks__ {
        [UnmanagedCallersOnly (EntryPoint = "_callback_CustomClass__ctor")]
        private static IntPtr callback_CustomClass__ctor (IntPtr handle, int x) {
            var obj = NSObject.CreateNSObject<CustomClass> (handle);
            ctor (obj, x);
            return Runtime.AllocGCHandle (obj);

            [UnsafeAccessor(UnsafeAccessorKind.Method, Name = ".ctor")]
            extern static void ctor(CustomClass @this, int x);
        }
    }
}

// xamarin runtime:
class NSObject {
    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern",
        MessageId = "We make sure there are hard references to the type's constructor(s) through the UnsafeAccessor " +
                    "and ILC will find it and include it.")]
    public static T CreateNSObject<T> (IntPtr handle) where T : NSObject {
        var obj = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); // ILC won't generate extra metadata since we don't use typeof(CustomClass) directly

        ref IntPtr _handle = ref GetHandle(obj);
        _handle = handle;

        return obj;
    }

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_handle")]
    extern static ref IntPtr GetHandle(NSObject @this);
}

Source of a working PoC is here: https://gist.github.com/simonrozsival/4c7d16a2b277145fe7ea0a73ea61357d
Ref #17693

rolfbjarne pushed a commit that referenced this issue Aug 15, 2023
…using reflection in the managed static registrar (#18519)

This PR adds lookup tables and factory methods for INativeObjects and
NSObjects. These generated methods allow us to create instances of these
objects without needing reflection.

Closes #18358.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance If an issue or pull request is related to performance
Projects
None yet
2 participants