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

Capability APIs for runtime code generation #25959

Closed
morganbr opened this issue Apr 21, 2018 · 31 comments
Closed

Capability APIs for runtime code generation #25959

morganbr opened this issue Apr 21, 2018 · 31 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.CompilerServices
Milestone

Comments

@morganbr
Copy link
Contributor

We believe we need to add reflection emit to .NET Standard.

We already have features in .NET Standard that imply support for dynamic code generation, for example Assembly.Load(byte[]), RegexOptions.Compiled, and LambdaExpression.Compile().

Today, developers have no way to test whether runtime code generation is actually supported -- the runtime might throw PlatformNotSupported. Furthermore, even if it is supported, it might be slower as the runtime is interpreting the code (for example, by walking the expressions trees or interpreting the IL). While that ensures that scenarios that depend on code generation just work, it makes it hard to support scenarios that use code generation as a performance optimization. In those cases, not using code generation is usually faster.

Hence, we need an API that allows developers to check for the support of code generation as well as an API that allows developers to check using runtime code generation as a performance optimization is sensible.

API Proposal

namespace System.Runtime.CompilerServices
{
    public static class RuntimeFeature
    {
        public static bool IsDynamicCodeSupported { get; }
        public static bool IsDynamicCodeCompiled { get; }
    }
}

The semantics are:

  • If the runtime supports loading and running dynamic code, IsDynamicCodeSupported returns true. This doesn't tell developers whether the code is interpreted or compiled.
  • If the runtime supports loading and running dynamic code by using a just-in-time compiler, IsDynamicCodeCompiled returns true.
  • RuntimeFeature.IsSupported(string) also returns the appropriate value when called with the strings "IsDynamicCodeSupported" and "IsDynamicCodeCompiled".

Affected features

  • Loading assemblies from bytes (Assembly.Load(byte[]))
  • Linq expression trees
  • Reflection emit
  • DynamicMethod (lightweight code generation, LCG)
  • DispatchProxy
  • Regex

Scenarios

Failing early with RuntimeFeature.IsDynamicCodeSupported

In some scenarios, performance isn't the primary goal but use to get stuff to work, for example, mocking. A mocking framework might provide an API like Proxy.Create<TInterface>() that returns you a type that implements TInterface. In this case, performance isn't the higher order and the only thing that matters is that the runtime supports generating code on the fly. Some environments, for example, iOS, disallow actual JITs. For that reason, the Xamarin team is considering providing an IL interpreter to make the scenarios work.

The author of the mocking framework could just use reflection emit and rely on the implementation of it to throw PlatformNotSupported but a much better developer experience would be to check early & fail early with a descriptive error message. For example:

public static class Proxy
{
    public static Proxy<TInterface> Create<TInterface>()
    {
        if (!RuntimeFeature.IsDynamicCodeSupported)
            throw new PlatformNotSupportedException("We cannot create proxies on your runtime because it doesn't allow dynamic code generation.");

        return UseReflectionEmitToCreateImplementation();
    }
}

Accelerating using RuntimeFeature.IsDynamicCodeCompiled

In most cases, dynamically generating code is meant as a performance optimization. This

var options = RegexOptions.IgnoreCase | RegexOptions.Singleline;

// We only want to use compiled regexes if the underlying runtime
// supports compiling IL with a JIT. Otherwise it's likely much slower
// than walking the state machine.

if (RuntimeFeature.IsDynamicCodeCompiled)
{
    options |= RegexOptions.Compiled;
}

Similar areas include using expression trees to accelerate setting properties and calling constructors in IoC systems.

@morganbr morganbr self-assigned this Apr 21, 2018
@morganbr morganbr changed the title API Proposal: RuntimeFeature.HasJit and RuntimeFeature.CanEmitCode API Proposal: RuntimeFeature.HasJit and RuntimeFeature.CanEmitIL Apr 21, 2018
@jkotas
Copy link
Member

jkotas commented Apr 21, 2018

We have duplicate issues of this:

https://github.com/dotnet/corefx/issues/13535
https://github.com/dotnet/corefx/issues/25530

The preferred design for API availability checks in earlier discussions was to just add static IsSupported property to the relevant API, like AssemblyBuilder.IsSupported, DynamicMethod.IsSupported or Registry.IsSupported. This pattern scalles well to 3rd party APIs.

RuntimeFeature.HasJit makes sense to me.

@morganbr
Copy link
Contributor Author

The IsSupported pattern would make this proposal look like this (with an IsSupported method for all entrypoint classes for reflection emit). What do folks think?

namespace System.Runtime.CompilerServices
{
    public class RuntimeFeature
    {
        public static bool HasJit { get; }
    }
}

namespace System.Reflection.Emit
{
    public class AssemblyBuilder
    {
        public static bool IsSupported { get; }
    }

    public class DynamicMethod
    {
        public static bool IsSupported { get; }
    }

    public class CustomAttributeBuilder
    {
        public static bool IsSupported { get; }
    }
}

@jkotas
Copy link
Member

jkotas commented Apr 23, 2018

LGTM.

I am not sure whether we need CustomAttributeBuilder.IsSupported. Can CustomAttributeBuilder be ever meaningfully used without AssemblyBuilder?

@morganbr
Copy link
Contributor Author

We could probably do without CustomAttributeBuilder.IsSupported. On closer inspection, the only thing it does by itself is build a binary representation of the custom attribute. While it's not in CoreRT right now, it could be as long as we drop the internal methods used by other parts of reflection emit.

After a bit of digging, we could potentially need more IsSupported APIs to cover downstream usage refemit. For example, Regex and System.Composition have APIs that depend on it. Would we want some sort of support check on those as well?

@morganbr
Copy link
Contributor Author

CC @davidfowl, who could hopefully use this in ASP.NET.

@jkotas
Copy link
Member

jkotas commented Apr 23, 2018

Yes, I think having a property like bool Regex.IsCompiledSupported would make sense.

@davidfowl
Copy link
Member

We should walk through the flow chart on what this would look like for our DI container. Today we have a couple of different modes (https://github.com/aspnet/DependencyInjection/blob/943ffa1187399ad4c2af0d23641af9804eacd4e6/src/DI/ServiceProviderMode.cs#L6):

  • Dynamic - Use reflection the first 2 times somebody asks to resolve a service, then switch then compile it in the background after that using the most efficient method (Ref Emit, or Compiled expressions)
  • Runtime - Always use reflection when resolving services
  • Expressions - Compile the expression tree before resolving service
  • ILEmit - Compile a dynamic method before resolving service

Here's what I'm thinking the flow control would be:

// Set the default mode to runtime if there's no JIT
https://github.com/aspnet/DependencyInjection/blob/06e2de235ce5b27b425e823d9dcbd045811ba48e/src/DI/ServiceProviderOptions.cs#L21

Mode = RuntimeFeature.HasJit ? ServiceProviderMode.Dynamic : ServiceProviderMode.Runtime;

https://github.com/aspnet/DependencyInjection/blob/06e2de235ce5b27b425e823d9dcbd045811ba48e/src/DI/ServiceLookup/ServiceProviderEngine.cs#L23

// This name should be CompiledBuilder
ExpressionBuilder = DynamicMethod.IsSupported ? new ILEmitResolver(...) : new CallSiteExpressionBuilder(...);

/cc @pakrym for DI
/cc @JamesNK (he originally asked for this) for JSON.NET

@morganbr
Copy link
Contributor Author

@davidfowl, now that I think about it a bit more, is DynamicMethod.IsSupported useful or should we only have RuntimeFeatures.HasJit? In other words if refemit is interpreted, would you ever want to use it?

@davidfowl
Copy link
Member

@davidfowl, now that I think about it a bit more, is DynamicMethod.IsSupported useful or should we only have RuntimeFeatures.HasJit? In other words if refemit is interpreted, would you ever want to use it?

No we'd never want to use it if it was interpreted in this scenario unless it was faster than reflection.

@JamesNK
Copy link
Member

JamesNK commented Apr 25, 2018

I want a way to figure out the fastest way to do reflection.

Compiled expressions are faster than traditional MethodInfo.Invoke reflection
But MethodInfo.Invoke reflection is faster that interpreted expressions.

A runtime way to figure out which to choose out of the two - basically are expressions going to be compiled or interpreted - is what I want.

@morganbr
Copy link
Contributor Author

Ok, let's just go with RuntimeFeatures.HasJit until/unless we see a useful scenario for using RefEmit with no JIT.

@morganbr morganbr changed the title API Proposal: RuntimeFeature.HasJit and RuntimeFeature.CanEmitIL API Proposal: RuntimeFeature.HasJit Apr 25, 2018
@morganbr
Copy link
Contributor Author

I incorrectly thought RuntimeFeatures was public. Would System.Runtime.InteropServices.RuntimeInformation be the right public class @stephentoub?

@jkotas
Copy link
Member

jkotas commented Apr 26, 2018

@morganbr
Copy link
Contributor Author

Ah, right you are. I just didn't find it in the contract sources due to magic in how System.Runtime is built. In that case, I'll go ahead and mark this ready for review.

@terrajobst
Copy link
Member

A few thoughts:

  • RuntimeFeature is mostly for static discovery of runtime features
  • This is a dynamic feature discovery. It seems this should be DynamicMethod.IsSupported and AssemblyBuilder.IsSupported.
  • We still have to figure out how either of these two can be used in the context of .NET Standard because currently you cannot consume either of these packages.

This needs a broader discussion. I'll invite @morganbr for the next one.

@morganbr
Copy link
Contributor Author

@terrajobst , the point of HasJit is that some features, like compiled LINQ expressions may work on AoT, but they're slow. That might eventually be true of DynamicMethod/Assemblybuilder as well. In those cases, IsSupported would be true, but a library like ASP.NET or Json.NET would want to find out whether they make code faster than reflection (because there's a JIT) or slower (because they're interpreted).

I'm open to putting HasJit on a different class.

@JamesNK
Copy link
Member

JamesNK commented May 22, 2018

This is a dynamic feature discovery. It seems this should be DynamicMethod.IsSupported and AssemblyBuilder.IsSupported.

I wanted to use a flag like this to test whether System.Linq.Expressions would get compiled or interpreted after calling Compile(). This flag isn't just specific to DynamicMethod so I don't think it should be placed on DynamicMethod..

@jkotas
Copy link
Member

jkotas commented May 22, 2018

We still have to figure out how either of these two can be used in the context of .NET Standard

The proposed API is for inclusion in the .NET Core platform. If somebody wants to build a package with API that does best effort approximation of this API for existing .NET Standard 2.0 platforms, that's fine but it should be different assembly and package name that does not unify with the .NET Core platform one.

@morganbr
Copy link
Contributor Author

I'd rather have HasJit in .NET Standard, particularly since it's relevant in UAP. AssemblyBuilder/DynamicMethod are different and not part of this proposal.

@jkotas
Copy link
Member

jkotas commented May 22, 2018

If we agree on a reasonable API, I am sure it will be included in .NET Standard 3.0. .NET Standard 2.0 is done. There is no way to add new APIs to it.

@morganbr
Copy link
Contributor Author

Agreed. We'll just need to bring it to UAP as well as .NET Core (and perhaps some Xamarin flavors too).

@davidfowl
Copy link
Member

So is the api ready for review?

@terrajobst
Copy link
Member

terrajobst commented Sep 25, 2018

Video

This is related: dotnet/standard#832

I believe this feature needs a more comprehensive design. I'll follow-up.

@terrajobst terrajobst changed the title API Proposal: RuntimeFeature.HasJit Capability APIs for runtime code generation Oct 19, 2018
@terrajobst
Copy link
Member

I've updated the proposal based on our meeting notes. I hope @morganbr doesn't mind it shows up as his :-)

