Why must fixed size buffers (arrays) be declared unsafe?
Asked Answered
P

3

32

Let's say I want to have a value type of 7 bytes (or 3 or 777).

I can define it like that:

public struct Buffer71
{
    public byte b0;
    public byte b1;
    public byte b2;
    public byte b3;
    public byte b4;
    public byte b5;
    public byte b6;
}

A simpler way to define it is using a fixed buffer

public struct Buffer72
{
    public unsafe fixed byte bs[7];
}

Of course the second definition is simpler. The problem lies with the unsafe keyword that must be provided for fixed buffers. I understand that this is implemented using pointers and hence unsafe.

My question is why does it have to be unsafe? Why can't C# provide arbitrary constant length arrays and keep them as a value type instead of making it a C# reference type array or unsafe buffers?

Pish answered 21/5, 2010 at 10:57 Comment(0)
R
14

Because a "fixed buffer" is not a real array. It is a custom value type, about the only way to generate one in the C# language that I know. There is no way for the CLR to verify that indexing of the array is done in a safe way. The code is not verifiable either. The most graphic demonstration of this:

using System;

class Program {
    static unsafe void Main(string[] args) {
        var buf = new Buffer72();
        Console.WriteLine(buf.bs[8]);
        Console.ReadLine();
    }
}
public struct Buffer72 {
    public unsafe fixed byte bs[7];
}

You can arbitrarily access the stack frame in this example. The standard buffer overflow injection technique would be available to malicious code to patch the function return address and force your code to jump to an arbitrary location.

Yes, that's quite unsafe.

Roxannaroxanne answered 21/5, 2010 at 11:39 Comment(7)
So is the problem simply that CIL lacks any means of performing bounded indexed operations? I don't see any semantic reason why CIL couldn't provide such a feature. Some things like graphics transforms may be a little bit over the "ideal" 16-byte size of a struct, but they should logically have mutable value semantics. Immutable semantics make it painful to tweak a value within an instance, and mutable reference semantics introduce ambiguities as to when e.g. a function which returns an instance is going to return a new instance or an existing one.Tit
Its not so simple it raises a whole lot of issues to do with concurrent safety guarantees .Martelli
It's insane that there's no safe way to embed a fixed sized array in a struct. For high performance sections of the code I want to use pretty much only 100% blittable structs. At least we have ref returns and ref locals now.Fujio
@Fujio I agree wholeheartedly. It seems asinine that they chose to implement this in an unsafe manner. There is fundamentally no difference between storing and accessing element FixedInts[8] and field FixedInt8, and because the size is fixed, the compiler and CLR certainly DO have access to all the information they need to make it safe, should the .NET team decide to properly utilize it. I am also shocked we cannot create fixed buffers of structs. Sometimes not spreading everything over the heap is more important than theoretical design ideals. I'd rather not have C++ forced on me.Multivocal
Hmya, I often get blamed for design oversights by Microsoft employees. Don't shoot the messenger, aim your gun elsewhere and don't tell me about it. It is not a real array, there is no decent way to discover the size of the fixed sized buffer at runtime. No Length property, at best index checking would have to be implemented with reflection. Which is far too slow.Roxannaroxanne
@Multivocal Not sure if you knew, but as of C#10, you can use readonly and an explicitly defined parameterless constructor to create an immutable reference to an array of set size. public readonly int[] fixedSizeArray = new int [5]; will get you what you want (with a few caveats I cover in my answer below).Stallion
A great replacement for buffers? No. But it's the closest you can get (as far as I know) in managed C# for now, unless you combine it with MemoryStallion
F
2

Beginning with C# 12, you can declare inline arrays as a struct type:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
    private char _firstElement;
}

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#inline-arrays

Fowkes answered 30/11, 2023 at 6:56 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Conclave
S
1

Just as a note for those finding this now.

As of C#10, you can directly define and assign fields on a struct. Combined with the readonly modifier, this allows you to (almost) guarantee an immutable reference to a mutable reference type (or mutable struct type), including arrays.

public struct Test
{
    public readonly int[] fixedSizeArray = new int [5];
    /// <summary>I'm important, don't forget me.</summary>
    public Test() {}
}

The only caveats being:

  1. Just like with all readonly fields, I believe this can be modified as much as desired in the constructor (which shouldn't be too big a problem as long as you're careful to never reassign this in your constructors as I believe structs can't be derived from).
    • In order to use this properly, I believe you have to explicitly define the parameterless constructor, as failure to do so means for all instances using the implicit public parameterless constructor, all fields will be set to their default uninitialized values (e.g. int i; would be 0, any reference type - like an array - would be null), ignoring the explicit initializers (for some reason, I'm not quite sure). That being said, declaring the public parameterless constructor (also introduced in C#10) and leaving it empty should result in the desired behavior (at least if I'm reading the docs and feature proposal right). So for our example from earlier to work, it must have that (seemingly) useless parameterless constructor. This won't work:
// THIS WON'T WORK
public struct Test
{
    public readonly int[] fixedSizeArray = new int[5];
}
  1. Using the default operator will always skip any and all constructors, so people can theoretically create an instance of the struct without the correct size. That being said, the field is still readonly, so I'm pretty sure no one's gonna be able to pull a fast one and use it incorrectly, as they'll be stuck with a null array. That being said, it's still worth noting as an edge case.

For buffers, you can combine this with Memory and get pretty close. If you don't want unsafe code but want a C++esque buffer, it's likely your best option.

Stallion answered 1/3, 2023 at 16:40 Comment(5)
I am responding to your comment directing me here. Yes, I am aware this is possible, but I am having trouble understanding why I would use it. I can only speak for myself, but whenever I find myself looking up questions like this one, it is generally because I have specific needs related to garbage collection, memory layout, copy semantics, and/or performance more generally. If heap allocation and reference semantics met my needs, I think I would save myself some headaches and use a class. What do you see as the benefit to this approach?Multivocal
@Multivocal In use cases like those, I'd agree that solutions like this are not always very effective. I misunderstood your restrictions. I wasn't sure if your case was restrictive enough to remove these as viable options. For my part, I found this question after looking for ways to force length restrictions on arrays (in structs specifically, and not just for "array-like" fixed buffers), and after stumbling into the changes to structs that made readonly viable for that, I thought I'd come back and share this for those cases. I find this useful primarily for design, not performance.Stallion
Can you help me understand why you would use a struct for this instead of a class? I am not doubting your usage, I just want to understand it so I might recognize opportunities more easily in my own code, going forward.Multivocal
@Multivocal I use it if value semantics > reference ones & init. logic is unneeded. E.g. a tunable correction algorithm that needs (class/struct) State: the x priorErrors (float[x]) & 5+ more fields. To tune & test, we want state history (State[200]). W/ struct: 1. no need to init. history elements 2. Metrics can +/-/average/etc State's w/o dirtying data or cloning, etc. When a type fits a struct (small, data-centric, little behavior) but needs an array of specific size (stops OutOfRange), this can do that w/o buffer's tradeoffs (loss of type) or using op's example + an indexer.Stallion
cont. Structs have alot of distinctions from classes that could be useful (blittability, value semantics, auto initialization, more options for compiler optimizations, more able to control & signal behavior w/ ref readonly/readonly struct), & this solution can provide all but blittability without resorting to (overly) exotic/extreme solutions like defining an array through individual fields (& maybe adding an indexer for readability/convienence outside the struct) or resorting to an unsafe, non-Array typed option. To get them all w/o tradeoffs in specific cases is (currently) impossible.Stallion

© 2022 - 2024 — McMap. All rights reserved.