Programming

Passing a struct as ref through an interface in C#

Didn’t think it’d be any complicated. I’m in the middle of a large refactoring job that is primarily concerned with memory adjacency. When data lies in memory continuously instead of being distributed segments, parallel code scales better because of CPU caching. This is currently my problem with the Parallel implementation of CombInduce, the ‘solver’/program synthesiser that forms the basis of our work in Interpretable AI.

This memory adjacency work involves, among other things, to replace a lot of classes with structs. Along with not being able to have inheritance, this change have had other unexpected behaviour. Having lost inheritance, I’ve been relying on interfaces for abstraction, but I had forgotten about boxing. When structs are passed through an interface, they’re boxed, meaning, their value is copied into a reference-type object to be able to passed as an interface type.

To demonstrate, let’s start with a struct:

struct Foo
 : IFoo
{
  public int A;
  public void SetA(int a)
  {
    this.A = a;
  }
}

class Program
{
  static void setAsFoo(Foo f)
  {
    f.SetA(15);
  }

  static void setAsRefFoo(ref Foo f)
  {
    f.SetA(20);
  }
  ...
}

This is how you expect it to behave:

    Foo f = new Foo();
    f.A = 5;
    Console.WriteLine("f.A=" + f.A);
    setAsFoo(f);
    Console.WriteLine("after setAsFoo, f.A=" + f.A);
    setAsRefFoo(ref f);
    Console.WriteLine("after setAsRefFoo, f.A=" + f.A);
// Console:
// f.A=5
// after setAsFoo, f.A=5
// after setAsRefFoo, f.A=20

The struct is modified in-place only when passed with the ref keyword. But if you use an interface, boxing means it’s functionally pass-by-value:

  static void setAsIFoo(IFoo i)
  {
    i.SetA(25);
  }
    Foo f = new Foo();
    f.A = 5;
    Console.WriteLine("f.A=" + f.A);
    setAsFoo(f);
    Console.WriteLine("after setAsFoo, f.A=" + f.A);
    setAsRefFoo(ref f);
    Console.WriteLine("after setAsRefFoo, f.A=" + f.A);
    setAsIFoo(f);
    Console.WriteLine("after setAsIFoo, f.A=" + f.A);
// Console:
// f.A=5
// after setAsFoo, f.A=5
// after setAsRefFoo, f.A=20
// after setAsIFoo, f.A=20

If you try to declare it as ref IFoo and pass it as SetAsIFoo(ref f), the C# compiler throws the towel:

Argument 1: cannot convert from 'ref Foo' to 'ref IFoo' [Structs]csharp(CS1503)

It can’t convert from ref Foo to ref IFoo, even though it can convert from Foo to IFoo. Apparently what you have to do, is to use a generic type and constrain it to the interface, then it magically works out. First,

  static void setAsRefT<T>(ref T i) where T : IFoo
  {
    i.SetA(35);
  }

And call it with the ref keyword:

f.A=5
after setAsRefT, f.A=35
// Console:
// f.A=5
// after setAsRefT, f.A=35

That’s what I want. But if the compiler can do this, why not let it me do it by ‘ref’ing the interface type? If we look at the IL code generated, we can see how the compiler treats the interface call and the ‘ref T’ call not so differently:

.method private hidebysig static 
	void setAsIFoo (
		class IFoo i
	) cil managed 
{
// ...
	IL_0004: callvirt instance void IFoo::SetA(int32)
// ...
}

.method private hidebysig static 
	void setAsRefT<(IFoo) T> (
		!!T& i
	) cil managed 
{
// ...
	IL_000a: callvirt instance void IFoo::SetA(int32)
// ...
}

Both setAsFoo and setAsRefT lead to virtual dispatch, but the interface call declares the parameters as class IFoo, while the generic one doesn’t. I suppose this kind of implies interfaces are treated as the same thing as a pure abstract class, internally, and that’s why the compiler refuses to implicitly convert ref Foo to ref IFoo.

The difference is visible in the calls too:

call void Program::setAsIFoo(class IFoo)
call void Program::setAsRefT<valuetype Foo>(!!0&)

If I wanted to bury more time in this I’d try to compile from IL and try to do an IFoo reference to figure out if it’s a C# limitation or a platform limitation. But this time the constrained generic will do.

2 thoughts on “Passing a struct as ref through an interface in C#”

  1. I was stuck, when I was using C#, by the number of things that C# would not do,
    even when the request was eminently reasonable.

    In short, it is not an orthogonal language. You should be able to use programming
    mechanisms without restriction.

    You have my sympathy.

    My theory is that the problems arise because there is no formal programming model
    behind C# that would supply the elegance of orthogonality. It is a cobbled together
    rival for Java. C# is an easy language to use for simple tasks especially within a good IDE,
    but the moment things get complicated, beware!

    As much as I hate C++, I would probably use this for ultimate memory-tuning.

    How many lines in your code base? Might be worth auto-translating this into something else?. 🙂

    1. Not so many LOC, maybe around 50K all in all, and frankly C/C++/Rust would be the ideal language for this job. But it just feels easier to deal with C#. That is of course until one of these edge cases hits you. Thankfully there was a workaround. If I was trying to squeeze every drop of efficiency I’d probably go with Rust, but in this case I only have to show parallel scalability.

      I very much agree with your observation on lack of orthogonality. I hadn’t thought this was the underlying problem but I guess you’re right. Mark Sutter’s book Exceptional C++ was full of stimulating examples mostly based on this combination of OOP and orthogonality to the underlying hardware. At times it’s quite fun to write C++ when you can deal with OO concepts and play very close to hardware, pointer arithmetic and all. But a lot of the time it’s just torture, too. In case of C# or similar, made-up abstraction always starts leaking at the seams.

      Thanks for the first non-spam comment on my blog! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *