-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Trimming and extensions compatibility design – discussion #86649
Comments
Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas Issue DetailsDescriptionXamarin is heavily dependent on ILLinker and use of custom linker steps in mobile platform builds. This dependency means that if we want to support building an application targeting iOS-like platforms with NativeAOT, we would have to run ILLinker and ILCompiler (NativeAOT's compiler and trimmer) in cascade during the build.
To use the full potential of both of the tools, we need to make them compatible (to some extent) for targeting mobile devices. This issue has been opened: to identify how we can reduce the incompatibility gap, to discuss possible approaches, ideas and suggestions on how to get pass this limitation in general with a goal of enabling NativeAOT to work with Xamarin.
|
|
Can this be address by leaving trimming of the runtime libraries to ILCompiler only?
Can this be addressed by passing a file with reflection roots from Linker to ILCompiler? (The format is TBD.) |
Yes; that's what I did in the first prototype (the code is now superseded by other iterations in various repository forks).
Possibly. I had the idea to output rd.xml from the custom linker step that analyzes |
It would be better to produce a managed .dll to pass this info along. For example, it can contain a giant method with a long list of |
Out of curiosity, when exactly is the method visibility evaluated for |
ILCompiler does not do any visibility checks. It assumes that it is given valid IL. |
The ldtoken/pop would also be a binary protocol within some well known type/method so even if we were to do visibility/accessibility checks they would be suppressed for the protocol because they wouldn't make sense. What are the things that are getting trimmed? Is it things that the ObjC interop layer is looking up using reflection at runtime? Could you share pointers to the source code of those places (the spot in iOS interop source code that is failing if ILCompiler trims it)? |
Yes, interop constructors that are accessed through reflection. One example I hit very early on is the Selector constructor that is access by reflection from one of the Runtime.GetINativeObject overloads. In this particular case we already discussed some solutions (without modifying ILCompiler behavior; and which would work for user written code using the same pattern):
|
You can use |
I am aware of that but I don't think that's a good idea. The custom ILLink step would rewrite user's assemblies, add that attribute along with the interop code, and it would affect the runtime visibility checks even for the non-interop code. At very least it's an observable change that affects user code. |
It can be its own assembly. (I actually assumed that it would be its own assembly so that it can passed explicitly via command line arg.) |
Ah, sorry, that's a misunderstanding. If we are specifically talking about producing additional input for ILCompiler to preserve methods then it can be separate assembly, and The idea with |
Looks like this is mostly used in the context of the generics (I don't see anything but dead references to the non-generic methods in the MAUI sample I got). Would it be feasible to change the pattern from:
To:
This would be a lot faster and is naturally trim safe. We're using static abstract methods for the COM source generator as well to solve similar problems. |
So, conceptually something like this? (deliberately oversimplified constructor lookup and actual parameters) using System;
Console.WriteLine(CreateINativeObject<NObject>(IntPtr.Zero));
Console.WriteLine(CreateINativeObject<NObject2>(IntPtr.Zero));
static T? CreateINativeObject<T>(IntPtr handle)
where T : INativeObject<T>
{
return T.Create(handle);
}
interface INativeObject<TSelf> // : INativeObject
where TSelf : INativeObject<TSelf>?
{
static virtual TSelf? Create(IntPtr handle)
{
// Default reflection-based implementation?
var ctor = typeof(TSelf).GetConstructor(new Type[] { typeof(IntPtr) });
if (ctor != null)
return (TSelf)ctor.Invoke(new object[] { handle });
return default;
}
}
class NObject : INativeObject<NObject>
{
public NObject(IntPtr handle) { }
}
class NObject2 : INativeObject<NObject2>
{
private NObject2(IntPtr handle) { }
static NObject2 INativeObject<NObject2>.Create(IntPtr handle) => new NObject2(handle);
} I suppose that would work. Since |
Yup, something like that. Softening the blow by just adding the implementations using ILLink custom steps is a nice touch. It would still be nice if this became a future public API (requiring people with their own projections to implement the interfaces so that we can have a custom-step-less future), but maybe it can all be wrapped in custom steps for now. |
As far as I understand it, the managed static registrar, solves a lot of problems with dependency analysis for the ILCompiler. We generate UnmanagedCallersOnly methods (with explicit entry points) that provide all the links to the types and methods that aren't called from anywhere else in IL and we only call them from the Objective-C runtime. When we move on to remove the ILLinker and the custom linker steps in some next iteration (.NET 9+) we will need to rethink this part. Besides the "IntPtr constructors" that Filip mentioned, I can see a few other places where we use reflection in the registrar's code for smart enum converters and for block proxies. I'm not sure if we use it only when we generate the static registrar or if those are used at runtime. I think @rolfbjarne should know this for sure. Xamarin and MAUI use the
|
An additional consideration: If we want to get best out of trimming capabilities of both tools (when running them in cascade), we also need to consider the fact that when targeting iOS-like platforms the default trimming behaviour is This prevents ILC to do any additional trimming in this setup because:
This is fixable by providing the right set up feature switches at the right moment (just wanted to bring it up in the discussion). |
I looked at the spec, and it seems no visibility checks are needed for
Given that we're already modifying IL during the build, there's a simpler solution:
Now the proposal to use an interface with a static abstract Create method is also interesting, if only to avoid the reflection code path on other runtimes, so I'll have a look at that. |
The problem with the |
Could you point me to those? I'd really like us to get into a state where discussing additional means of rooting things is the last resort. I have a couple reasons:
I think spending some time on investigating the ways to get rid of the reflection could provide more visible immediate improvements even for customers that don't try the NativeAOT experiment.
My suggestion is to condition _AggressiveAttributeTrimming on PublishAot not being turned on in the SDK. Savings from aggressive attribute trimming are peanuts on NativeAOT - I've not seen this save more than 0.5%. Alternative we could fiddle with removing it from the feature switches when ILLinker sees it and adding it back for Native AOT but I don't know if it's really worth the effort. |
These two cases don't use reflection anymore when using the Managed Static Registrar (i.e. for NativeAOT).
We use reflection in the following scenarios: When instantiating any custom managed subclasses of NSObject from Objective-C.Say we have the following managed class: class MyObject : NSObject {
[Export ("initWithSomething:")]
public MyObject (int something) {}
} In this case we do something like: partial class MyObject {
class __RegistrarHelper_ {
[UnmanagedCallersOnly ("__MyObject_initWithSomething__")]
static void DoSomething (IntPtr handle, IntPtr selector, int arg1)
{
var obj = RuntimeHelpers.GetUninitializedObject (type);
var ctorHandle = ldtoken (MyObject..ctor);
var ctor = MethodBase.GetMethodFromHandle (ctorHandle);
obj.Handle = handle; // The Handle field needs to be set before calling the constructor
MyObject..ctor (arg); // IL: callvirt MyObject..ctor
}
}
} I don't think we have a way around using RuntimeHelpers.GetUninitializedObject, because we need to set the Handle field before NSObject's constructor is called (NSObject's constructor needs to know whether to create a native instance, or if a native instance already exists and we're just creating a managed wrapper instance around it). One idea might be to introduce a generic When calling managed methods on a generic type from Objective-C.Say we have the following managed class: class MyObject<T> : NSObject where T: NSObject {
[Export ("doSomething:")]
public void DoSomething () {}
} the generated UnmanagedCallersOnly method will be something like: partial class MyObject<T> {
class __RegistrarHelper_ {
[UnmanagedCallersOnly ("__MyObject_DoSomething__")]
static void DoSomething (IntPtr handle, IntPtr selector)
{
var obj = Runtime.GetNSObject (handle);
var openTypeHandle = ldtoken (MyObject<T>);
var openMethodHandle = ldtoken (MyObject<T>.DoSomething);
// Runtime.FindClosedMethod:
// https://github.com/xamarin/xamarin-macios/blob/3dd412daa6b5522029014b0be298f27d751c5f45/src/ObjCRuntime/Runtime.cs#L2182-L2189
MethodInfo closedMethod = Runtime.FindClosedMethod (obj, openTypeHandle, openMethodHandle);
closedMethod.Invoke (new object [] {});
}
}
} I don't think there's a way around using reflection here, since the UCO entry point doesn't know the closed generic type/method, and we need to figure out in order to call the target method. When instantiating a managed NSObject instance (or subclass) for an existing Objective-C objectSay 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 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)
Agreed - and that's really what the static registrar does, computes as much as possible during build, so that we can do as little as possible during app execution. Note that we haven't been able to spend much time in recent years to take advantage of improvements in the BCL and the runtimes, so there are certainly many locations where we can do better. |
Could we move the
Does the Objective-C code that calls the UCO know what the generic types are? Could it pass the "type ids" to the UCO as an argument and then use some generated lookup table to find the method we should call? |
A managed object can be created in two ways:
new NSString ("whatever); in this case we'll create a native NSString instance in the NSObject constructor (in AllocIfNeeded). Note that in this case there's no UCO method involved.
In this case we must not create anything in There is one rather ugly solution: inject a new constructor into the type we need to construct, and chained through a new constructor in all base classes until we reach a special constructor in NSObject. So something like this: class UIView : UIResponder {
public UIView (IntPtr handle, ... <other arguments to ensure this ctor doesn't have the same signature as an existing ctor>)
: base (handle, ... <other arguments>) {}
}
class UIResponder : NSObject {
public UIResponder (IntPtr handle, ... <other arguments to ensure this ctor doesn't have the same signature as an existing ctor>)
: base (handle, ... <other arguments>) {}
}
class NSObject {
public NSObject (IntPtr handle, ... <other arguments to ensure this ctor doesn't have the same signature as an existing ctor>)
{
// Don't call AllocIfNeeded here, because we know we already have a native object.
}
}
No, the only thing that knows the closed generic type is the managed instance the method is called on. |
Would this be fixable by just injecting one constructor? I don't think we need to chain it - we'd basically inject:
I don't have any good idea on what to do there. But this is not fundamentally trimming unsafe - we have a handle for the open method, we have an instance of the object, and we can therefore find the method. This should not be generating any trimming warnings. The implementation of Even though I don't have trimming/preservation concerns around this, this pattern will not win any perf prizes. Cc @jkoritzinsky @AaronRobinsonMSFT for ideas. I think in general we solve this by banning generics in other interops.
Having some form of lookup table for this would help, like you write. We could also generate "creator" methods that just call the right constructor based on a series of ifs. I don't know which one would be better. |
That's an interesting idea, it's curious what we can actually do in IL / when not limited to C#...
@simonrozsival had an interesting idea here, basically injecting an interface and using the vtable to indirectly lookup the closed generic type: https://dotnetfiddle.net/orpEt8 In any case we've documented this as being slow, and customers can easily make it faster if they wish, so I'm not all that concerned about speed. The main benefit would probably be to be able to trim away a lot of reflection code in the BCL. |
In the current setup with NativeAOT, during app build we run both ILLink and ILCompiler in cascade. When `_AggressiveAttributeTrimming` feature switch is set to `true` ILLink removes `IsTrimmable` attribute from assemblies, which means that when we are also running in `TrimMode=partial` (which translates to `--defaultrooting` ILC command-line arguments) ILC trimming is disabled completely. This PR disables `_AggressiveAttributeTrimming` in the first pass ie during trimming by ILLink and enables it in the second trimming pass performed by ILCompiler. Additionally, to workaround ILCompiler incompatibility with `Microsoft.iOS` (and friends) this platform assembly is explicitly rooted when passed to ILCompiler for trimming (this will be fixed once dotnet/runtime#86649 is resolved). Estimated savings: This change reduces the size of the application bundle by `0,58Mb` (or ~4,3% compared to the baseline) | MAUI ios app | Base | This PR | diff (%) | |--------------|-----------|-----------|----------| | SOD (Mb) | 41,93 | 40,5 | -3,4% | | .ipa (Mb) | 13,43 | 12,85 | -4,3% | Fixes: #18479 --------- Co-authored-by: Alex Soto <[email protected]>
I think we resolved all of the problematic cases and there's now individual issues tracking actual work. I'm going to close this but please reopen if something still needs attention in this issue. |
Description
Xamarin is heavily dependent on ILLinker and use of custom linker steps in mobile platform builds.
This dependency means that if we want to support building an application targeting iOS-like platforms with NativeAOT, we would have to run ILLinker and ILCompiler (NativeAOT's compiler and trimmer) in cascade during the build.
For such scenarios, the compatibility between the tools arises as an issue, for example:
System.Private.CoreLib.dll
trimmed by ILLinker cannot be used with ILCompiler, as it requiresArray<T>
type for compilation, which gets removed in the previous step.To use the full potential of both of the tools, we need to make them compatible (to some extent) for targeting mobile devices.
This issue has been opened: to identify how we can reduce the incompatibility gap, to discuss possible approaches, ideas and suggestions on how to get pass this limitation in general with a goal of enabling NativeAOT to work with Xamarin.
Implementation
The implementation of the identified work is being tracked: xamarin/xamarin-macios#18584
/cc: @rolfbjarne @simonrozsival
The text was updated successfully, but these errors were encountered: