-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Reference assemblies need to include private struct fields #16402
Comments
This has versioning implications. The best definition for a valid reference assembly I have is that it's indistinguishable to the compiler from what could just have been an earlier version of the implementation assembly. As such, for any transformation deemed invalid from implementation to reference assembly, the inverse should be deemed to be a breaking change. We should update https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md accordingly. (That document could probably use a general review as I've just noted that the second bullet is |
I agree there are versioning implications here. I looked at that document for a bit but I'm having trouble processing it because it's unclear what it is targeting for breaking changes. Is the document concerned only about compilation back compat or is it concerned with binary compat? This has a big distinction for structs contained in a reference assembly. Here are my quick thoughts on it. Binary CompatAt this level structs need to be broken down into two categories: those which have a field which is a reference type and those which do not. Structs without a reference type field cannot change size between versions. The developer has to assume the worst which is the struct is involved in a PInvoke / COM interop scenario. Changing the size of the struct in such a scenario is an observable change which could break runtime compat. There is probably some flexibility if such structs could change shape so long as they don't change size or add a reference type field. At a glance that seems okay but I may be missing something. A struct with a reference type field could never be used in interop though. Hence it can fall back to the compile compat rules. Compile CompatFor structs the hardest language to deal with compat on is C# as it has both definite assignment rules (F# does as well) and
|
For compile-time compat, the following rules should work for producing a reference assembly. We drop all private fields, but add back certain synthesized private fields for a value type (struct) as follows:
|
This breaks the ability for developers to write correct PInvoke and unsafe code. The size of the struct is important in both scenarios to write correct code. |
@jaredpar Can you unpack that assertion? The size of a struct isn't considered to be a compile-time constant except for primitive types and enums. What kind of code would be broken by this? |
I would say that interop is the issue more than unsafe code. You can certainly write correct unsafe code without having size or layout guarantees. e.g. There is nothing wrong with &foo.x even if x moves around. If we put further restrictions on struct changes in the name of interop, then structs marked with auto layout in v1 would be exempt. They are rejected by the interop layer, but that (justifiably) does not prevent them from being used by unsafe managed code. I wish C# made auto the default and that sequential was opt-in for interop so that we didn't have to assume that any struct might be used for interop, but rather only those that were explicitly marked sequential or explicit. |
Agree but there is plenty of interop that is built around exactly such types. Or more accurately structs that transitively wrap those types. Take for example |
Agree. Auto is also advantageous from the stand point of performance because the JIT can pack structs tighter. Stupid back compat 😦 |
Well said, @jaredpar! :)
That would have been advantageous, especially since intereop usually requires a good understanding of what's going on at runtime. Another choice that would have been good was requiring constructor call chaining rather than implicitly adding it. :) I normally always follow that rule, except for the one time in the code that "discovered" this issue, specifically because it takes away the possibility of ambiguity and lead to "strange" results/compiler errors. 20-20 hindsight and all. |
Update CCIExtensions to understand private struct fields Cost assumes its @weshaggard Will be done along with dotnet/corefx#6185. |
How are you going to deal with the fact that the private fields are different between different implementations in number of cases? |
@jkotas is that true for any public structs? |
A few examples: // Similar differences exist for other async-related structs. These structs have been tweaked
// for performance a lot in .NET Core, and I think we would love to have the option to keep tweaking
// them without setting their layout in stone.
// .NET Framework
public struct AsyncVoidMethodBuilder
{
private SynchronizationContext m_synchronizationContext;
private AsyncMethodBuilderCore m_coreState;
private Task m_task;
}
// .NET Core
public struct AsyncVoidMethodBuilder
{
private SynchronizationContext _synchronizationContext;
private AsyncTaskMethodBuilder _builder;
}
// .NET Native
public struct AsyncVoidMethodBuilder
{
private Action m_moveNextAction;
private SynchronizationContext m_synchronizationContext;
private Task m_task;
}
// .NET Framework
public struct AsyncFlowControl: IDisposable
{
private bool useEC;
private ExecutionContext _ec;
private SecurityContext _sc;
private Thread _thread;
}
// .NET Core
public struct AsyncFlowControl : IDisposable
{
private Internal.Runtime.Augments.RuntimeThread _thread;
}
// .NET Framework
struct OpCode
{
private String m_stringname; // not used - computed lazily
private StackBehaviour m_pop;
private StackBehaviour m_push;
private OperandType m_operand;
private OpCodeType m_type;
private int m_size;
private byte m_s1;
private byte m_s2;
private FlowControl m_ctrl;
private bool m_endsUncondJmpBlk;
private int m_stackChange;
}
// .NET Core
public struct OpCode
{
private OpCodeValues m_value;
private int m_flags;
}
// Similar differences exist for other XXXHandles
// NET Framework and .NET Core
struct RuntimeTypeHandle
{
private RuntimeType m_type;
}
// .NET Native:
struct RuntimeTypeHandle
{
private IntPtr _value;
}
// Similar differences exist for other types related to Span
// Fast span
ref struct Span<T>
{
internal readonly ByReference<T> _pointer;
private readonly int _length;
}
// Slow span
ref struct Span<T>
{
private readonly Pinnable<T> _pinnable;
private readonly IntPtr _byteOffset;
private readonly int _length;
} |
For compile-time compat, the following rules should work for producing a reference assembly. We drop all private fields, but add back certain synthesized private fields for a value type (struct) as follows: - If there are any private fields that are or contain any value type members, add a single private field of type int - If there are any private fields that are or contain any reference type members, add a single private field of type object. - If the type is generic, then for every type parameter of the type, if there are any private fields that are or contain any members whose type is that type parameter, we add a direct private field of that type. For more details see issue https://github.com/dotnet/corefx/issues/6185 this blog is helpful as well http://blog.paranoidcoding.com/2016/02/15/are-private-members-api-surface.html
This also includes an update to the genapi tool that includes the necessary private fields in structs to enable the compiler to do correctness checks. See https://github.com/dotnet/corefx/issues/6185.
For compile-time compat, the following rules should work for producing a reference assembly. We drop all private fields, but add back certain synthesized private fields for a value type (struct) as follows: - If there are any private fields that are or contain any value type members, add a single private field of type int - If there are any private fields that are or contain any reference type members, add a single private field of type object. - If the type is generic, then for every type parameter of the type, if there are any private fields that are or contain any members whose type is that type parameter, we add a direct private field of that type. For more details see issue https://github.com/dotnet/corefx/issues/6185 this blog is helpful as well http://blog.paranoidcoding.com/2016/02/15/are-private-members-api-surface.html
All the refs in corefx that have structs have had their refs updated to have private fields that follow the rules defined in our tools |
This fixes dotnet#678. In a nutshell, the compiler needs to know whether a struct has any fields in order to apply definitive assignment rules. While stripping all private fields from types is generally OK, we can't do this for structs. Fortunately, for private fields the compiler doesn't really care what they are, but that their characteristics are. For example: 1. Does the struct have any fields? 2. Does the struct contain any reference types (to validate generic instantiations that have the unmanaged constraint)? 3. Does the struct use the generic parameter in a field declaration (to validate cyclic layout problems)? This adds dummy fields to structs to conform to these rules. These aren't computed separately but are instead taken from .NET Core. For more details, see this issue in CoreFX: https://github.com/dotnet/corefx/issues/6185
This fixes dotnet#678. In a nutshell, the compiler needs to know whether a struct has any fields in order to apply definitive assignment rules. While stripping all private fields from types is generally OK, we can't do this for structs. Fortunately, for private fields the compiler doesn't really care what they are, but what their characteristics are. For example: 1. Does the struct have any fields? 2. Does the struct contain any reference types (to validate generic instantiations that have the unmanaged constraint)? 3. Does the struct use the generic parameter in a field declaration (to validate cyclic layout problems)? This adds dummy fields to structs to conform to these rules. These aren't computed separately but are instead taken from .NET Core. For more details, see this issue in CoreFX: https://github.com/dotnet/corefx/issues/6185
This fixes dotnet#678. In a nutshell, the compiler needs to know whether a struct has any fields in order to apply definitive assignment rules. While stripping all private fields from types is generally OK, we can't do this for structs. Fortunately, for private fields the compiler doesn't really care what they are, but what their characteristics are. For example: 1. Does the struct have any fields? 2. Does the struct contain any reference types (to validate generic instantiations that have the unmanaged constraint)? 3. Does the struct use the generic parameter in a field declaration (to validate cyclic layout problems)? This adds dummy fields to structs to conform to these rules. These aren't computed separately but are instead taken from .NET Core. For more details, see this issue in CoreFX: https://github.com/dotnet/corefx/issues/6185
Today the reference assemblies for .NET assemblies strip private fields from structs. This has an observable, and potentially dangerous, impact on project that consume them:
[FieldOffset]
to verify when they should not.More details are available here and an example of the problems this can produce is here.
I understand part of the motivation for removing these fields is to keep the reference assemblies small. Keeping the fields necessitates keeping the type definitions for the types of the fields and this can cascade into many more types / members being included thus increasing size.
Cutting private fields for classes is fine as it's unobservable. Unfortunately for structs fields are observable irrespective of their accessibility and must be maintained in reference assemblies. The only action I think that can be done to curtail the number of types a struct brings in is the following:
Essentially
class
,interface
,delegate
and generic type parameters constrained toclass
can be transformed to object.This is unobservable to the compiler and can help limit the number of types brought it.
The text was updated successfully, but these errors were encountered: