dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.27k stars 4.73k forks source link

`UnsafeAccessor` works with AOT but does not work with JIT for a type of Class<float> shape #108046

Closed neon-sunset closed 1 month ago

neon-sunset commented 1 month ago

Description

It appears that UnsafeAccessor may not be working on .NET 9 as expected with JIT configuration, but continues to work with NativeAOT.

Reproduction Steps

Given simple program created from a default console template:

using System.Runtime.CompilerServices;

var holder = new Holder<float>();
Console.WriteLine(ArrayRef(holder).Length); // 8

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "array")]
static extern ref float[] ArrayRef(Holder<float> instance);

class Holder<T>
{
    T[] array = new T[8];
}

Expected behavior

> dotnet run -c Release -f net9.0
8

Actual behavior

> dotnet run -c Release net8.0
8
> dotnet run -c Release net9.0
Unhandled exception. System.MissingFieldException: Field not found: 'Holder`1.array'.
   at Program.<<Main>$>g__ArrayRef|0_0(Holder`1 instance)
   at Program.<Main>$(String[] args) in ***\AccessorRepro\Program.cs:line 4
> dotnet publish -f net8.0 -o . -p:PublishAot=true; .\AccessorRepro.exe
8
> dotnet publish -f net9.0 -o . -p:PublishAot=true; .\AccessorRepro.exe
8

Regression?

Yes

Known Workarounds

Using reflection 😢

Configuration

.NET SDK:
 Version:           9.0.100-rc.2.24468.2
 Commit:            c204043de1
 Workload version:  9.0.100-manifests.8fbc914b
 MSBuild version:   17.12.0-preview-24467-02+988196b1c

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22631
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\9.0.100-rc.2.24468.2\

but did also reproduce with RC.1

Other information

No response

dotnet-policy-service[bot] commented 1 month ago

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

KeterSCP commented 1 month ago

As a workaround you can use generic wrapper for the accessor (not sure what changed in .NET 9 to require that):

using System.Runtime.CompilerServices;
using Google.Protobuf.Collections;

var repeatedField = new RepeatedField<float> { 1.0f, 2.0f, 3.0f };

Console.WriteLine(MyAccessor<float>.ArrayRef(repeatedField).Length); // 8

public class MyAccessor<T>
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "array")]
    public static extern ref T[] ArrayRef(RepeatedField<T> instance);
}
neon-sunset commented 1 month ago

Thanks, this works the other way around - on 9 but not on 8 (which does not support unsafe accessor generics), so I'm kind of back to square one 😀

I have simplified the example - does not need Protobuf dependency to repro.

jkotas commented 1 month ago

cc @AaronRobinsonMSFT

dotnet-policy-service[bot] commented 1 month ago

Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices See info in area-owners.md if you want to be subscribed.

AaronRobinsonMSFT commented 1 month ago

Thanks, this works the other way around - on 9 but not on 8 (which does not support unsafe accessor generics), so I'm kind of back to square one 😀

I have simplified the example - does not need Protobuf dependency to repro.

@neon-sunset The solution here is to use an accessor class, as suggested in https://github.com/dotnet/runtime/issues/108046#issuecomment-2362520815. The UnsafeAccessorAttribute wasn't supposed to support generics in .NET 8. Unfortunately it did and enabled users to perform actions that weren't expected or properly spec'd. This was filed as a break-change in .NET 9, eventhough using generics in .NET 8 wasn't supported. See https://github.com/dotnet/docs/issues/41769.

The documentation for UnsafeAccessorAttribute has additional details that are also relevant.

neon-sunset commented 1 month ago

Thanks. This is fair, the only question is the discrepancy with NativeAOT 9.0 where it does continue to work.

AaronRobinsonMSFT commented 1 month ago

Thanks. This is fair, the only question is the discrepancy with NativeAOT 9.0 where it does continue to work.

Yes, sadly native AOT has a very different Type System API and in this case was written "properly" in .NET 8.0. Since native AOT must be computed at compile time it is much easier to reason about. The check at run-time in CoreCLR was a bit harder and took me longer to wrap my head around "how" to make it work. The lack of support in .NET 8.0 was simply me not having the knowledge to make it work in the timeframe.

neon-sunset commented 1 month ago

I see, thank you for the explanation. In this case I ended up unintentionally relying on undocumented behavior where the unsafe accessor for RepeatedField<float> works in .NET 8 but no longer does in .NET 9. Its use will be removed for 8 altogether, and a working solution for 9 will be provided in net9.0-specific package instead, but it's not ideal (well, protobuf abstractions are not exactly great in general, to the point of System.Text.Json being more performance-friendly, but that's another topic).