Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How to have readonly methods in interfaces?

Discussion in 'Entity Component System' started by davenirline, Apr 1, 2022.

  1. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    Readonly methods matter a lot in DOTS due to the usage of in modifier when passing structs. I just found out that I can't have readonly methods in interface. Consider this example:

    Code (CSharp):
    1. interface IConditionResolver {
    2.     bool IsMet(); // Can't be readonly
    3. }
    4.  
    5. // Say you have this generic method
    6. void Foo<T>(in T resolver) where T : struct, IConditionResolver {
    7.     // The passed resolver here will be copied prior to calling IsMet() since IsMet() is not marked as a readonly method
    8.     if(resolver.IsMet()) {
    9.         ...
    10.     }
    11. }
    12.  
    I want to use in parameters but also prevent copying when readonly methods are invoked, but I can't. The solution of course is to not use in but this means copying for every invocation of such method. What I want should at least be possible I think. Is this some kind of C# limitation?

    Passing structs without copying is important to me because some of these kinds of struct could be big with references to native collections or ComponentDataFromEntity or Blob* stuff.
     
  2. Anthiese

    Anthiese

    Joined:
    Oct 13, 2013
    Posts:
    72
    If you could tighten the type parameter constraint to unmanaged, you could convert to a pointer with a fixed statement and call the interface method without a copy.

    Otherwise, you'll probably still remain constrained by what the compiler's willing to do here. You could [Pure] the interface method and pretend everything's OK even though it's not and the IL will still have a ldobj. Depending on where you're getting the data from in the first place, you could just make them ref to begin with (like fields in a job struct?).

    Code (csharp):
    1.  
    2. BenchmarkDotNet=v0.13.1, OS=macOS Monterey 12.3 (21E230) [Darwin 21.4.0]
    3. Apple M1, 1 CPU, 8 logical and 8 physical cores
    4. .NET SDK=6.0.201
    5.   [Host]     : .NET 6.0.3 (6.0.322.12309), Arm64 RyuJIT
    6.   DefaultJob : .NET 6.0.3 (6.0.322.12309), Arm64 RyuJIT
    7.  
    8.  
    9. |                 Method |         Mean |       Error |      StdDev |
    10. |----------------------- |-------------:|------------:|------------:|
    11. | ExecSuckThroughPointer |     5.526 us |   0.0491 us |   0.0460 us |
    12. |      ExecSuckInGeneral | 9,296.028 us | 185.2776 us | 454.4891 us |
    13.  
    (sample attached)
     

    Attached Files:

    • abc.zip
      File size:
      888 bytes
      Views:
      189
    davenirline likes this.
  3. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    That's interesting. I can't constrain it to unmanaged due to usage of pointers like native collections and such structures. But yeah, pass by ref is another solution even though it suggests that the passed value can be modified.
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    The collections package contains a utility to convert in to ref. If consuming the in parameter is internal logic, that might be your best shot to get the API you want.
     
  5. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    Which one is that?
     
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
  7. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
  8. Mortuus17

    Mortuus17

    Joined:
    Jan 6, 2020
    Posts:
    105
    This might be dumb but...

    This...:
    Code (CSharp):
    1.  
    2. public interface AAA
    3. {
    4.     void DO();
    5. }
    6. public struct BBB : AAA
    7. {
    8.     public readonly void DO()
    9.     {
    10.  
    11.     }
    12. }
    ... is legal C# code and should, SHOULD, do what you want, since you use generics correctly (with structs in regards to performance) and thus the method call is not a virtual one. The C# compiler has access to the exact method definition and sees the
    readonly
    metadata, resulting in the code gen you want.

    Am I wrong?
     
  9. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    296
    C# Compiler cannot use this information, it compiles Foo method for any value type (not just specific one with readonly method). As you can see in the IL, there's
    ldobj
    , which is making a copy.
    JIT could do this in theory as optimization, but I wouldn't bet on it.

    Code (CSharp):
    1.  
    2. // Say you have this generic method
    3. static void Foo<T>(in T resolver)
    4.     where T : struct, IConditionResolver
    5. {
    6.     // The passed resolver here will be copied prior to calling IsMet() since IsMet() is not marked as a readonly method
    7.     if (resolver.IsMet())
    8.     {
    9.         Console.WriteLine("IsMet");
    10.     }
    11.     else
    12.     {
    13.         Console.WriteLine("IsNotMet");
    14.     }
    15. }
    16.  
    Code (CSharp):
    1.  
    2. .method private hidebysig static
    3.     void Foo<valuetype .ctor (ReadonlyTest2.Program/IConditionResolver, [System.Runtime]System.ValueType) T> (
    4.         [in] !!T& resolver
    5.     ) cil managed
    6. {
    7.     .param [1]
    8.         .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
    9.             01 00 00 00
    10.         )
    11.     // Method begins at RVA 0x2050
    12.     // Header size: 12
    13.     // Code size: 54 (0x36)
    14.     .maxstack 1
    15.     .locals init (
    16.         [0] bool,
    17.         [1] !!T
    18.     )
    19.  
    20.     // {
    21.     IL_0000: nop
    22.     // T val = resolver;
    23.     IL_0001: ldarg.0
    24.     IL_0002: ldobj !!T
    25.     IL_0007: stloc.1
    26.     // if (val.IsMet())
    27.     IL_0008: ldloca.s 1
    28.     IL_000a: constrained. !!T
    29.     IL_0010: callvirt instance bool ReadonlyTest2.Program/IConditionResolver::IsMet()
    30.     IL_0015: stloc.0
    31.     IL_0016: ldloc.0
    32.     IL_0017: brfalse.s IL_0028
    33.  
    34.     // Console.WriteLine("IsMet");
    35.     IL_0019: nop
    36.     IL_001a: ldstr "IsMet"
    37.     IL_001f: call void [System.Console]System.Console::WriteLine(string)
    38.     // (no C# code)
    39.     IL_0024: nop
    40.     // }
    41.     IL_0025: nop
    42.     IL_0026: br.s IL_0035
    43.  
    44.     // Console.WriteLine("IsNotMet");
    45.     IL_0028: nop
    46.     IL_0029: ldstr "IsNotMet"
    47.     IL_002e: call void [System.Console]System.Console::WriteLine(string)
    48.     // (no C# code)
    49.     IL_0033: nop
    50.     IL_0034: nop
    51.  
    52.     IL_0035: ret
    53. } // end of method Program::Foo
     
    davenirline likes this.
  10. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    296
    You could have two structs, one with the data (that would be never copied) and one which would provide behavior on that data and that would be copied, but since it would be empty, it should not matter.

    It's overly complicated and I wouldn't use it, but it's a way.

    Example:

    Code (CSharp):
    1. using System;
    2. using System;
    3.  
    4. namespace ReadonlyTest2
    5. {
    6.     class Program
    7.     {
    8.         interface IConditionResolver<T>
    9.         {
    10.             bool IsMet(in T data);
    11.         }
    12.  
    13.         struct CondData
    14.         {
    15.             public int Value;
    16.  
    17.             public readonly bool IsMet()
    18.             {
    19.                 return Value > 0;
    20.             }
    21.         }
    22.  
    23.         struct TestCond : IConditionResolver<CondData>
    24.         {
    25.             public bool IsMet(in CondData data)
    26.             {
    27.                 return data.IsMet();
    28.             }
    29.         }
    30.  
    31.         // Say you have this generic method
    32.         static void Foo<T, TData>(in T resolver, in TData data) where T : struct, IConditionResolver<TData>
    33.         {
    34.             // The passed resolver here will be copied prior to calling IsMet() since IsMet() is not marked as a readonly method
    35.             if (resolver.IsMet(data))
    36.             {
    37.                 Console.WriteLine("IsMet");
    38.             }
    39.             else
    40.             {
    41.                 Console.WriteLine("IsNotMet");
    42.             }
    43.         }
    44.  
    45.         static void Main(string[] args)
    46.         {
    47.             CondData c = new CondData { Value = args.Length == 0 ? 100 : 0 };
    48.             Foo(default(TestCond), c);
    49.         }
    50.     }
    51. }
    52.  
    53.  
     
    davenirline likes this.
  11. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    That's interesting! Sad that it doesn't work. Maybe because upon invocation, the compiler is looking at the definition of the interface, not the concrete implementation, thus still making the copy.
     
  12. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    Yes, that also works! Foo could actually be rewritten to accept the same T then just move the code of readonly IsMet() to another method or inline them in the interface's IsMet().
    Code (CSharp):
    1. Foo<T>(in T resolver) where T : struct, IConditionResolver<T> {
    2.     if(resolver.IsMet(resolver)) {
    3.         ...
    4.     }
    5. }
    But I hate that we have to go through hoops just to make this work.