Skip to content

Instantly share code, notes, and snippets.

@sodiboo
Last active September 23, 2022 20:16
Show Gist options
  • Save sodiboo/12e00f47e52973899c711d87963855fc to your computer and use it in GitHub Desktop.
Save sodiboo/12e00f47e52973899c711d87963855fc to your computer and use it in GitHub Desktop.
Let's Fix Math (but only for the worst number type)

So, recently, i wrote a couple tweets where i explored how to theoretically overwrite the division implementation for C# numbers.

In short:

  • C# doesn't really have "primitives" but .NET does.
  • For primitives, division works with the div instruction. You can't edit this
  • C# does have first-class support like .NET's primitives (i.e. numeric literals, a keyword) for the decimal type
  • decimal corresponds to System.Decimal which is not a primitive type. Let's break it! i mean, uhh, fix math!
  • The goal is to make 0 / 2 == 0.5, or since we're doing decimals only, 0m / 2m == 0.5m. M for money i guess? i don't really know why that's the suffix C# uses, d was already taken for double i guess.

This is basically an introduction to HarmonyX i guess? That's not what i intended to make, but that's what this document is.

So what do we want to override? Override is the wrong word. That's used for inheritance. What do we want to hijack? Well, public static decimal operator /(decimal d1, decimal d2) seems like a good place to start. It actually just... calls another method?

public static decimal operator /(decimal d1, decimal d2)
{
    DecCalc.VarDecDiv(ref AsMutable(ref d1), ref AsMutable(ref d2));
    return d1;
}

And there's also a second method public static decimal Divide(decimal d1, decimal d2) But it has the same implementation? Guess it's for those languages without operator overloading.

public static decimal Divide(decimal d1, decimal d2)
{
    DecCalc.VarDecDiv(ref AsMutable(ref d1), ref AsMutable(ref d2));
    return d1;
}

Now what the heck is this DecCalc class and what is VarDecDiv? Variable Decrement Division? Sounds like an algorithm name?

If we go to the implementation of DecCalc, we see a bunch of division methods for some reason?

  • Div96By32
  • Div96ByConst
  • Div96By64
  • Div128By96
  • DivByConst
  • VarDecDiv
  • DecDivMod1E9

They seem to be all private though, except for VarDecDiv and DecDivMod1E9. Apparently DecDivMod1E9 is used by number formatting according to that region name (such a short region. i thought it was a comment until i had to describe it and i looked again and nope. that's not actually a comment) - We don't wanna fuck with the formatting, so let's leave it alone. We want just VarDecDiv. This is the implementation. And after seeing all these differently sized division methodn names, the function name makes some more sense. Its implementation is long, and breaks GitHub's highlighting (after line 2009 it thinks if is a method) but i guess it's like, "Variable Length Division". It picks the best implementation for the size of the numbers. Dec probably means decimal, which is... redundant, but i'll allow it. I'm not gonna paste the whole implementation here, due to its length.

Let's fix math! or at least, let's fix division.

$ dotnet new console -o fixmath
$ cd fixmath

How do we patch methods? I've done some Unity game modding in the past, so i know exactly how to do this. We'll use HarmonyX, which is "a fork of Harmony 2 that specializes on support for games and game modding frameworks". Game modding consists largely of code crimes with reflection. This sounds perfect for us. They have a NuGet package and on there if you click the tab called ".NET CLI" you get a command that is supposed to install it. I don't get paid enough to check the legitimacy of that command, let's run it. Not as root, though.

$ dotnet add package HarmonyX --version 2.10.0

And let's just write some boilerplate to show the current behaviour:

using HarmonyLib;

Console.WriteLine("Hello World!");
var x = 0m;
var y = 2m;
Console.WriteLine(x / y);

outputs:

Hello World!
0

Oof. That second line hurts. We don't want that. Let's first make our own method. Also, we're moving it all to a class, because Harmony needs the type.

using HarmonyLib;

class Program
{

    static void Main()
    {
        Console.WriteLine("Hello World!");
        Console.WriteLine(DivideMyNumbers(0, 2));
    }

    public static decimal DivideMyNumbers(decimal x, decimal y) => x / y;
}

Runs the same. We want to also patch stuff. In Harmony, you can do stuff manually, but there are also attributes that make your code look nicer. Let's use those attributes. We add another class, this time annotated with HarmonyPatch. And a method in there, to patch our DivideMyNumbers method.

[HarmonyPatch]
class Patches
{
    [HarmonyPrefix, HarmonyPatch(typeof(Program), nameof(Program.DivideMyNumbers))]
    static void PatchDivision()
    {
        Console.WriteLine($"I'm boutta divide");
    }
}

We run the code and... nothing. Harmony is never invoked. At the top, we need System.Reflection to get the current assembly.

using System.Reflection;

and in the Main method, we wanna call Harmony.CreateAndPatchAll() and tell it to search the current assembly.

    static void Main()
    {
        Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly());
        Console.WriteLine("Hello World!");
        Console.WriteLine(DivideMyNumbers(0, 2));
    }

And... nothing. I actually spent way too long fucking around with method impl options to prevent inlining and optimizations because THEY WILL BREAK YOUR CODE. And i couldn't find a solution. Is it the default compiler settings? Who knows! For now, we're changing the DivideMyNumbers(0, 2) to typeof(Program).GetMethod("DivideMyNumbers")!.Invoke(null, new object[] { 0m, 2m }). It's equivalent, just a bit more verbose. This prevents the method from being optimized in any way, because we're intentionally calling it the slowest/worst way possible. We run it after that and...

Hello World!
I'm boutta divide
0

Woo! We've patched a method. Let's try to like, actually make this patch work. First, let's get some parameters in there. You don't have to take them all, but we wanna do that in our case.

    [HarmonyPrefix, HarmonyPatch(typeof(Program), nameof(Program.DivideMyNumbers))]
    static void PatchDivision(decimal x, decimal y)
    {
        Console.WriteLine($"I'm boutta divide {x} by {y}");
    }
Hello World!
I'm boutta divide 0 by 2
0

To modify the result, we need to take a parameter by the name __result.

    [HarmonyPrefix, HarmonyPatch(typeof(Program), nameof(Program.DivideMyNumbers))]
    static void PatchDivision(ref decimal __result, decimal x, decimal y)
    {
        Console.WriteLine($"I'm boutta divide {x} by {y}");

        if (x is 0 && y is 2) {
            __result = 0.5m;
        }
    }

ref means it's a "managed reference". It's like &mut T. We can write to it. But the code still says 0 / 2 is 0. What gives? Well, we're just updating the result in the prefix. The result of the call is overwritten by the original implementation. We want to... not run the original. You could return a bool but that's confusing. Instead, we'll take another magic parameter called __runOriginal

    [HarmonyPrefix, HarmonyPatch(typeof(Program), nameof(Program.DivideMyNumbers))]
    static void PatchDivision(ref decimal __result, ref bool __runOriginal, decimal x, decimal y)
    {
        Console.WriteLine($"I'm boutta divide {x} by {y}");

        if (x is 0 && y is 2) {
            __result = 0.5m;
            __runOriginal = false;
        }
    }

The order of these parameters don't matter. You might be wondering where these magic names come from, why prefixed with double underscores? Here's a list of them.

Hello World!
I'm boutta divide 0 by 2
0,5

The code works! 0 / 2 is now 0,5, because .NET likes to print things in the current locale by default. You might think that's dumb that your code prints different output to mine despite being identical, and that's because it is dumb. Here's a fun short thread about a game where French players get two cars because of that.

But 0 / 2 isn't 0,5, just DivideMyNumbers(0, 2). Let's patch VarDecDiv instead.

    [HarmonyPrefix, HarmonyPatch(typeof(Decimal.DecCalc), nameof(Decimal.DecCalc.VarDecDiv))]
    static void PatchDivision(ref decimal __result, ref bool __runOriginal, decimal x, decimal y)
    {
        Console.WriteLine($"I'm boutta divide {x} by {y}");

        if (x is 0 && y is 2) {
            __result = 0.5m;
            __runOriginal = false;
        }
    }

Oh No It's Private

So they can't just let us off that easily. Roslyn tells me DecCalc does not exist. I'm not sure i believe that, because i can open my own assembly (the last good compiled version) in ILSpy, go to the definition of decimal which it magically finds somewhere that apparently isn't my build artifacts directly, and i can see it right there!

The Decimal type in ILSpy is expanded, and the private nested type DecCalc is selected

Now, of course, that's a problem game modders have already solved, right? Yeah, they have. OpenSesame is a Roslyn compiler (fork? plugin? i'm actually not sure) that ignores visibility rules on purpose. But that's boring. That's like, cheating almost. No, we can't have that.

First, i just wanna show off Traverse which lets the above reflection code look a bit nicer and it's also strongly typed with the <decimal> generic for the return type.

    static void Main()
    {
        Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly());
        Console.WriteLine("Hello World!");
        Console.WriteLine(Traverse.Create<Program>().Method("DivideMyNumbers", 0m, 2m).GetValue<decimal>());
    }

    public static decimal DivideMyNumbers(decimal x, decimal y) => x / y;

But, now, we need to patch VarDecDiv. Forget what i said about Traverse though, it won't help us patch things, only call them.

I removed the attributes on my patch class, because to patch private methods, you need to manually call Harmony.Patch on an instance. Let's do that. Here's the full new code:

using System.Reflection;
using HarmonyLib;

class Program
{

    static void Main()
    {
        var harmony = new Harmony("fixmath");
        harmony.Patch(
            original: AccessTools.Method(AccessTools.Inner(typeof(decimal), "DecCalc"), "VarDecDiv"),
            prefix: new HarmonyMethod(typeof(Patches), nameof(Patches.PatchDivision))
        );
        Console.WriteLine("Hello World!");
        Console.WriteLine(Traverse.Create<Program>().Method("DivideMyNumbers", 0m, 2m).GetValue<decimal>());
    }

    public static decimal DivideMyNumbers(decimal x, decimal y) => x / y;
}

class Patches
{
    public static void PatchDivision()
    {
        Console.WriteLine($"I'm boutta divide");

        // if (x is 0 && y is 2) {
        //     __result = 0.5m;
        //     __runOriginal = false;
        // }
    }
}

And it prints! But, uh, these parameters are DecCalcs. Not Decimals. We can take them as objects though.

    public static void PatchDivision(ref object d1, object d2)
    {
        Console.WriteLine($"I'm boutta divide {d1} by {d2}");

        // if (x is 0 && y is 2) {
        //     __result = 0.5m;
        //     __runOriginal = false;
        // }
    }

The first parameter has to be ref because this method is special in that it doesn't have a result type. There's some performance optimization probably by overwriting the first object, because it's reinterpreting the reference, mem::transmuteing it if you will. That's what the AsMutable method above does, by the way. I guess that's why there are two structs, right? Decimal is immutable, but DecCalc is not. That makes sense.

But our code is gonna get messy from here. i think. Actually, wait, can i just use dynamic?

class Patches
{
    public static void PatchDivision(ref dynamic d1, ref dynamic d2)
    {
        Console.WriteLine($"I'm boutta divide {d1} by {d2}");

        // if (x is 0 && y is 2) {
        //     __result = 0.5m;
        //     __runOriginal = false;
        // }
    }
}

Holy shit dynamic works, that's a godsend. Let's just pray to the runtime gods that we can legit just transmute these back to decimals.

class Patches
{
    public static void PatchDivision(ref bool __runOriginal, ref dynamic d1, ref dynamic d2)
    {
        ref var x = ref Unsafe.As<dynamic, decimal>(ref d1);
        ref var y = ref Unsafe.As<dynamic, decimal>(ref d2);

        Console.WriteLine($"I'm boutta divide {x} by {y}");

        if (x is 0 && y is 2) {
            x = 0.5m;
            __runOriginal = false;
        }
    }
}
I'm boutta divide -0,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012230191320869432721409 by -0,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012230191323719758785920

Of course not. That would be silly. Why would it ever work? What the fuck just happened? I think i fucked up with the Unsafe class. I believe the problem is that i transmuted a boxed value, when it requires an unboxed value. I can't get an unboxed value, as i can't use the DecCalc type. I mean, i could, with reflection emit, but at that point i'd be writing CIL code directly.

Let's try something else. Traverse is actually useful after all, just not for the patching. We'll be using it to get the private fields from the private DecCalc class.

  • uint ulo
  • uint umid
  • uint uhi
  • uint uflags

These fields correspond to the four bytes taken by the Decimal(int[]) constructor.

class Patches
{
    static decimal AsDecimal(object d) {
        var lo = Traverse.Create(d).Field("ulo").GetValue<uint>();
        var mid = Traverse.Create(d).Field("umid").GetValue<uint>();
        var hi = Traverse.Create(d).Field("uhi").GetValue<uint>();
        var flags = Traverse.Create(d).Field("uflags").GetValue<uint>();
        return new decimal(new int[] { (int)lo, (int)mid, (int)hi, (int)flags });
    }
    public static void PatchDivision(ref bool __runOriginal, ref object d1, ref object d2)
    {
        var x = AsDecimal(d1);
        var y = AsDecimal(d2);
        Console.WriteLine($"Dividing {x} by {y}");
        if (x is 0 && y is 2)
        {
            Console.WriteLine("We gotta do something!");
        }
    }
}

