dadhi / DryIoc

DryIoc is fast, small, full-featured IoC Container for .NET
MIT License
1.03k stars 122 forks source link

Compatibility with Android/iOS AOT compilation #552

Open breenbob opened 1 year ago

breenbob commented 1 year ago

Prior to the removal of the .WithoutFastExpressionCompiler rule, DryIoc used to work fine in Xamarin.Android, Xamarin.iOS, and I suspect .Net Maui Android/iOS apps also. The introduction of fast expression compilation seems (to me) to be the point at which code that depended on availability of the JIT compiler to run was introduced.

As per the documentation on limitations of Xamarin.iOS, iOS apps must use AOT compilation and as such do not support aspects of generics and reflection that depend on JIT compilation.

I have run into this issue when using the Prism.DryIoc.Maui package:

Attempting to JIT compile method 
'FastExpressionCompiler.LightExpression.ExpressionCompiler:TryCompile (FastExpressionCompiler.LightExpression.ExpressionCompiler/ClosureInfo&,
    System.Type,System.Type[],
    System.Type,FastExpressionCompiler.LightExpression.Expression,
    System.Collections.Generic.IReadOnlyList`1<FastExpressionCompiler.LightExpression.ParameterExpression>,bool)' 
while running in aot-only mode. 
See https://docs.microsoft.com/xamarin/ios/internals/limitations for more information.

The issue itself seems to stem from DryIoc and the FastCompilers packages in their dependency on these unsupported aspects.

This documentation also suggests marking methods that rely on these with one of the following attributes would bypass this problem: UnmanagedFunctionPointerAttribute (preferred, since it is cross-platform and compatible with .NET Standard 1.1+) MonoNativeFunctionWrapperAttribute

It states that failing to provide one of these attributes will result in a runtime error exactly like I am seeing in my iOS app above.

As an experiment I have tried forking DryIoc from v5.1.0 (version referenced by Prism.DryIoc.Maui) and re-adding the dropped commit for .WithoutFastExpressionCompiler. This was painstaking to say the least! I pushed the packed nuget for this to a private repo and referenced from there. After updating Prism.DryIoc.Maui and my app to use the updated packages, my app still crashed with a similar error to above... but it was in a different method this time, nothing to do with the fast compiler stuff:

Attempting to JIT compile method
'(wrapper delegate-invoke) DryIoc.IScope <Module>
  :invoke_callvirt_IScope_IResolverContext (DryIoc.IResolverContext)'
while running in aot-only mode.
See https://docs.microsoft.com/xamarin/ios/internals/limitations for more information.

So I am guessing DryIoc now has other areas in the code also dependent on JIT compilation added since this feature was removed that these changes alone do not cover.

Is it possible for support for AOT compilation to be added to DryIoc?

Note, best I can tell this issue does not seem to occur in Prism.Forms, which references DryIoc v4.7.7.

Any help or hints greatly appreciated!

dadhi commented 1 year ago

@breenbob Hi, thanks for thorough analysis. I am struggling to find the cause before, so hopefully I will find something now with your details.

breenbob commented 1 year ago

To be honest I was aware of the limitations around AOT/generics, but I wasn't aware of those attributes until I went searching for the docs to link here. I will give them a go in my fork of 5.1.0 and let you know on here how that works out for me. Thanks for considering this issue, and the great library!

breenbob commented 1 year ago

Theoretically, if they work, I am guessing it would mean no need to re-introduce WithoutFastExpressionCompiler functionality, but instead updating the FastExpressionCompiler library to use same annotations..

breenbob commented 1 year ago

After reading the docs some more, I came across this:

Rules.WithUseInterpretation

The compilation (essentially `System.Reflection.Emit`) is not supported by all targets, e.g. by the Xamarin iOS. In this case you may specify to always use the interpretation via the rule: 

var c = new Container(rules => rules.WithUseInterpretation());

DryIoc uses its own interpretation mechanism which is faster than `System.Linq.Expressions.Expression.Compile(preferInterpretation: true)` because DryIoc can recognize its own internal methods in the resolved expression tree, and call them directly without reflection. It has other optimizations as well.

I knew Prism.Maui did not set this rule by default, so I tried adding it in a new branch of Prism.Maui referencing the stock v5.1.0 of DryIoc.dll from Nuget (as opposed to my fork). Pushed the Prism Nugets to private repo, referenced them in my app, did a clean/build & run in Release mode on iOS to ensure AOT compilation, and my issue is resolved!

Would like your opinion @dadhi but pretty sure that means this issue can be closed?

I will PR the fix into Prism.Maui separately.

dadhi commented 1 year ago

@breenbob Huh, sorry :-( I was for some reason assuming that WithUseInterpretation() is used. Yes, this is the way to solve it.

In the related issues, I wanted to improve the situation by recognizing the target is not supporting System.Reflection.Emit and setting it in the container automatically. But I did not find the way.

Maybe you will have a suggestion, as someone really working on these targets. It may be some environment check, some reflection probe for missing property, method or class... or preferably more robust modern way?

breenbob commented 1 year ago

Yes sorry, I didn't write the package and not really looked at this before.

I'm honestly not sure if there is anyway to detect AOT compilation, either directly or by proxy. I do know there is a csproj setting that gets enabled for this by default in Release build configurations: <RunAOTCompilation>true</RunAOTCompilation>

But doubt there is any way to check that at runtime. Perhaps some msbuild magic...

Whether you can check for unsupported aspects using reflection, I just don't know, but because of AOT compilation coming into play again I would doubt it.

All I can suggest (for Xamarin/.Net Maui at least) is to do something like this:

#if RELEASE && (ANDROID || IOS || MACCATALYST)
defaultRules = defaultRules.WithUseInterpretation();
#endif

But that would not be fool proof, e.g. a custom Release build configuration where the standard Release constant has not been defined. We do this sometimes to have separate configs for CI vs App Store releases...

dadhi commented 1 year ago

@breenbob This one looks promising:

#if RELEASE && (ANDROID || IOS || MACCATALYST)
defaultRules = defaultRules.WithUseInterpretation();
#endif

Maybe RELEASE is not required here, because why care for the compilation in the DEBUG. I think from the debugging perspective it even better to have the interpretation, because it is more consistent experience (no compilation jump).

Then the next question, how to approach it when building the DryIoc library.

What are your thoughts on this? What targets are you using personally, what .net?

kyurkchyan commented 1 year ago

I am facing this same issue. I am on the same page with @breenbob using Prism, Maui and DryIoc. @breenbob did you manage to create a PR for Prism, or perhaps you found a different way of resolving this issue?

kyurkchyan commented 1 year ago

For those who may come across this thread. I found a solution with Prism.Maui and DryIoc.

Luckily there's an overload of UserPrism which takes the DryIoc Rules. So, I created rules and passed them to that extension method. IOS/Android apps in Release/AOT mode are not crashing anymore.

private static Rules DefaultRules => Rules.Default.WithConcreteTypeDynamicRegistrations(reuse:Reuse.Transient)
                                                  .WithUseInterpretation();

.....

var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .UsePrism(DefaultRules, Startup.SetupPrismApplication)
....