-
Notifications
You must be signed in to change notification settings - Fork 1k
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
allow references to structs to be stored in fields of ref structs #1147
Comments
@Joe4evr I don't think that comment is necessarily apropos. |
ref fields are not directly expressible in IL. You can very close to the desired behavior if there is a public type like: https://github.com/dotnet/corert/blob/796aeaa64ec09da3e05683111c864b529bcc17e8/src/System.Private.CoreLib/src/System/ByReference.cs The bigger problem (and one of the reasons the type is not public) is tracking of life times. Such wrapping could be ok though, if wrapped ref is guaranteed to refer to a heap location. There are ideas how this could be allowed via some specialized syntax like: ByReference<int> r = ref inArray[0]; // ok since array element is definitely on the heap Just wondering, if the feature comes with "only on the heap" restriction, would it be acceptable limitation? |
That is possible but we intend to use this for simplifying our API's significantly for our upcoming new component system. We want the syntax to be straightforward, and typing .Value is quite annoying for what is a very common pattern in that Component system. For our use case:
For our particular use case, we can live with any restriction on what you can assign to it. Internally we are patching a pointer to the data in by writing to memory at an offset relative to the adress of the Group struct. But certainly for the feature to be useful in general beyond our use case it seems useful to be able to assign to the ref fields from the constructor |
Stack-only structs were introduced in C# 7.2. See #666 |
This (a ref stored in a struct) would need to be supported by the CLR before/when it could be supported by C#. |
Can you expand the sample to include a couple of other use cases? In particular:
|
Our full example usage code looks like this:
The expected behaviour would be to transform this:
to this:
For our specific use case we do not require initializing ref values on the struct. We are writing to the pointers by offset using unsafe code inside the enumerator that GetEntities returns. That is our use case. I can however imagine that being able to assign the ref in the constructor would cover other use cases for this.
Our use case is where the ref fields to Boid and MyTransform above are blittable. Thats our current requirements. We probably don't want to support interfaces / class types inside of Boid and MyTransform in the example above for other reasons. Not 100% on this, but if thats a hard requirement for some reason, we will find a way to live with it. |
I would imagine that for other use cases allowing assignment to ref fields from the constructor would be the sensible thing to do. This matches how it works in C++.
@benaadams since you emoticon loved it, I imagine you have some use cases for this as well? |
Yes
Gather ops (returning multiple refs); side car data (ref + info fields) |
This feature request started with an ask that the feature be usable in safe code. This design seems to require there is an unsafe creator behind the scenes newing up these values. |
This was mainly what I was hoping for when I heard about 7.2 ref struct. I thought ref fields was the main reason to introduce ref structs in C# 7.2. |
The same for me. I planned to use them as wrappers for |
You can do that as the ref field is hidden in the Span e.g. Invalid struct Thing
{
Span<byte> span;
} Valid ref struct Thing
{
Span<byte> span;
} Is bit wasteful for a Span of count 1; also introduces variability to the field (as could now be count > 1) |
Great! 👍 |
The requirement to initialize such fields could make it safe: ref struct MessageWrapper
{
private ref Header header;
private Span<byte> body; a
public MessageWrapper(ref Header header, Span<byte> body)
{
this.header = ref header;
this.body = body;
}
} So ref var wrapper = new MessageWrapper { Header = ref header, Body = body }; |
Not really: MessageWrapper Create()
{
var header = CreateHeader();
return new MessageWrapper(ref header, CreateBody());
}
// call this method and watch bad things happen
void Crash() {
var w = Create();
// do something with w.header here
} To make that safe the language/runtime would need to recognize that ref struct MessageWrapper
{
private Span<Header> header;
private Span<byte> body;
public MessageWrapper(ref Header header, Span<byte> body)
{
this.header = SpanEx.DangerousCreate(ref header);
this.body = body;
}
}
public static class SpanEx
{
/// <summary>
/// Creates a new span over the target. It is up to the caller to ensure
/// the target is pinned and that the lifetime of the span does not escape
/// the lifetime of the target.
/// </summary>
/// <param name="target">A reference to a blittable value type.</param>
public static unsafe Span<T> DangerousCreate<T>(ref T target) where T : struct
{
return new Span<T>(Unsafe.AsPointer(ref target), 1);
}
} You can move the unsafe code to a second assembly and mark the one containing Wrapping the " I entered https://github.com/dotnet/corefx/issues/26228 to make this a little nicer (don't expect much though, this sort of thing plays very loose with the safety boundaries of managed code). I sort of think requiring the unsafe flag might be reasonable for playing with |
You mean like this: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.2/span-safety.md ? |
I am not sure if that is enough because public class Program {
static MessageWrapper Create()
{
var header = CreateHeader();
header.one = 1;
var w = new MessageWrapper(ref header, CreateBody());
if (w.header[0].one != 1) throw new InvalidOperationException("Header didn't make it");
return w;
}
static Span<byte> CreateBody() => default;
static Header CreateHeader() => default;
public static void Main()
{
var w = Create();
// anything that causes a GC event or sufficiently
// modifies the stack should cause breakage here?
Thread.Sleep(1);
// do something with w.header here
if (w.header[0].one != 1) throw new InvalidOperationException("Header not valid here");
}
}
public ref struct MessageWrapper
{
public Span<Header> header;
public Span<byte> body;
public MessageWrapper(ref Header header, Span<byte> body)
{
this.header = SpanEx.DangerousCreate(ref header);
this.body = body;
}
}
public static class SpanEx
{
/// <summary>
/// Creates a new span over the target. It is up to the caller to ensure
/// the target is pinned and that the lifetime of the span does not escape
/// the lifetime of the target.
/// </summary>
/// <param name="target">A reference to a blittable value type.</param>
public static unsafe Span<T> DangerousCreate<T>(ref T target) where T : struct
{
return new Span<T>(Unsafe.AsPointer(ref target), 1);
}
}
public struct Header {
public byte one;
} |
How about creating a "RefWrapper" or a "SingleSpan" ref struct which is almost same as Span but doesn't have a length field? |
The reason the I think most of us looking for |
@VSadov, what do you say on this?
otherwise:
|
@joeante wrote:
If you're willing to switch over from malloc to allocating your structs in large C# arrays, then you could use the solution that I describe in #2417 in order to eliminate frame rate stalls/stutters caused by GC, without resorting to unsafe code. An additional advantage: When you use malloc, then obviously you must later free the memory manually, but when you allocate your structs in C# arrays (call them "pages"), then each array ("page") is automatically garbage-collected when no more references to that "page" exist, as you know. Thus you can easily manage and balance the costs by merely changing the length of the arrays/pages that you create.
In the case of a game where frame rates are important, you'd create big arrays/pages to keep GC to the minimum, or potentially never release the arrays/pages while the user is still playing the game and not between levels etc. You would have the control AND the necessary performance, without resorting to unsafe code. When allocating a struct, should you search for a free slot in an array/page? If you want to, you can, but note that a simpler solution is satisfactory in many apps: Don't reuse any slots. When the page is full, simply allocate a new page, and the previous page will be automatically garbage-collected when all of its slots become unused. This GC cost is low and completely satisfactory for most apps, but if your game is very sensitive to frame rate stalls, then you could simply increase the page size (array length), and if this GC cost is still not low enough, then you could keep track of which slots are free and reuse slots. You could start using the solution already today with the current version of C#, but in order to make it a comfortable and convenient solution, C# needs the syntactic sugar that I describe in #2417, which is mostly the same syntax as @lucasmeijer described ("ref Color color;") except designed for pointing to elements of C# arrays instead of unsafe C++ malloc. Also note that lucasmeijer wrote "ref struct Group {...}" but in my proposal, both "Group" and "Color" would be normal structs, whereas the field "ref Color color;" is special. |
As I understand it, you would be required to supply the Would it be possible to have a struct containing fields itself that are used by unsafe struct Example
{
Vector3 position;
Quaternion rotation;
public ref Vector3 Position => ref *(Vector3*)Unsafe.AsPointer(ref position);
public ref Quaternion Rotation => ref *(Quaternion*)Unsafe.AsPointer(ref rotation);
} Which would allow for more convenient syntax, such as:
When using properties. |
From the [documentation] on
Essentially this method is incredibly unsafe. The returned |
It is unsafe only for lifetime. But if lifetime of ref is less than lifetime of variable then it is safe. c# is not Rust it does not have lifetime safety. You try to add it. For lifetime safety use array of length 1 and Span to it. |
Span is more safe than pointer * and it does not requare to pin memory. |
Indeed. 100% of unsafe code is safe when used safely. The point of unsafe code though is that the training wheels are off, the language is no longer providing the typical guarantees it provides (type and memory safety).
Sorry this is incorrect. For |
@AlexRadch Span itself uses a quirk to store a ref instead of using proper language/runtime feature. Now you propose using even more quirks instead of having a proper runtime/language feature. Ref field Color != field Color*, there is huge difference between references and pointers. |
Already. #3936 |
Indeed |
Yes. |
@jaredpar I know about it, yes. I call it a quirk because it's internal, hard to use and has no language guarantees. I know of cases where people had to copy that stuff from runtime but there is a meaning in keeping stuff like that hidden from the general community. And thank you for sharing this proposal, it's a good read :)
You mean with safety, not without |
As I know |
I hate that I cannot implement functionality similar to Span in C# - it feels like I am being cheated. @jaredpar |
As you said, you can actually do that already, it's just not ideal. You can make it work and with very good codegen as well though. For instance, you can piggyback one 4 bytes value in your custom span-like type into the Of course, being able to do this with |
What more are you expecting to see? This is a championed issue, triaged into the Working Set milestone, with a proposal checked in, that the LDM has met twice on within the last month. If that's not on the roadmap, I don't know what is. |
I... Huh. Apparently we missed this when triaging things. Well, it's in the working set now. |
I'm confused about the state of this issue - the linked PR appears to add support for ref fields, it has been merged to master, but when checking the master branch via sharplab, it doesn't work. What am I missing? Is this already scheduled for a specific C# version release? |
@ilexp Is the linked PR you're referring to the one that merged spec updates into the dotnet/csharplang master branch? The master branch that Sharplab refers to is the dotnet/roslyn master branch. Here's how you can keep tabs on implementation status in dotnet/roslyn: https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md |
Oh - yes, that is the one I was referring to. I didn't actually check the modified files 🤦 Thanks for pointing it out 👍 |
Unity is a game engine that uses C# extensively.
We're in need of a way to have a struct that contains one or more pointers to other structs that we want to be able to read/write from/to. We do this with unsafe code today:
But we would love to do this without unsafe code, and would like to suggest a feature where it's allowed to store references to structs in fields of ref structs, that can guarantee that these pointers don't escape onto the heap:
EDIT link to spec for this feature https://github.com/dotnet/csharplang/blob/main/proposals/low-level-struct-improvements.md
The text was updated successfully, but these errors were encountered: