Skip to content

A function calling Assembly.GetExecutingAssembly() gets inlined across assembly boundaries #9283

Closed

Description

@ForNeVeR

Repro steps

  1. Create two projects: one exposing a function let getName() = Assembly.GetExecutingAssembly().GetName(), and another one using that function
  2. Run the projects in Debug: name of the first project will be returned
  3. Run the projects in Release: the call will be inlined, so the name of the second project will be returned

Here's a sample project with reproduction; just in case, I'll attach all the sources as a ZIP archive, too: fsharp-inlining-bug-master.zip

For ease of reading, here're the main source files:

// projecta/Program.fs
module ProjectA

open System.Reflection

let getName() =
    Assembly.GetExecutingAssembly().GetName()
// projectb/Program.fs
module ProjectB

open System.Reflection

let getName() =
    Assembly.GetExecutingAssembly().GetName()

[<EntryPoint>]
let main argv =
    printfn "Hello World from F#!"
    printfn "ProjectA: %A" <| ProjectA.getName()
    printfn "ProjectB: %A" <| getName()
    0

Output:

$ dotnet run --project projectb --configuration Debug
Hello World from F#!
ProjectA: projecta, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ProjectB: projectb, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
$ dotnet run --project projectb --configuration Release
Hello World from F#!
ProjectA: projectb, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
ProjectB: projectb, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null

Expected output: in both cases, the same:

Hello World from F#!
ProjectA: projecta, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ProjectB: projectb, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null

Known workarounds

Mark a function containing a call to Assembly.GetExecutingAssembly() as [<MethodImpl(MethodImplOptions.NoInlining)>].

Related information

  • Operating system: Windows 10
  • .NET Runtime: .NET Core SDK 3.1.201

