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.
- 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.