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

Unintuitive value type field access in .NET Core 3.1 vs .NET 5 #57712

Closed
AaronRobinsonMSFT opened this issue Aug 19, 2021 · 5 comments
Closed

Comments

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Aug 19, 2021

While doing work to implement a ComWrappers solution it was discovered there is a discrepancy between .NET Core 3.1 and .NET 5. The .NET 5 behavior is what is expected. Consider the following code example which is similar to how one would construct a COM vtable. When run on .NET 5, the output is the expected "Two", but when running on .NET Core 3.1 the output is "One". This happens in both Debug and Release on .NET Core 3.1.

If the contents of A are inlined into B the output is the expected "One".

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    unsafe class Program
    {
        struct A
        {
            public delegate* managed<void> First;
        }

        struct B
        {
            public A Inst;
            public delegate* managed<void> Second;
        }

        static void One()
        {
            Console.WriteLine(nameof(One));
        }
        static void Two()
        {
            Console.WriteLine(nameof(Two));
        }

        static B* Alloc()
        {
            IntPtr* b = (IntPtr*)Marshal.AllocCoTaskMem(sizeof(B));
            b[0] = (IntPtr)(delegate* managed<void>)&One;
            b[1] = (IntPtr)(delegate* managed<void>)&Two;
            return (B*)b;
        }
        static void Main()
        {
            B* b = Alloc();

            // .NET Core 3.1 outputs "One"
            // .NET 5 outputs "Two"
            b->Second();
        }
    }
}

The disassembly for the calls:

.NET Core 3.1

            b->Second();
00007FF8CCE030A4 48 8B 45 30          mov         rax,qword ptr [rbp+30h]  
00007FF8CCE030A8 48 8B 00             mov         rax,qword ptr [rax]  
00007FF8CCE030AB 48 89 45 20          mov         qword ptr [rbp+20h],rax  
00007FF8CCE030AF 48 8B 45 20          mov         rax,qword ptr [rbp+20h]  
00007FF8CCE030B3 FF D0                call        rax  
00007FF8CCE030B5 90                   nop  

.NET 5

            b->Second();
00007FF8CCE28680 48 8B 45 38          mov         rax,qword ptr [rbp+38h]  
00007FF8CCE28684 48 8B 40 08          mov         rax,qword ptr [rax+8]  
00007FF8CCE28688 48 89 45 28          mov         qword ptr [rbp+28h],rax  
00007FF8CCE2868C 48 8B 45 28          mov         rax,qword ptr [rbp+28h]  
00007FF8CCE28690 FF D0                call        rax  
00007FF8CCE28692 90                   nop  

/cc @RussKie @jkoritzinsky @elinor-fung

@AaronRobinsonMSFT AaronRobinsonMSFT added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Aug 19, 2021
@ghost
Copy link

ghost commented Aug 19, 2021

Tagging subscribers to this area: @JulieLeeMSFT
See info in area-owners.md if you want to be subscribed.

Issue Details

While doing work to implement a ComWrappers solution it was discovered there is a discrepancy between .NET Core 3.1 and .NET 5. The .NET 5 behavior is what is expected. Consider the following code example which is similar to how one would construct a COM vtable. When run on .NET 5, the output is the expected "Two", but when running on .NET Core 3.1 the output is "One".

If the contents of A are inlined into B the output is the expected "One".

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    unsafe class Program
    {
        struct A
        {
            public delegate* managed<void> First;
        }

        struct B
        {
            public A Inst;
            public delegate* managed<void> Second;
        }

        static void One()
        {
            Console.WriteLine(nameof(One));
        }
        static void Two()
        {
            Console.WriteLine(nameof(Two));
        }

        static B* Alloc()
        {
            IntPtr* b = (IntPtr*)Marshal.AllocCoTaskMem(sizeof(B));
            b[0] = (IntPtr)(delegate* managed<void>)&One;
            b[1] = (IntPtr)(delegate* managed<void>)&Two;
            return (B*)b;
        }
        static void Main()
        {
            B* b = Alloc();

            // .NET Core 3.1 outputs "One"
            // .NET 5 outputs "Two"
            b->Second();
        }
    }
}

/cc @RussKie @jkoritzinsky @elinor-fung

Author: AaronRobinsonMSFT
Assignees: -
Labels:

area-CodeGen-coreclr

Milestone: -

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Aug 19, 2021
@jkoritzinsky
Copy link
Member

I think this is probably related to how function pointers types weren’t considered blittable until after 3.1, so the managed layout might not be sequential. #37295

@AaronRobinsonMSFT
Copy link
Member Author

@jkoritzinsky Good call. I think you're right.

@jkotas jkotas added area-Interop-coreclr and removed area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI labels Aug 19, 2021
@AaronRobinsonMSFT
Copy link
Member Author

Have confirmed that converting the function pointers to IntPtr do make this work.

@AaronRobinsonMSFT AaronRobinsonMSFT removed the untriaged New issue has not been triaged by the area owner label Aug 19, 2021
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the Future milestone Aug 19, 2021
@AaronRobinsonMSFT
Copy link
Member Author

Fix was already considered in dotnet/coreclr#28046 and rejected.

@ghost ghost locked as resolved and limited conversation to collaborators Sep 18, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants