1

I am currently writing a library that encodes and decodes images in qoi format. This image format uses rgb and rgba pixel formats.

I made the IPixel interface and two implementations RgbPixel and RgbaPixel. IPixel is not intended to be used as an extension point (because there are only rgb and rgba), I made this interface to use generic classes so as not to write the same thing twice.

RgbPixel and RgbaPixel are structs with the [StructLayout] attribute. Some of my generic classes use unsafe code to unsafely cast these structs and other unmanaged types to each other to improve performance, so if user/another implementation of IPixel gets to them, it will most likely cause an exception.

To avoid all these problems, I would like to allow only internal implementation of the IPixel interface. But I don't know how to do it in C#.

If IPixel were an abstract class, I could prevent implementation from other assemblies by having only internal constructors. But I can only use interfaces because I need RgbPixel and RgbaPixel to be structs.

.netstandart 2.0, c# 10.0

SAY
  • 26
  • 3
  • 2
    As you know, if you are ever using `IPixel` in a context in which it isn't merely a generic constraint, you will [incur the cost of boxing](https://stackoverflow.com/questions/3032750/structs-interfaces-and-boxing) and obviate the use of structs. So given that, I'm curious what those generic scenarios are. If it were mediated through a generic class type constraint, you could use a static constructor to validate the type. If it's through a method parameter generic type constraint you wouldn't have that option. Could you elaborate on how you're using the interface? – Kirk Woll Jun 20 '22 at 00:09
  • @KirkWoll I store the data as byte array and use T (where T : unmanaged, IPixel) only for casting. IPixel allows me to eliminate some branching, and gives me control over the endianness. So I don't have boxing allocations – SAY Jun 20 '22 at 00:23
  • Right, it's the `T : unmanaged, IPixel` that's key. But where is that constraint defined? On a type constraint to a class or method? – Kirk Woll Jun 20 '22 at 00:28
  • @KirkWoll I have a class with that constraint and 2 static methods – SAY Jun 20 '22 at 00:31
  • Perfect. In that case, can't you just add a static constructor to the class that checks that that type parameter is one of the two types you've prescribed? (And throws an exception if not) – Kirk Woll Jun 20 '22 at 00:33
  • @KirkWoll Of course I can, but it's runtime. I thought maybe there is a cleaner way to do this, but if not, then I will use this approach :) Thanks – SAY Jun 20 '22 at 00:41
  • Yup, totally reasonable desire. Reminds me a lot of this [similar answer around delegates](https://stackoverflow.com/questions/191940/c-sharp-generics-wont-allow-delegate-type-constraints). – Kirk Woll Jun 20 '22 at 00:47

2 Answers2

0

I would make IPixel internal and have public shim methods that accept RgbPixel and RgbaPixel which then call the common method that accepts IPixel (or a base class).

tymtam
  • 31,798
  • 8
  • 86
  • 126
0

I made all the class constructors internal and added two static methods Load and New<T>, the first one takes an encoded data and by decoding the header it creates either QImage<RgbPixel> or QImage<RgbaPixel>, the second method merely checks the type parameter passed by the user, as suggested by Kirk Woll in the comments.

Looks something like this:


public class QImage : IImage {
internal QImage(/*args*/) { /*...*/ }

public static QImage Load(byte[] bytes) {
    //...

    var qr = new QoiBytesReader(bytes);
    var qHeader = Decoder.DecodeHeader(ref qr);

    //ugly :|
    return qHeader.Channels == Channels.Rgb 
         ? new QImage<RgbPixel>(Decoder.DecodePixels<RgbPixel>(ref qr, in qHeader), qHeader) 
         : new QImage<RgbaPixel>(Decoder.DecodePixels<RgbaPixel>(ref qr, in qHeader), qHeader);
    }

//...
}

public class QImage<T> : QImage where T : unmanaged, IPixel {
    internal QImage(/*args*/) : base(/*params*/) { /*...*/ }

public static QImage<T> New(byte[] pixels, QHeader qHeader) {
    if (typeof(T) == typeof(RgbPixel) || typeof(T) == typeof(RgbaPixel)) 
        return new QImage<T>(pixels, qHeader);
        
    throw new NotSupportedException($"Specified type <{typeof(T).Name}> is not supported. Use <{nameof(RgbPixel)}> or <{nameof(RgbaPixel)}> instead.");
    }
//...
}

internal static unsafe class Decoder {
internal static byte[] DecodePixels<T>(ref QoiBytesReader qr, in QHeader metadata) 
    where T : unmanaged, IPixel { /*decode*/ }

//...
}

SAY
  • 26
  • 3