@morganbr
Copy link
Contributor Author

Heh, the update is fine (though I don't think there's actually a penalty for using compiled regex, it just uses the state machine).

@marek-safar
Copy link
Contributor

The existing design of the class is using string constants, is there any reason we are deviating from that and switching to bool properties instead?

@terrajobst
Copy link
Member

terrajobst commented Oct 20, 2018

The string constants are meant for static discovery of runtime features. I believe these capabilities are inherently dynamic, so making them string constants feels wrong (note that the presence of the string constants presupposes the runtime must return true from IsSupported(string)).

jkotas referenced this issue in jkotas/coreclr Dec 3, 2018
jkotas referenced this issue in jkotas/coreclr Dec 3, 2018
jkotas referenced this issue in jkotas/coreclr Dec 4, 2018
jkotas referenced this issue in dotnet/coreclr Dec 4, 2018
jkotas referenced this issue in dotnet/corefx Dec 4, 2018
Contributes to dotnet/corefx#29258

Signed-off-by: dotnet-bot <[email protected]>
jkotas referenced this issue in dotnet/corert Dec 4, 2018
Contributes to dotnet/corefx#29258

Signed-off-by: dotnet-bot <[email protected]>
jkotas referenced this issue in jkotas/corefx Dec 8, 2018
jlennox referenced this issue in jlennox/corefx Dec 16, 2018
Contributes to dotnet/corefx#29258

Signed-off-by: dotnet-bot <[email protected]>
@pakrym
Copy link
Contributor

pakrym commented Jan 9, 2019

Are we going to backport the implementation of RuntimeFeature.IsSupported to 2.1/2.2?

If we don't, any app that's targeting netcoreapp lower than 3.0 and using RuntimeFeature.IsSupported("IsDynamicCodeCompiled") would be told that compilation is not supported.

@darkl
Copy link

darkl commented Dec 27, 2019

Hi @terrajobst,
Can I use RuntimeFeature to detect whether DispatchProxy is supported on the current platform?

Thanks and happy holidays
Elad

@darkl
Copy link

darkl commented Jan 3, 2020

Anyone else who knows the answer is also welcome to reply :)

@jkotas
Copy link
Member

jkotas commented Jan 3, 2020

@darkl We are not monitoring old closed issues, and your question is somewhat unrelated to what this issue about. If you would like to get answer to your question, open a new question issue in https://github.com/dotnet/runtime

@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 3.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 17, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.CompilerServices
Projects
None yet
Development

No branches or pull requests

9 participants