Activity

  1. changed the title [-]Assembly.GetExecutingAssembly() gets inlined across assembly boundaries[/-] [+]A function calling Assembly.GetExecutingAssembly() gets inlined across assembly boundaries[/+] on May 24, 2020
  2. abelbraaksma commented on May 24, 2020

    @abelbraaksma
    Contributor

    I think this is a feature and not a bug. Assembly.GetExecutingAssembly() is defined to walk the stack to find out where it is. If functions get inlined, either by JIT (which I think happens here) or F# (when using inline implicitly or explicitly), the stack changes, and so does the result of this call. This is also suggested in this SO answer from 2013: https://stackoverflow.com/a/19845144/111575.

    In my local logging framework I exploit this behavior to get the executing assembly from the call to the log, not the log assembly itself. Changing this behavior would break that code and possibly others that use similar techniques.

    You probably want to use Type.GetExecutingAssembly instead. It doesn't have the same issue, I believe.

  3. ForNeVeR commented on May 25, 2020

    @ForNeVeR
    ContributorAuthor

    @abelbraaksma

    If functions get inlined, either by JIT (which I think happens here)

    No, in this case, the function is inlined by the F# compiler; I was able to confirm this hypothesis by reading the decompiled code of my assembly. The entry point looks like this:

    	public static int main(string[] argv)
    	{
    		PrintfFormat<Unit, TextWriter, Unit, Unit> printfFormat = new PrintfFormat<Unit, TextWriter, Unit, Unit, Unit>("Hello World from F#!");
    		PrintfModule.PrintFormatLineToTextWriter<Unit>(Console.Out, printfFormat);
    		PrintfFormat<FSharpFunc<AssemblyName, Unit>, TextWriter, Unit, Unit> printfFormat2 = new PrintfFormat<FSharpFunc<AssemblyName, Unit>, TextWriter, Unit, Unit, AssemblyName>("ProjectA: %A");
    		PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<AssemblyName, Unit>>(Console.Out, printfFormat2).Invoke(Assembly.GetExecutingAssembly().GetName());
    		printfFormat2 = new PrintfFormat<FSharpFunc<AssemblyName, Unit>, TextWriter, Unit, Unit, AssemblyName>("ProjectB: %A");
    		PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<AssemblyName, Unit>>(Console.Out, printfFormat2).Invoke(Assembly.GetExecutingAssembly().GetName());
    		return 0;
    	}

    Here, the first call to Assembly.GetExecutingAssembly().GetName() is, in fact, inlined code from another F# assembly.

    Also, I believe that Assembly.GetExecutingAssembly is marked by a special attribute (System.Security.DynamicSecurityMethod) to avoid this JIT behavior:

    All methods that use StackCrawlMark should be marked with this attribute. This attribute disables inlining of the calling method to allow stackwalking to find the exact caller.

    It may or may not be a problem that I wasn't able to find DynamicSecurityMethod attribute on the Assembly.GetExecutingAssembly in the ref assemblies I (seemingly) compile my .NET Core code against.

    I mean, if F# compiler starts respecting this attribute at some point (currently it seems it doesn't), its absence from the ref assemblies may impose a problem.

    In my local logging framework I exploit this behavior to get the executing assembly from the call to the log, not the log assembly itself.

    How do you work around the fact that this approach doesn't work in Debug mode?

    You probably want to use Type.GetExecutingAssembly instead. It doesn't have the same issue, I believe.

    You probably meant Type.GetAssembly(), and, yes, this is a viable work around this particular problem, too. But sometimes this itself may be a problem, since it's not that easy to get any meaningful type from an F# assembly, e.g. typeof<MyModule> doesn't work.

    To conclude, currently I believe this is, in fact, a bug and not a feature. F# compiler inlines the code that shouldn't be inlined, as described by DynamicSecurityMethod documentation, which leads to unreliable runtime behavior (different between debug and release builds). Any method calling a method marked with DynamicSecurityMethod shouldn't be inlined — either by JIT, or by the F# compiler (since it moslty respects attributes used to control JIT).

    Not a big deal? Probably. A feature? Nope.

  4. KevinRansom commented on May 26, 2020

    @KevinRansom
    Contributor

    @ForNeVeR
    To disable compiler inlining use: [<MethodImpl(MethodImplOptions.NoInlining)>]

    Try this:

    // projecta/Program.fs
    module ProjectA
    
    open System.Reflection
    open System.Runtime.CompilerServices
    
    [<MethodImpl(MethodImplOptions.NoInlining)>]
    let getName() =
        Assembly.GetExecutingAssembly().GetName()
    

    Produces this output on retail build:

    Hello World from F#!
    ProjectA: ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    ProjectB: ConsoleApp5, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    
  5. abelbraaksma commented on May 27, 2020

    @abelbraaksma
    Contributor

    How do you work around the fact that this approach doesn't work in Debug mode?

    @ForNeVeR, I was wrong, the code uses Assembly.GetCallingAssembly instead, and to find the line number it injects a dummy function (and extracts the line number from that), and further some magic to distinguish between lambdas and local functions, to get the relevant function name.

    All these methods are marked debug-only, so I'm doing the inverse: when in debug mode, I know certain functions are not erased or inlined (unless inline is present) and leverage that info. The release-mode log functions don't do this, as the same trick wouldn't work.

    Sorry for the clutter, as I deliberately wrote in my code not to use Assembly.GetExecutingAssembly. I searched for the places where I do use it, and these are all module-level let-bindings. Since these are never inlined anyway, and will only execute once, this gives me the right assembly (which is, btw, another way to work around the inlining issue).

    Not a big deal? Probably. A feature? Nope.

    Totally in agreement there now, thanks for the insights!

  6. ForNeVeR commented on May 27, 2020

    @ForNeVeR
    ContributorAuthor

    @KevinRansom, thanks, I confirm that this workaround works (and already mentioned it in my opening post). I do still believe the current behavior is an issue, but I don't think it's a critical one.

  7. KevinRansom commented on May 27, 2020

    @KevinRansom
    Contributor

    If you don't mind, I'm going to close this, everything is working as it is intended.

    Thanks for the report.

  8. abelbraaksma commented on May 27, 2020

    @abelbraaksma
    Contributor

    @KevinRansom, I don't think it is working as expected.

    You wrote:

    To disable compiler inlining use: [<MethodImpl(MethodImplOptions.NoInlining)>]

    But in this case, the method Assembly.GetExecutingAssembly is actually marked with precisely that, but this isn't honored:

    [MethodImpl(MethodImplOptions.NoInlining)]
    [SecuritySafeCritical]
    [__DynamicallyInvokable]
    public static Assembly GetExecutingAssembly()
    {
    	StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
    	return RuntimeAssembly.GetExecutingAssembly(ref stackMark);
    }

    And:

        // DynamicSecurityMethodAttribute:
        //  All methods that use StackCrawlMark should be marked with this attribute. This attribute
        //  disables inlining of the calling method to allow stackwalking to find the exact caller.
        //
        //  This attribute used to indicate that the target method requires space for a security object
        //  to be allocated on the callers stack. It is not used for this purpose anymore because of security
        //  stackwalks are not ever done in CoreCLR.

    And I can confirm, by looking at the generated IL from the FSC compiler that @ForNeVeR is correct that the method that uses GetExecutingAssembly gets inlined indeed, and the NoInlining from the BCL library seems to have not been taken into account.

    Yes, the workaround is to add NoInlining to your own function, but surely, that oughtn't be needed? Or I'm missing something in how NoInlining is supposed to work.

  9. ForNeVeR commented on May 30, 2020

    @ForNeVeR
    ContributorAuthor

    I agree with the previous comment. It looks like this is a compiler bug (or, let's say, an open issue in compiler), even if a minor one. I think it should be reopened, if there's no policy to close minor/archived/postponed bugs.

  10. cartermp commented on May 30, 2020

    @cartermp
    Contributor

    Yeah, this looks like a bug.

  11. added this to the Backlog milestone on May 30, 2020
  12. 22 remaining items

  13. teo-tsirpanis commented on Sep 3, 2020

    @teo-tsirpanis
    Contributor

    If we optimize and inline small methods not marked inline, we should use a deterministic other way to find out whether it's eligible or not.

    And I was ready to create an issue about that...

    My opinion is that inlining functions at the compiler-level without the inline keyword is a bad idea. It affects generated code size and such automatic inlining for the purposes of optimization is the job of the JIT, not the compiler. It also widens the gap between the IL code the developer expects to be generated, with the one that gets actually generated.

    However, such automatic inlining looks OK for limited cases like small nested functions or active patterns.

    Automatically inlining functions across assembly boundaries, like what this issue is concerned with, is a terrible idea because it breaches the barrier of abstraction between assemblies and should never be performed without the inline keyword.

  14. KevinRansom commented on Sep 3, 2020

    @KevinRansom
    Contributor

    @teo-tsirpanis -- I don't know that I agree that it is a terrible idea, however, it is often quite inconvenient, and makes the source code a lot harder to reason about. I think that having fslint tell developers this is an iffy construct and they should use the noinlining attribute is the best result we can do right now. For sure turning off automagic inlining would ensure that the performance characteristics of most code would change, and probably not for the better.

  15. jl0pd commented on Oct 29, 2021

    @jl0pd

    I agree with Teo that automatic cross-assembly inlining is bad idea. Code tends to change and once correct code can cause MissingMethodException/TypeLoadException or old/incorrect behavior. Example: application with plugins - you can't control all code and recompile every library. As developer you don't change public api, but F# compiler goes through public methods and can inline internal/private code.

    Maybe compiler should have flag to control implicit inlining, e.g. none, projectReferences, all: <ImplicitInlines>none</ImplicitInlines>. By default set it to projectReferences as safe version, but people could extend it to all references to improve performance

  16. dsyme commented on Oct 29, 2021

    @dsyme
    Contributor

    You can disable cross-assembly inlining using --crossoptimize-

    Libraries can suppress cross-assembly inline information using --nooptimizationdata

  17. abelbraaksma commented on Feb 23, 2022

    @abelbraaksma
    Contributor

    The dotnet bug report led to a new API proposal that would allow to implicitly pass caller identity, which would ultimately be an alternative for the flawed behavior explained in this issue, see: dotnet/csharplang#4984.

  18. KevinRansom commented on Mar 14, 2023

    @KevinRansom
    Contributor

    I am going to close this, there is nothing really for us to do. To address this a developer can add [<MethodImpl(MethodImplOptions.NoInlining)>], there's no reliable attribute for us to detect, and lists of well-known APIs are always difficult to reason about and maintain, and the developer can also suppress cross-assembly inlining when needed.

    Thanks for the discussion

    Kevin

  19. abelbraaksma commented on Mar 21, 2023

    @abelbraaksma
    Contributor

    To address this a developer can add [<MethodImpl(MethodImplOptions.NoInlining)>],

    @KevinRansom, the point of this whole discussion and issue was, that NoInlining does not work, except only partially. Unless that has been fixed meanwhile. I.e.:

    See this comment for a summary: #9283 (comment) and this:

    As you said, NoInlining is not transitive, but security-criticality is

    I'm not saying it is wrong to close this, I consider it "not worth the effort" (however much this makes GetExecutingAssembly unreliable), but C# solved it, so why can't F#? But IIRC, there's an effort underway in .NET to use caller identity: dotnet/csharplang#4984, which would allow for a reliable workaround. It may not happen before .NET 8 though.

  20. abelbraaksma commented on Mar 21, 2023

    @abelbraaksma
    Contributor

    Even if we wanted to fix this behavior, not having reqsecobj in reference assemblies makes this next to impossible or really awkward (#9283 (comment)).

    It is sad, but looks like you are right in that there's nothing we can do here...

  21. ForNeVeR commented on Mar 23, 2023

    @ForNeVeR
    ContributorAuthor

    C#, notably, "solved" it by not having inlining at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-Compiler-OptimizationThe F# optimizer, release code gen etc.BugImpact-Medium(Internal MS Team use only) Describes an issue with moderate impact on existing code.

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions