1

Wrote the following struct to minimize the number of types for vectors with different components:

[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 3 * sizeof(T))]
public struct Vector3<T> where T : unmanaged
{
    public T X, Y, Z;

    public Vector3(T x, T y, T z)
    {
        X = x;
        Y = y;
        Z = z;
    }
}

This is basically, to avoid writing numerous types such Vec3u8u8u8, Vec4i32i32i32 and so on.

Also because of the need to enforce the type, e.g. sometimes byte instead of int.

However, the above code won't compile because of sizeof(T), producing CS0233:

'identifier' does not have a predefined size, therefore sizeof can only be used in an unsafe context

In case you ask, these types won't be marshalled back and forth with unmanaged side.

But I will occasionally need to use Unsafe.SizeOf<T> or Marshal.SizeOf<T> on them.

Question:

Can you suggest a working alternative?

dbc
  • 104,963
  • 20
  • 228
  • 340
aybe
  • 15,516
  • 9
  • 57
  • 105
  • Isn't [`Size`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.size?view=net-7.0) optional? From the docs: *This field must be equal or greater than the total size, in bytes, of the members of the class or structure. This field is primarily for compiler writers who want to extend the memory occupied by a structure for direct, unmanaged access.* If I just don't set `Pack` then `Unsafe.SizeOf<>` works just fine, see https://dotnetfiddle.net/InJjaL. – dbc Feb 11 '23 at 17:07
  • Now `Marshal.SizeOf>()` fails, but that seems to be because it simply doesn't support generics, see https://dotnetfiddle.net/mUwToY. – dbc Feb 11 '23 at 17:13
  • You can see the restriction that `Marshal.SizeOf()` does not support generics in the reference source here: [Marshal.cs](https://github.com/dotnet/runtime/blob/4af855b0101bad16a4e25a7ec3c4c6a2f2984fb0/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.cs#L125); it throws whenever `t.IsGenericType`. You won't be able to use your `Vector3` with `Marshal.SizeOf()` whether or not you set `Pack`. – dbc Feb 11 '23 at 17:21
  • Okay, fair enough, I abandon this route, thank you! – aybe Feb 11 '23 at 17:29
  • Should I make that an answer? – dbc Feb 11 '23 at 17:31
  • Yes, if you want. – aybe Feb 11 '23 at 19:57

1 Answers1

1

StructLayoutAttribute.Size is optional. If you don't set it, .NET will compute the size automatically. From the docs:

This field must be equal or greater than the total size, in bytes, of the members of the class or structure. This field is primarily for compiler writers who want to extend the memory occupied by a structure for direct, unmanaged access.

If I simply remove it from Vector3<T> like so:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Vector3<T> where T : unmanaged
{
    public T X, Y, Z;
    public Vector3(T x, T y, T z) => (X, Y, Z) = (x, y, z);
}

Then Unsafe.SizeOf<Vector3<T>() will work for any valid T such as int or double:

Console.WriteLine(Unsafe.SizeOf<Vector3<int>>()); // 12
Console.WriteLine(Unsafe.SizeOf<Vector3<double>>()); // 24

Demo #1 here.

However, Marshal.SizeOf<T>() simply does not support generics. (See demo #2 here). The docs are a little ambiguous about this. For Marshal.Sizeof(Type t) the docs remark:

Exceptions
ArgumentException
The t parameter is a generic type definition.

While there is no similar remark for Marshal.SizeOf<T>(), a check of the source code shows that the method throws whenever typeof(T) is generic.

So, what are your options?

Firstly, you could replace all uses of Marshal.SizeOf<T>() with Unsafe.SizeOf<T>() in your codebase, and continue using your Vector3<T> struct.

Secondly, if you must continue to use Marshal.SizeOf<T>(), you may need to create non-generic types like Vector3Double and Vector3Int for every required component type. However, you could reduce duplicated code by extracting an interface that defines a minimal set of operations for vectors, then extract any additional required code into helper methods. .NET 7's generic math makes this fairly straightforward.

First define an vector interface with some minimal required operations like so:

public interface IVector3<TVector, TValue> : IAdditionOperators<TVector, TVector, TVector>, IAdditiveIdentity<TVector, TVector>, IMultiplyOperators<TVector, TValue, TVector>
    where TValue : unmanaged, INumber<TValue>
    where TVector : IVector3<TVector, TValue>, new ()
{
    TValue X { get; init; }
    TValue Y { get; init; }
    TValue Z { get; init; }
}  

Next, implement for your required component value types e.g.:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Vector3Double :  IVector3<Vector3Double, double>
{
    public double X { get; init; }
    public double Y { get; init; }
    public double Z { get; init; }

    public Vector3Double(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    public static Vector3Double operator +(Vector3Double left, Vector3Double right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
    public static Vector3Double operator checked +(Vector3Double left, Vector3Double right) => checked(new(left.X + right.X, left.Y + right.Y, left.Z + right.Z));
    public static Vector3Double operator *(Vector3Double left, double right) => new(right * left.X, right * left.Y, right * left.Z);
    public static Vector3Double operator checked *(Vector3Double left, double right) => checked(new(right * left.X, right * left.Y, right * left.Z));
    public static Vector3Double AdditiveIdentity => new Vector3Double();
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Vector3Int :  IVector3<Vector3Int, int>
{
    public int X { get; init; }
    public int Y { get; init; }
    public int Z { get; init; }

    public Vector3Int(int x, int y, int z) => (X, Y, Z) = (x, y, z);

    public static Vector3Int operator +(Vector3Int left, Vector3Int right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
    public static Vector3Int operator checked +(Vector3Int left, Vector3Int right) => checked(new(left.X + right.X, left.Y + right.Y, left.Z + right.Z));
    public static Vector3Int operator *(Vector3Int left, int right) =>  new(right * left.X, right * left.Y, right * left.Z);
    public static Vector3Int operator checked *(Vector3Int left, int right) => checked(new(right * left.X, right * left.Y, right * left.Z));
    public static Vector3Int AdditiveIdentity => new Vector3Int();
}

And now you will be able to create extension methods base on these interfaces, for instance:

public static class VectorExtensions
{
    public static TValue Dot<TVector, TValue>(this TVector left, TVector right) where TValue : unmanaged, INumber<TValue> where TVector : IVector3<TVector, TValue>, new ()
        => left.X + right.X + left.Y + right.Y + left.Z + right.Z;
    
    public static TVector Sum<TVector, TValue>(this IEnumerable<TVector> vectors) where TValue : unmanaged, INumber<TValue> where TVector : IVector3<TVector, TValue>, new ()
        => vectors.Aggregate(TVector.AdditiveIdentity, (s, c) => s + c);

    public static TVector Lerp<TVector, TValue>(TVector a, TVector b, TValue t) where TValue : unmanaged, INumber<TValue> where TVector : IVector3<TVector, TValue>, new ()
        => a * (TValue.MultiplicativeIdentity - t) + b*t;
}

With that done, you can do things like:

var vec1 = new Vector3Double(1.1, 1.1, 1.1);
var vec2 = new Vector3Double(2.2, 2.2, 2.2);

var sum = vec1 + vec2;
var scaled = vec1 * 2.2;
var dotP = vec1.Dot<Vector3Double, double>(vec2);
var aggregate = new [] { vec1, vec2 }.Sum<Vector3Double, double>();

Notes:

  • I was unable to get type inferencing to work automatically for the VectorExtensions methods, which is why I had to specify the generic parameters explicitly.

  • Your vector type is a struct, and Microsoft recommends that structs be immutable. So, in my IVector3 interface, I made X, Y and Z be init-only.

Demo #3 here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Even though I cannot use .NET 7 because I'm under Unity, I accept your answer because it's definitely right! – aybe Feb 22 '23 at 20:27