0

I'm writing a library for ternary computing, based on Trits instead of Bits. Trits can have three values. Sometimes represented as 0,1 and 2, sometimes as -1, 0 and 1. I've called them Down, Middle and Up.

In some scenario's, it's useful to have these custom struct values as constants. At this moment I'm working around it with static readonly values:

global using trit = Ternary3.Trit;
global using static Ternary3.Trit.Values;

public readonly partial struct Trit
{
    [SpecialName]
    private readonly sbyte value__;

    private Trit(byte value) => value__ = value;

    public readonly struct Values
    {
        public static readonly Trit down = new Trit(0);
        public static readonly Trit middle = new Trit(1);
        public static readonly Trit up = new Trit(2);
    }
}

Which enables:

void MyMethod(bool b, trit t) => ...

...

MyMethod(true, up);

In some cases, using an actual constant is a must. In attributes, for example. A somewhat more advanced workaround is using an enum and an implicit cast:

global using trit = Ternary3.Trit;
global using static Ternary3.Trit.Values;

public readonly partial struct Trit
{
    ...

    public enum Values : byte
    {
        down,
        middle,
        up
    }

    public static implicit operator Trit(Trit.Values value) => new Trit((byte)value);
}

However, this still isn't a true struct constant, which cannot be achieved in pure c#.

If, however, you decompile using ildasm, modify the Intermediate Language (CIL) from .field public static initonly valuetype Ternary3.Trit up to .field public static literal valuetype Ternary3.Trit up = uint8(0x02), and recompile using ilasm, you get a dll that contains the constant you want.

Now the question: what would be a good way to modify the CIL? I've found the InlineIL.Fody NuGet-package, but I cannot find a way to create a field declaration, let alone a literal field declaration.

Am I on the right track?

edit

As a duct tape fix I created a powershell script that calls ildasm, replaces the constant and then calls ilasm. This creates the library that works as expected on any runtime and language I've tested it against. However, I'm still looking for a better way to manipulate the il.

param([string]$DllName);
$Ildasm = """C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\ildasm.exe"""
$CilName = "$DllName.il"
Start-Process $Ildasm -Argument "$DllName /OUT:$CilName"

# replace 
#.field public static initonly valuetype Ternary3.Trit down
#.field public static initonly valuetype Ternary3.Trit middle
#.field public static initonly valuetype Ternary3.Trit up
# by
#.field public static literal valuetype Ternary3.Trit down = uint8(0x00)
#.field public static literal valuetype Ternary3.Trit middle = uint8(0x01)
#.field public static literal valuetype Ternary3.Trit up = uint8(0x02)

$FixedCilName = "$DllName.fixed.il"
(Get-Content $CilName). replace(".field public static initonly valuetype Ternary3.Trit down", ".field public static literal valuetype Ternary3.Trit down = uint8(0x00)").replace(".field public static initonly valuetype Ternary3.Trit middle", ".field public static literal valuetype Ternary3.Trit middle = uint8(0x01)").  replace(".field public static initonly valuetype Ternary3.Trit up", ".field public static literal valuetype Ternary3.Trit up = uint8(0x02)") | Set-Content $FixedCilName
Remove-Item $CilName

$Ilasm = """C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ilasm.exe"""
Start-Process $Ilasm -Argument "/DLL ""$FixedCilName"" /OUTPUT=""$DllName"""

Remove-Item $FixedCilName

It DOES produce sort-of-the right code. Using the Ternary3 library

  • Edit *

I've written a small custom Fody Code Weaver to enable creating constant custom structs. This approach also sort-of works, but it still has the disadvantage of making it impossible to have unit tests in the same solution as the actual code: Visual Studio ignores Fody modifications.

realbart
  • 3,497
  • 1
  • 25
  • 37
  • 1
    There's nothing in the spec to suggest that what you're doing here has to be supported by a compiler. Specifically, it's up to a compiler to decide how to coerce the value `0x00` to a `Ternary3.Trit`, and you don't appear to have specified any way of converting that value (the implicit conversion coerces from an enum only). If this works I'm actually a bit mystified how the C# compiler is handling it -- it may be that the code it uses for enums happens to work for structs like this too. It would certainly be worth testing if this still works for .NET Core. – Jeroen Mostert Mar 22 '22 at 17:02
  • I've built it in core. Am I using the incorrect ildasm/ilasm? I could also let my Trit inherit from enum... But this has the same problem: there's no way to do this in c#. – realbart Mar 23 '22 at 13:06
  • 1
    No, the IL is correct (or at least, it's valid) but the problem (if you can call it that) is that this kind of struct constant is not guaranteed to be supported by compilers. The IL spec leaves it open how such values are to be interpreted by compilers (all they see is "here's a `byte`, you figure out how to make a `Trit` out of it"). In the case of enums this is clear, but in the case of arbitrary value types that are initialized by numeric values it's not. If the C# compiler happens to support this, great; it's not necessarily portable across .NET languages, though. – Jeroen Mostert Mar 23 '22 at 13:52
  • By using a backing field named ```value__```, decorated with a ```SpecialNameAttribute```, I'm telling the *runtime* to treat this struct just like it would treat enums. It seems to work: I tested on the dotnet 6.0.200 on a Mac, Windows and Ubuntu. Oh and in a browser with Blazor WebAssembly. I've not seen any *runtime* specification, and I agree I should look into this before shipping my library. However, *compilers* must simply copy the struct value to any place the constant is used. So even if C# (or any other language) forbids creation of custom struct constants, you can *use* them. – realbart Mar 23 '22 at 23:15
  • 1
    No, again: when it works it will work cross-platform since you're certainly allowed to *declare* that, it will just not necessarily work *cross-language* since the specs don't formally say how this works beyond enums (at least I didn't see any details on it). I know this is not an issue for most people since C# is the only .NET language they care about anyway, but let's not forget that VB.NET, F# and C++/CLI are still a thing. :P – Jeroen Mostert Mar 24 '22 at 06:46
  • Dank je! I'll keep seeing it as an experimental feature then... It seems to work fine under VB and C#. The specs say "The literal constraint promises that the value of the location is actually a fixed value of a built-in type. The value is specified as part of the constraint. **Compilers are required to replace all references to the location with its value...**" So even though I'm bending the rules, I'm fairly confident it will work and keep working on the compiler side. Just tested with F# and VB. – realbart Mar 24 '22 at 08:14
  • Apart from the question whether I *should* do this, I'm still curious of ways I *can* do this. There's nuget packages that allow you to insert IL into c#, but the ones I've seen focus on a *method* body. – realbart Mar 24 '22 at 08:27

0 Answers0