I haven't written the "fix the return value" part here because i have yet to do this same process in reverse. And...

Hello World!
Dividing 0 by 2
We gotta do something!
0

Woo! We got the value out successfully. We copied its fields. Time to overwrite it. The other way around is less painful, because the implementation of Decimal already requires that conversion and conveniently leaves us an AsMutable method. Let's abuse it!

    public static void PatchDivision(ref bool __runOriginal, ref object d1, ref object d2)
    {
        var x = AsDecimal(d1);
        var y = AsDecimal(d2);

        Console.WriteLine($"Dividing {x} by {y}");
        if (x is 0 && y is 2)
        {
            var result = Traverse.Create<decimal>().Method("AsMutable", 0.5m).GetValue();
            Console.WriteLine($"Result: {result}");
            if (result is null) {
                Console.WriteLine("Result is null");
            }
        }
    }
Hello World!
Dividing 0 by 2
Result:
Result is null
0

Nope. That gives us null. We'd need to pass the ref value into the method, which only works if we have the correct type already.

Let's just do the process in reverse then.

    static void AsDecCalc(ref object decCalc, decimal d) {
        Traverse.Create(decCalc).Field("ulo").SetValue(Traverse.Create(d).Property("Low").GetValue<uint>());
        Traverse.Create(decCalc).Field("umid").SetValue(Traverse.Create(d).Property("Mid").GetValue<uint>());
        Traverse.Create(decCalc).Field("uhi").SetValue(Traverse.Create(d).Property("High").GetValue<uint>());
        Traverse.Create(decCalc).Field("uflags").SetValue((uint)Traverse.Create(d).Field("_flags").GetValue<int>());
    }

    public static void PatchDivision(ref bool __runOriginal, ref object d1, ref object d2)
    {
        var x = AsDecimal(d1);
        var y = AsDecimal(d2);

        Console.WriteLine($"Dividing {x} by {y}");
        if (x is 0 && y is 2)
        {
            AsDecCalc(ref d1, 0.5m);
            __runOriginal = false;
        }
    }

And it works!

Hello World!
Dividing 0 by 2
0,5

VarDecDiv is complex enough that it isn't inlined i guess. After removing the Traverse mention of DivideMyNumbers, the code still behaves as expected!

Parting words

I fixed math. Implementing the above for System.Half is left as an exercise to the reader.

(the final implementation is available in the attached files)

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.10.0" />
</ItemGroup>
</Project>
using HarmonyLib;
class Program
{
static void Main()
{
var harmony = new Harmony("fixmath");
harmony.Patch(
original: AccessTools.Method(AccessTools.Inner(typeof(decimal), "DecCalc"), "VarDecDiv"),
prefix: new HarmonyMethod(typeof(Patches), nameof(Patches.PatchDivision))
);
Console.WriteLine("Hello World!");
Console.WriteLine(DivideMyNumbers(0, 2));
}
public static decimal DivideMyNumbers(decimal x, decimal y) => x / y;
}
class Patches
{
static decimal AsDecimal(object d) {
var lo = Traverse.Create(d).Field("ulo").GetValue<uint>();
var mid = Traverse.Create(d).Field("umid").GetValue<uint>();
var hi = Traverse.Create(d).Field("uhi").GetValue<uint>();
var flags = Traverse.Create(d).Field("uflags").GetValue<uint>();
return new decimal(new int[] { (int)lo, (int)mid, (int)hi, (int)flags });
}
static void AsDecCalc(ref object decCalc, decimal d) {
Traverse.Create(decCalc).Field("ulo").SetValue(Traverse.Create(d).Property("Low").GetValue<uint>());
Traverse.Create(decCalc).Field("umid").SetValue(Traverse.Create(d).Property("Mid").GetValue<uint>());
Traverse.Create(decCalc).Field("uhi").SetValue(Traverse.Create(d).Property("High").GetValue<uint>());
Traverse.Create(decCalc).Field("uflags").SetValue((uint)Traverse.Create(d).Field("_flags").GetValue<int>());
}
public static void PatchDivision(ref bool __runOriginal, ref object d1, ref object d2)
{
var x = AsDecimal(d1);
var y = AsDecimal(d2);
if (x is 0 && y is 2)
{
AsDecCalc(ref d1, 0.5m);
__runOriginal = false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment