133 lines
4.2 KiB
Markdown
133 lines
4.2 KiB
Markdown
|
# Pattern-based `fixed` statement
|
|||
|
|
|||
|
* [x] Proposed
|
|||
|
* [ ] Prototype: Not Started
|
|||
|
* [ ] Implementation: Not Started
|
|||
|
* [ ] Specification: Not Started
|
|||
|
|
|||
|
## Summary
|
|||
|
[summary]: #summary
|
|||
|
|
|||
|
Introduce a pattern that would allow types to participate in `fixed` statements.
|
|||
|
|
|||
|
## Motivation
|
|||
|
[motivation]: #motivation
|
|||
|
|
|||
|
The language provides a mechanism for pinning managed data and obtain a native pointer to the underlying buffer.
|
|||
|
|
|||
|
```C#
|
|||
|
fixed(byte* ptr = byteArray)
|
|||
|
{
|
|||
|
// ptr is a native pointer to the first element of the array
|
|||
|
// byteArray is protected from being moved/collected by the GC for the duration of this block
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
The set of types that can participate in `foreach` is hardcoded and limited to arrays and `System.String`. Hardcoding "special" types does not scale when new primitives such as `ImmutableArray<T>`, `Span<T>`, `Utf8String` are introduced.
|
|||
|
|
|||
|
In addition, the current solution for `System.String` relies on a fairly rigid API. The shape of the API implies that `System.String` is a contiguous object that embeds UTF16 encoded data at a fixed offset from the object header. Such approach has been found problematic in several proposals that could require changes to the underlying layout.
|
|||
|
It would be desirable to be able to switch to something more flexible that decouples `System.String` object from its internal representation for the purpose of unmanaged interop.
|
|||
|
|
|||
|
## Detailed design
|
|||
|
[design]: #detailed-design
|
|||
|
|
|||
|
## *Pattern* ##
|
|||
|
A viable pattern-based <20>fixed<65> need to:
|
|||
|
- Provide the managed references to pin the instance and to initialize the pointer (preferably this is the same reference)
|
|||
|
- Convey unambiguously the type of the unmanaged element (i.e. <20>char<61> for <20>string<6E>)
|
|||
|
- Prescribe the behavior in "empty" case when there is nothing to refer to.
|
|||
|
- Should not push API authors toward design decisions that hurt the use of the type outside of `fixed`.
|
|||
|
|
|||
|
|
|||
|
I think the above could be satisfied by recognizing a specially named ref-returning member:
|
|||
|
`ref [readonly] T DangerousGetPinnableReference()`.
|
|||
|
|
|||
|
In order to be used by the `foreach` statement the following conditions must be met:
|
|||
|
|
|||
|
1) There is only one such member provided for a type.
|
|||
|
1) Returns by `ref` or `ref readonly`.
|
|||
|
(readonly is permitted so that authors of immutable/readonly types could implement the pattern)
|
|||
|
1) T is an unmanaged type.
|
|||
|
(since `T*` becomes the pointer type)
|
|||
|
1) Returns managed `nullptr` when there is no data to pin <20> probably the cheapest way to convey emptiness.
|
|||
|
(note that <20><> string returns a ref to <20>\0<> since strings are zero-terminated)
|
|||
|
|
|||
|
|
|||
|
Alternatively for the `#3` we can allow the result in empty cases be undefined or implementation-specific.
|
|||
|
That, however, may make the API more dangerous and prone to abuse and unintended compatibility burdens.
|
|||
|
|
|||
|
## *Translation* ##
|
|||
|
|
|||
|
|
|||
|
```C#
|
|||
|
fixed(byte* ptr = thing)
|
|||
|
{
|
|||
|
// <BODY>
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
becomes the following pseudocode (not all expressible in C#)
|
|||
|
|
|||
|
```C#
|
|||
|
|
|||
|
byte* ptr;
|
|||
|
// specially decorated "pinned" IL local slot, not visible to user code.
|
|||
|
pinned ref byte _pinned;
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
// NOTE: null check is omitted for value types
|
|||
|
// NOTE: `thing` is evaluated only once (temporary is introduced if necessary)
|
|||
|
if (thing != null)
|
|||
|
{
|
|||
|
// obtain and "pin" the reference
|
|||
|
_pinned = ref thing.DangerousGetPinnableReference();
|
|||
|
|
|||
|
// unsafe cast in IL
|
|||
|
ptr = (byte*)_pinned;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
ptr = default(byte*);
|
|||
|
}
|
|||
|
|
|||
|
// <BODY>
|
|||
|
}
|
|||
|
finally // finally can be omitted when not observable
|
|||
|
{
|
|||
|
// "unpin" the object
|
|||
|
_pinned = nullptr;
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
## Drawbacks
|
|||
|
[drawbacks]: #drawbacks
|
|||
|
|
|||
|
- DangerousGetPinnableReference is intended to be used only in `fixed`, but nothing prevents its use in safe code, so implementor must keep that in mind.
|
|||
|
|
|||
|
## Alternatives
|
|||
|
[alternatives]: #alternatives
|
|||
|
|
|||
|
Users can introduce DangerousGetPinnableReference or similar member and use it as
|
|||
|
|
|||
|
```C#
|
|||
|
fixed(byte* ptr = thing.DangerousGetPinnableReference())
|
|||
|
{
|
|||
|
// <BODY>
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
There is no solution for `System.String` if alternative solution is desired.
|
|||
|
|
|||
|
|
|||
|
## Unresolved questions
|
|||
|
[unresolved]: #unresolved-questions
|
|||
|
|
|||
|
- [ ] Behavior in "empty" state. - `nullptr` or `undefined` ?
|
|||
|
|
|||
|
## Design meetings
|
|||
|
|
|||
|
None yet.
|