Blittable design update

This updates the blittable design to reflect the most recent design
meeting on the subject. Summary of the changes:

1. Decided on `unmanaged` over `blittable`: This is the term used by
the spec and prior art (in F#). Additionally `blittable` is actually
a stronger constraint than what we actually need here.
2. Removed the struct declaration section: After further examination
it was decided that forcing existing structs to be annotated with
`unmanaged` before meeting the `unmanaged` constraint would severely
limit the feature. Decided to focus on the constraint here only.

Note: even though we changed the name I still kept the doc as
blittable.md for now to avoid breaking links and keeping doc history.
This commit is contained in:
Jared Parsons 2018-01-24 16:33:49 -08:00
parent c52436f0e5
commit 42bf13e172

View file

@ -1,4 +1,4 @@
# Blittable Types
# Unmanaged type constraint
* [x] Proposed
* [ ] Prototype
@ -8,129 +8,92 @@
## Summary
[summary]: #summary
The blittable feature will give language enforcement to the class of types known as "unmanaged types" in the C# language spec. This is defined in section 18.2 as a type which is not a reference type and doesn't contain reference type fields at any level of nesting.
The unmanaged constraint feature will give language enforcement to the class of types known as "unmanaged types" in the C# language spec. This is defined in section 18.2 as a type which is not a reference type and doesn't contain reference type fields at any level of nesting.
## Motivation
[motivation]: #motivation
The primary motivation is to make it easier to author low level interop code in C#. Blittable types are one of the core building blocks for interop code, yet abstracting around them is difficult today due to a lack of declarative language support.
This is the case even though blittable types are well defined in the language spec and many features, like pointers, can operate only on them. The language chooses to let structs implicitly opt into being blittable by virtue of their construction with no avenue for opting out of such a classification.
While attractive in small applications this is more difficult to manage across a large set of libraries authored by different developers. It means small field additions to structs can cause compile and runtime breaks in downstream consumers with no warning to the developers that made the change. This spooky action at a distance is one of the core problems with having an implicit opt in model here.
A declarative, explicit opt in model makes it easier to code in this area. Developers can be very explicit about the types they intend for interop. This gives them compiler help when mistakes are made in their application and in any libraries they consume.
The lack of compile time support also makes it difficult to abstract over blittable types. It's not possible for instance to author common helper routines using generic code:
The primary motivation is to make it easier to author low level interop code in C#. Unmanaged types are one of the core building blocks for interop code, yet the lack of support in generics makes it impossible to create re-usable routines across all unmanaged types. Instead developers are forced to author the same boiler plate code for every unmanaged type in their library:
``` c#
void Hash<T>(T value) where T : blittable struct
int Hash(Point point) { ... }
int Hash(TimeSpan timeSpan) { ... }
```
To enable this type of scenario the language will be introducing a new constraint: unmanaged:
``` c#
void Hash<T>(T value) where T : unmanaged
{
...
}
```
This constraint can only be met by types which fit into the unmanaged type definition in the C# language spec. Another way of looking at it is that a type satisfies the unmanaged constraint iff it can also be used as a pointer.
``` c#
Hash(new Point()); // Okay
Hash(42); // Okay
Hash("hello") // Error: Type string does not satisfy the unmanaged constraint
```
Type parameters with the unmanaged constraint can use all the features available to unmanaged types: pointers, fixed, etc ...
``` c#
void Hash<T>(T value) where T : unmanaged
{
// Okay
using (T* p = &value) {
...
}
}
```
Instead developers are forced to rewrite virtually the same code for all of their blittable types:
This constraint will also make it possible to have efficient conversions between structured data and streams of bytes. This is an operation that is common in networking stacks and serialization layers:
``` c#
void Hash(Point p) {
...
}
void Hash(Time t) {
Span<byte> Convert<T>(ref T value) where T : unmanaged {
...
}
```
The lack of constraints here also make it impossible to have efficient conversions between streams of data and more structured types. This is an operation that is common in networking stacks and serialization layers:
``` c#
Span<byte> Convert<T>(ref T value) where T : blittable {
...
}
```
Such routines are advantageous because they are provably safe at compile time and allocation free. Interop authors today can not due this (even though it's at a layer where perf is critical). Instead they need to rely on allocating routines that have expensive runtime checks to verify values are correctly blittable.
Such routines are advantageous because they are provably safe at compile time and allocation free. Interop authors today can not do this (even though it's at a layer where perf is critical). Instead they need to rely on allocating routines that have expensive runtime checks to verify values are correctly unmanaged.
## Detailed design
[design]: #detailed-design
The language will introduce a new declaration modifier named `blittable` that can be applied to `struct` definitions.
The language will introduce a new constraint named `unmanaged`. In order to satisfy this constraint a type must be a struct and all the fields of the type must fall into one of the following categories:
``` c#
blittable struct Point
{
public int X;
public int Y;
- Have the type `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `char`, `float`, `double`, `decimal`, or `bool`.
- Be any `enum` type.
- Be a pointer type.
- Be a user defined struct that satsifies the `unmanaged` constraint.
Compiler generated instance fields, such as those backing auto-implemented properties, must also meet these constraints.
For example:
``` c#
// Unmanaged type
struct Point {
int X;
int Y {get; set;}
}
```
The compiler will enforce that the fields of such a struct definition fit one of the following categories:
- `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `char`, `float`, `double`, `decimal`, or `bool`.
- Any `enum` type
- Any pointer type which points to a `blittable` type
- Any user defined struct explicitly declared as `blittable`
Any type fitting the definition above, or any element in the list, is considered a valid `blittable` type.
``` c#
struct User
{
// Not an unmanaged type
struct Student {
string FirstName;
string LastName;
}
```
blittable struct ItemData
{
// Error: blittable struct cannot contain field of type User which is not blittable
User User;
int Id;
}
```
The token `unmanaged` in the constraint is not a keyword, nor a contextual keyword. Instead it is like `var` in that it is evaluated at that location and will either:
Note that a user defined struct must be explicitly declared as `blittable` in order to meet the requirements above. This is required even if the struct otherwise meets the requirements of `blittable`.
- Bind to user defined or referenced type named `unmanaged`: This will be treated just as any other named type constraint is treated.
- Bind to no type: This will be interpreted as the `unmanaged` constraint.
``` c#
struct SimplePoint
{
public int X;
public int Y;
}
blittable struct Data
{
// Error: blittable struct cannot contain field of type SimplePoint which is not blittable
SimplePoint Point;
}
```
The language will also support the ability to constrain generic type parameters to be `blittable` types.
``` C#
void M<T>(T p) where T : blittable struct
{
...
}
M<Point>(); // Ok
M<User>(); // Error: Type User does not satisfy the blittable constraint.
```
One of the primary motivations for `blittable` structs is ease of interop. As such it's imperative that such a type have it's field ordered in a sequential, or explicit, layout. An auto layout of fields makes it impossible to reliably interop the data.
The default today is for a sequential layout so this doesn't represent a substantial change. However the compiler will make it illegal to mark such types as having an auto layout.
``` c#
// Error: A blittable struct may not be marked with an auto layout
[StructLayout(LayoutKind.Auto)]
blittable struct LayoutExample
{
...
}
```
In the case there is a type named `unmanaged` and it is available without qualification in the current context, then there will be no way to use the `unmanaged` constraint. This parallels the rules surrounding the feature `var` and user defined types of the same name.
## Drawbacks
[drawbacks]: #drawbacks
@ -139,22 +102,26 @@ The primary drawback of this feature is that it serves a small number of develop
Yet these frameworks are are often the basis for the majority of .NET applications out there. Hence performance / correctness wins at this level can have a riple effect on the .NET ecosystem. This makes the feature worth considering even with the limited audience.
There will also likely be a small transition period after this is released where core libraries move to adopt it. Types like [System.Runtime.InteropServices.ComTypes.FILETIME](https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.comtypes.filetime(v=vs.110).aspx) are common in interop code. Until it is marked as `blittable` in source though, developers won't be able to depend on it in their libraries.
## Alternatives
[alternatives]: #alternatives
There are a couple of alternatives to consider:
- The status quo: The feature is not justified on its own merits and developers continue to use the implicit opt in behavior.
- Generic constraint only: The blittable keyword is used on generic constraints only. This allows for developers to author efficient helper libraries. But the types involved lack any declarative support and hence are fragile across distributed development.
## Resolved questions
[resolved]: #resolved-questions
## Unresolved questions
[unresolved]: #unresolved-questions
### Blittable vs. Unmanaged
The F# language has a very [similar feature](https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/generics/constraints) which uses the keyword unmanaged. The blittable name comes from the use in Midori. May want to look to precedence here and use unmanaged instead.
- blittable vs. unmanaged. The F# language has a very [similar feature](https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/generics/constraints) which uses the keyword unmanaged. The blittable name comes from the use in Midori. May want to look to precedence here and use unmanaged instead.
- Verifier: does the verifier / runtime need to be updated to understad the use of pointers to generic type parameters? Or can it simply work as is without changes.
**Resolution** The language decide to use unmanaged
### Verifier
Does the verifier / runtime need to be updated to understad the use of pointers to generic type parameters? Or can it simply work as is without changes.
**Resolution** The new IL verification spec will need to be updated to account for this.
## Design meetings