Nigger-level reading comprehension. I was wondering why there exists a lobby for rust while there is no ML, HS, CL, C3, Zig etc. lobby, or at least if there are, why are they significantly less successful.
Micro$oft overall has been completely retarded with their versions of C. Their latest troyan horse, C#, pisses me off in ways that I find hard to articule as it's tied to VSCode, a fucking IDE that is shafted in other platforms.
According to this table, that will be a problem for anyone using Windows. There's no C11 table, but considering how long it took for MSVC to support C99 (VS 2013?), I wouldn't be surprised if they haven't even got C11 done yet.
The latest MSVC supports "legacy", C11, and C17, which isn't even on that table.
I wonder how many people on Windows are actually using straight C and not just "run everything C/C++-like through the C++ compiler, whatever". For the longest time MSVC's C was in a sort of no-man's-land where it didn't support all the old pre-K&R lolcow stuff, but also had dicey support for anything newer than C89.
I was wondering why there exists a lobby for rust while there is no ML, HS, CL, C3, Zig etc. lobby, or at least if there are, why are they significantly less successful.
The last big government push for language adoption I can think of off the top of my head was Ada, a reasonably well-designed, well-intentioned programming language that nevertheless crashed Ariane 5. A memorable quote about the incident that stuck to me is "reliable software can reliably fail."
I like this conclusion from the above linked Medium post:
Culture matters and can sometimes eat strategy for breakfast as well as your technology project (R14). Besides a design bias to mitigating failure through shutdown and backup failover, there were QA shortcuts taken, aggressive borrowing of A4 code, and there was no single point of accountability on the A5 team. The ESA held no one responsible for the failure — a classic tragedy of the commons.
You can take this many steps further if you want. Community culture matters, but besides that software-enforced safety itself breeds complacency and hinders understanding, abstraction inherently does. Do you understand what your greedily centralized but novel and non-standardized infrastructure is doing for you? This is not a criticism targeted at Rust alone, but very much at the ML crowd: are you really sure your type system is consistent? Do you think that even matters? Culture permeates down to the very logic of your systems
I'm not sure if this is the right place to put this, but I've been digging into Harmony and C# a lot in the past few months, fixing 3rd party game mods. There is a lack of good examples on the internet, so I will put up a tutorial here so what I learned won't disappear forever and in case anybody needs to fix 3rd party C# game code or start modding Unity games (Rimworld, Terra Invicta, Rogue Trader, Kerbal Space Program etc), though any .NET CIL based applications will work so long they could load custom built assemblies. It should also work with Godot in theory. Assemblies are just .NET Common Intermediate Language bytecode stored in Windows PE .DLL files, like Java bytecode in JAR zip files.
What so special about Harmony? It's used to patch up .NET code in memory non-destructively so changes can be stacked up in most cases, just add more mods until the game crashes. You also don't need access to the original source code. It's how entirely new mechanics can be added to Unity games. Of course, you can also use it to fix up bad mod interactions or even fix game bugs yourself, or make submods for mods. Harmony should work on any .NET CIL applications, not necessarily in C# since the underlying runtime doesn't care how the CIL generated. You can patch it manually yourself if you want since all the functionality is in the .NET specification, but Harmony makes it very convenient.
Feel free to ask questions since I'm probably not aware of problems anyone not experienced with Harmony/C#.
Harmony patch types are broadly divided into Prefix, Postfix, Finallizer and Transpiler. Patches are always done in memory at runtime without modifying any files.
.NET SDK (Yes, it works on Linux too in the command line)
For Windows, you can install it as part of Visual Studio Community Edition too, so long as you check the .NET SDK targeting.
On Linux, you should also install mono if you want to use .NET Framework, which Unity uses. You can also use the "msc" command from mono in place of "csc" for the C# compiler if you must use mono.
Decompilers
dnSpyEx for Windows. Windows Presentation Foundation has yet to be ported to Wine/mono.
ilspycmd for Linux/MacOSX (You can build it yourself or download it through the dotnet tool from the .NET SDK)
Avalonia-ILSpy cross platform with GUI written in C# .NET 8.0, like dnSpyEx but a little older, but will work fine for our example.
Harmony 2
Use the dotnet tool to install it (dotnet add package Lib.Harmony)
Or get it from github directly, use the fat download and the same .NET version you're planning to use.
IDE (Optional)
I'm not going to instruct you on setting up your favorite editor, the tutorial is mostly focused on the older .NET Framework.
You can also use the command line dotnet to setup projects for .NET 5 and later (eg. dotnet new console -o HarmonyExample, dotnet publish).
Visual Studio is also fine if you installed the .NET SDK through it.
I've been using Notepad++ on Windows and Nano on Linux.
The game/application you want to patch up (You'll need to use the right version of .NET for whatever application you're ultimately patching)
Unity currently uses .NET Framework 4.7.2, 4.8 syntactic sugar should be fine, which what our tutorial will be concentrating on.
Most games have the game logic in Assembly-CSharp.dll.
Or write your own application as a test to patch against, which we will use in the tutorial.
We'll use the decompilers to inspect .NET assemblies (DLLs). On Windows, just drag and drop the game assembly or application DLL into dnSpyEx. For Linux, just point ilspycmd to the assembly and an output directory to dump the decompiled code.
C#:
namespace ThirdParty {
public class ThirdPartyCode {
private int c;
public ThirdPartyCode(int constant = 5){
c = constant;
}
public int Multiply(int v){
return v * c;
}
}
}
You should get a ThirdParty.dll file, this will be our example to patch up.
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ThirdParty;
namespace Example {
class Start {
static void Main(string[] args){
var t = new ThirdPartyCode();
Console.WriteLine($"{t.Multiply(3)}");
}
}
}
There are many ways to compile C# programs, you can use Visual Studio or dotnet to create projects, or in our case, targeting the .NET Framework for Unity, calling csc directly for our sample application.
This generates an executable which you can run directly on Windows Command Prompt or via mono on Linux.
Code:
mono Example.exe
It should print "15"
You can use dnSpyEx or ilspycmd to inspect Example.exe and ThirdParty.dll.
As you can see in our earlier example, we are using Main to start our program. For Games, it depends on how your assembly is loaded. For Rimworld, there is the StaticConstructorOnStartup class attribute or inheriting from the Mod class. Check your game or application documentation. As long as your entrypoint is started, you can continue on to load Harmony to patch up the main program.
Prefix and Postfix patching is the easiest to get into, it just inserts new code to be run before and after a patched method.
It's always recommended to use Postfix over Prefix since there may be other modifications to the patched method that could introduce side effects added by other patches.
Before starting, look at the disassembled code, find out what do you want to change. You can't continue if you can't imagine what you want to change it.
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using ThirdParty;
using HarmonyLib;
namespace Example {
class Start {
static void PatchUp() {
var h = new Harmony("Example.Start.Patch");
h.PatchAll();
}
static void Main(string[] args){
PatchUp();
var t = new ThirdPartyCode();
Console.WriteLine($"{t.Multiply(3)}");
}
}
[HarmonyPatch]
class Patcher {
static void Prefix(){
Console.WriteLine("Early Patch!");
}
static void Postfix(){
Console.WriteLine("Late Patch!");
}
static IEnumerable<MethodBase> TargetMethods(){
yield return AccessTools.Method("ThirdParty.ThirdPartyCode:Multiply");
}
}
}
Runs the Example.exe as usual. You will see the Prefix and Postfix method being called.
The Harmony ID that you set in
C#:
static void PatchUp() {
var h = new Harmony("Example.Start.Patch");
h.PatchAll();
}
can be set to anything, so long as it is unique. PatchAll will search for all patches in the Assembly with the HarmonyPatch class attribute. Older examples on the internet manually reflect and call unity to patch methods one at time, but the example above is much more organized and concise. Patches are applied in the order defined in the patch class.
You could just get away with using TargetMethod since we're patching a single function, but this is pretty much my go to template for fixing Rimworld problems, like null dereference errors. It expects references to methods to patch.
You can give it a string, but it must include the full namespace, class and method name. This is useful for games, especially if you are patching up other mods that may be rebuilt/updated at any time, Harmony will still be able to search for it, or allow you to handle it if the assembly is gone.
This is more type safe, but runs the risk of requiring your code needing to be recompiled every time there is an update to the original code, causing CIL level errors. This also introduce a hard reference to the Assembly that contains the class/method that will break if it was renamed.
C#:
yield return AccessTools.Method("ThirdParty.ThirdPartyCode:Multiply", new Type[] {typeof(int)});
You can also supply an optional Type[] argument if there are multiple methods that share the same name. Method() must only resolve to one method at a time or it will throw an exception. You can also use Traverse reflection to walk and search through all a class if you know what you're doing. AccessTools does not care if a method or field is private or static.
Sometimes, we want to add checks or even change values to solve problems or change game behavior.
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using ThirdParty;
using HarmonyLib;
namespace Example {
class Start {
static void PatchUp() {
var h = new Harmony("Example.Start.Patch");
h.PatchAll();
}
static void Main(string[] args){
PatchUp();
var t = new ThirdPartyCode();
Console.WriteLine($"3: {t.Multiply(3)}");
Console.WriteLine($"30: {t.Multiply(30)}");
Console.WriteLine($"-5: {t.Multiply(-5)}");
}
}
[HarmonyPatch]
class Patcher {
static void Prefix(int ___c){
Console.WriteLine($"The internal value c is {___c}");
}
static bool Prefix(int v, ref int __result){
Console.WriteLine($"if {v} < 0, sets results to 0 and bypass the original function.");
bool ret = true;
if(v < 0) {
__result = 0;
ret = false;
} else {
Console.WriteLine("Not skipping the original code");
}
return ret;
}
static void Postfix(ref int __result){
Console.WriteLine("Setting maximum value to 100");
if(__result > 100)
__result = 100;
}
static void Postfix(int __result, int ___c, int v){
Console.WriteLine($"{___c} * {v} = {__result}");
}
static IEnumerable<MethodBase> TargetMethods(){
yield return AccessTools.Method("ThirdParty.ThirdPartyCode:Multiply");
}
}
}
Code:
The internal value c is 5
if 3 < 0, sets results to 0 and bypass the original function.
Not skipping the original code
Setting maximum value to 100
5 * 3 = 15
3: 15
The internal value c is 5
if 30 < 0, sets results to 0 and bypass the original function.
Not skipping the original code
Setting maximum value to 100
5 * 30 = 100
30: 100
The internal value c is 5
if -5 < 0, sets results to 0 and bypass the original function.
Setting maximum value to 100
5 * -5 = 0
-5: 0
You can access values in the patched method by matching the variable name (refer to the disassembly for the class you want to patch):
Method argument: just match the name.
Internal/private class field: prefix the name with 3 underscores.
Access returned value: Use __result.
You can use the ref keyword to allow modifications to any variable.
There is also __state variable that can be of any type that can be used to pass state between Prefix and Postfix like the stopwatch performance monitor in the example.
There is also __instance variable that you can use to access the "this" object in a patched method. Obviously this will only apply for non-static methods and must match the class type of the method you are patching. You can use it to access any public methods and fields through it, or use reflection to access private ones.
All variables must match their types in the patched method/class.
For prefixes, you can skip the original code by using the bool return type in the prefix and then returning false.
I will put tutorials on finalizers and transpilers later, there is enough to go on for an entire new post.
Edit: Put output of 2nd Example.
Probably true, but I think the (small) reduction of friction will be nice for a real beginner. Things such as "True" "False" not existing (only as macros) in C11 may be confusing or unpleasant.
Does it really matter that true, false, and bool are macros and not keywords? They are still typed just the same. The compiler will even mention explicitly If the header has not been included. I think it might be far more confusing to a beginner that some functions (but not all of them) return 0 ("false") upon successful execution.
I found this page, apparently C99 support came in VS 2015, and most of C11 in VS 2019 (they're getting faster) no mention of C17 or C23, or "C compiler features". As I understand, "compile everything as C++" is how it goes. The support for C is limited to a standard library implementation, not an actual compiler. (Can anyone who still uses Windows confirm this?)
Example/reference: _Complex.
MSVC doesn't support the _Complex keyword or native complex types. The Universal CRT <complex.h> uses implementation-specific macros to achieve the same effect.
The Microsoft C Runtime library (CRT) provides complex math library functions, including all of the ones required by ISO C99. The compiler doesn't directly support a complex or _Complex keyword, therefore the Microsoft implementation uses structure types to represent complex numbers.
Nigger-level reading comprehension. I was wondering why there exists a lobby for rust while there is no ML, HS, CL, C3, Zig etc. lobby, or at least if there are, why are they significantly less successful.
Probably because borrow checker is actually something that truly differentiate it from other languages. The rest are just GC languages, or languages that promise their way of handling memory manually is better. Rust actually has way to enforce deterministic memory management. It also has C-style syntax which makes it easier for average programmer to pick up, over something like ML.
Also, many say that C++ with RAII can do the same. Where as that is true, the issue is when you work with 100s people on same project, C++ does not give you guarantees about it. But Rust does. And that's massive, because C++ is such bloated language everyone has their ideas about ways thing should be, and code review can only do so much.
Rust enforcing coding style is good for enterprise. It makes programmers much more replaceable.
Probably a lame question to ask in this thread but is learning to code as a lateral career move (edit: still) worth it?
I’m in early stage of a (real) engineering career and I’ve been learning C++ to fulfil a childhood aspiration. It seems like something I wouldn’t hate working in, so wondering in idle curiosity if it’s (financially) worth the hassle of trying to switch considering I don’t have a relevant degree or experience and the job market seems to have changed with all the Indians + AI.
Probably true, but I think the (small) reduction of friction will be nice for a real beginner. Things such as "True" "False" not existing (only as macros) in C11 may be confusing or unpleasant.
if you can't understand c's incredibly simple "everything is a number and any other number than 0 is true" philosophy you should try a language more suited for your skill level like javascript
The last big government push for language adoption I can think of off the top of my head was Ada, a reasonably well-designed, well-intentioned programming language that nevertheless crashed Ariane 5. A memorable quote about the incident that stuck to me is "reliable software can reliably fail."
I like this conclusion from the above linked Medium post:
You can take this many steps further if you want. Community culture matters, but besides that software-enforced safety itself breeds complacency and hinders understanding, abstraction inherently does. Do you understand what your greedily centralized but novel and non-standardized infrastructure is doing for you? This is not a criticism targeted at Rust alone, but very much at the ML crowd: are you really sure your type system is consistent? Do you think that even matters? Culture permeates down to the very logic of your systems
Also, many say that C++ with RAII can do the same. Where as that is true, the issue is when you work with 100s people on same project, C++ does not give you guarantees about it. But Rust does. And that's massive, because C++ is such bloated language everyone has their ideas about ways thing should be, and code review can only do so much.
if you have literally any sort of standards for development c++ won't have any real problems
and in rust you can still shoot yourself in the foot with unsafe {} but more importantly one of your 593 cargo dependencies can do unsafe {} and it can fuck everything up
Previous Post on Prefix and Postfix patching.
Now we move on to Harmony Finalizers in C#. Finalizers are like Postfix patches, but are always run regardless and have a chance to intercept any Exceptions. Finalizers can support all the arguments used in Postfix patches, though you should really use Postfix patches to modify state rather than with a Finalizer if possible since Finalizers can skip a lot of side effects and statements in the original code if an exception was thrown in an unexpected location, leaving you in an inconsistent state. It is best used as a debugging aid to observe class state and monitor for unhandled exceptions.
Consider the following C# Code:
C#:
using System;
namespace ThirdParty2 {
public class Area {
private double len;
public Area(double length) {
if(length <= 0) throw new ArgumentOutOfRangeException("length must be a positive value");
len = length;
}
public double Square(){ return len * len; }
public double Circle(){ return Math.PI * len * len; }
}
}
The constructor may throw an exception, but for whatever reason, some code calling it is giving it an invalid value. Maybe we don't care if the value is invalid so long as our game doesn't crash. We can use a Finalizer to intercept and suppress the exception, or even remap the exception to something else.
C#:
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Reflection;
using ThirdParty2;
using HarmonyLib;
namespace Example {
class Start {
static void PatchUp() {
var h = new Harmony("Example.Start.Patch");
h.PatchAll();
}
static void Main(string[] args){
PatchUp();
var a = new Area(2.0);
Console.WriteLine($"square: {a.Square()}");
a = new Area(0.0);
Console.WriteLine($"circle: {a.Circle()}");
a = new Area(5.0);
Console.WriteLine($"circle: {a.Circle()}");
}
}
}
Calling Area constructor with 0 will surely cause an exception to be thrown, and will halt the program, but we can handle that as needed with a Finalizer.
C#:
[HarmonyPatch]
class Patcher {
[return: MaybeNull] // suppress warning about deliberate null return
static Exception Finalizer(Exception __exception, double length, ref double ___len){
// finalizers can observe and change values
// argument also must be called __exception
Console.WriteLine($"Observed: length = {length}");
if(__exception != null) {
Console.WriteLine($"Intercepted and suppressed an exception!");
___len = 1.0; // a sane value
}
return null;
}
static IEnumerable<MethodBase> TargetMethods(){
yield return AccessTools.TypeByName("ThirdParty2.Area").Constructor(new Type[] {typeof(double)});
}
}
In the first patch class, we targeted the constructor method to suppress the exception and used Finalizer arguments to observe arguments passed to it.
When an exception is thrown, we suppress it by returning a null in the Finalizer after setting the internal state to some hopefully sane value.
You can also use it to throw another different exception by creating a new exception and then returning that (not by throwing it again).
The code will also run even if no exception is thrown, denoted by __exception being null.
You can't use ".ctor" as a name for the constructor, hence the more awkward convention to select it for patching. The Type[] part denotes the ordered list of arguments types accepted by the constructor method, you must also include any optional arguments, since those are just syntactic sugar. Constructors do NOT have __result return values.
C#:
[HarmonyPatch]
static class Cube {
// awkward attempt to make a cube, there are better ways to do this with a transpiler
// or a simple PostFix
static IEnumerable<MethodBase> TargetMethods(){
yield return AccessTools.Method("ThirdParty2.Area:Square");
}
static void Finalizer(ref double __result, double ___len){
// change the return result, a postfix could have done this
// but in case you want to act on it depending if an exceptions was thrown
__result *= ___len;
Console.WriteLine($"Set __result = {__result}");
}
}
This Finalizer modifies the return value of the patched function. You could also choose to modify the result to some value only if an exception is thrown, using it to bypass buggy game code.
Code:
Observed: length = 2
Set __result = 8
square: 8
Observed: length = 0
Intercepted and suppressed an exception!
circle: 3.141592653589793
Observed: length = 5
circle: 78.53981633974483
Next, we'll finally talk about CIL and transpilers.
Because there's no stable ABI for Rust. The distributions would have to recompile every crate for every version of Rust. And given that the culture of Rust is "just use nightly, bro", this isn't brilliant.
Micro$oft overall has been completely retarded with their versions of C. Their latest troyan horse, C#, pisses me off in ways that I find hard to articule as it's tied to VSCode, a fucking IDE that is shafted in other platforms.
I'm guessing MS has given up on C (which, in a world where C++ exists, isn't unreasonable). There's not a great deal of point in adding new features to C, because people like C precisely because it doesn't change.
In practice people are reluctant to use C features that aren't in C++, like _Generic. And if you're doing that, you might as well compile your C code as C++ anyway.
i cannot fucking comprehend why a language supposedly built to be a more secure c++ has such shit integration with the greater compiled language ecosystem
nice job trying to make something nice then taking a huge shit all over it because 2016 nigger package manager brainrot
as a language designer it is your priority to allow the system package manager to handle everything
if possible just assume the system package manager is always handling everything, because it's simpler to do that and any windows users complaining don't matter because windows users don't really deserve any nice things
when c gets a new feature it's generally a special semantics-tweaking keyword or some really nice convenience syntax extension or something that compilers have pretty much universally supported for 25 years
and that's the way mature languages should be designed
Part of it, I'm sure, is that the C++ dependencies are baked right into Windows - look how many things are COM interfaces. You could do COM in C if you're a total schizo, but I'm pretty sure anyone doing that is one step away from going full Terry and writing HolyCOM.
then taking a huge shit all over it because 2016 nigger package manager brainrot
as a language designer it is your priority to allow the system package manager to handle everything
if possible just assume the system package manager is always handling everything, because it's simpler to do that and any windows users complaining don't matter because windows users don't really deserve any nice things
Cannot agree more, all the programing language package managers are utter dogshit that either do not have even the most basic degree of reproducibility(try running any python shit 6 months after it was written) or they pin packages which is even more retarded as in the best scenario you download shit ton of redundant garbage(11GB downloads in dependencies is criminal and anyone that designed such garbage deserves death penalty) and in the worst create dependency hell once you try to run 2 large projects.
Not to mention the fact that all of them allow developers to update their packages without any security(anyone still remembers te ukrofag obsessed commie that decided to put malware into his NPM garbage?) or correctness checks which inturn creates subhuman culture in which devs release 2137 versions monthly each fixing previous version issues and introducing new ones instead of fixing the code correctly in one big patch.
The only technical advantage they offered were virtual environments however Guix provides superior version of that as such i no longer see any technical advantages to them.
The only way to do audio in Windows in C other than the venerable old waveIn / waveOut APIs is to do COM in C. WinQuake did COM in C for this reason. The API is actually designed for it and it isn't nearly as bad as you'd think.
Cannot agree more, all the programing language package managers are utter dogshit that either do not have even the most basic degree of reproducibility(try running any python shit 6 months after it was written) or they pin packages which is even more retarded as in the best scenario you download shit ton of redundant garbage(11GB downloads in dependencies is criminal and anyone that designed such garbage deserves death penalty) and in the worst create dependency hell once you try to run 2 large projects.
Not to mention the fact that all of them allow developers to update their packages without any security(anyone still remembers te ukrofag obsessed commie that decided to put malware into his NPM garbage?) or correctness checks which inturn creates subhuman culture in which devs release 2137 versions monthly each fixing previous version issues and introducing new ones instead of fixing the code correctly in one big patch.
The only technical advantage they offered were virtual environments however Guix provides superior version of that as such i no longer see any technical advantages to them.
all we really need is a system-level package manager that does its job really well and is completely language-agnostic guix is just the language-specific package manager for guile scheme if you think about it so it doesn't really count
The latest MSVC supports "legacy", C11, and C17, which isn't even on that table.
I wonder how many people on Windows are actually using straight C and not just "run everything C/C++-like through the C++ compiler, whatever". For the longest time MSVC's C was in a sort of no-man's-land where it didn't support all the old pre-K&R lolcow stuff, but also had dicey support for anything newer than C89.
Anecdotally, I unironically use msvc C11 for my legacy windows filesystem driver, tis the official language to do drivers in.
However, due to crunch and in the interest of unit tests and reducing code duplication, you are able to effectively hack c++ into kernel mode, bar STL (because exceptions).
You'll need to globally override new and delete operators with the kernel allocator and also perform some linkage shenanigans to get globally namespaced object ctors working.
Because there's no stable ABI for Rust. The distributions would have to recompile every crate for every version of Rust. And given that the culture of Rust is "just use nightly, bro", this isn't brilliant.
I'm guessing MS has given up on C (which, in a world where C++ exists, isn't unreasonable). There's not a great deal of point in adding new features to C, because people like C precisely because it doesn't change.
In practice people are reluctant to use C features that aren't in C++, like _Generic. And if you're doing that, you might as well compile your C code as C++ anyway.
Softgen is surprisingly good at building frontends for crud apps. Specifically things that can filter or sort normal data. $25 for frontend isn't bad.
That being said you still need to know if the thing is hallucinating or doing retarded shit like sorting in panel as opposed to letting the backend do it.
Introduction, Prefix and Postfix Previous Post on Harmony Finalizer
I'd argue the transpiler is the most powerful and important part of Harmony for making 3rd party code that you don't have access to behave under your control. Once you get it, you'll be editing the bytecode instruction streams directly in memory without needing to use a hex editor on the assembly on file. Unlike Prefix, Postfix and Finalizer which will put your injected code to run before or after a patched method, transpilers allow you to inject code precisely anywhere within the patched method, it depends on how you search and emit (or delete) the instructions.
C# documentation on OpCodes. Check out the fields if you are unsure what each opcode is doing. If you are familiar with assembly programming ye old days, it should come natural to you, its an extremely simplified version of machine code. The usual rule about the stack required to be balance still applies, but there can only be a single Ret instruction to exit a method, you'll have to rely on branch, jump and leave instructions for control flow. It is also possible to handle exceptions at the IL level, but I'll get to that in the future.
Consider the earlier example:
C#:
using System;
using System.Collections.Generic;
using System.Linq;
namespace ThirdParty2 {
public class Area {
private double len;
public Area(double length) {
if(length <= 0) throw new ArgumentOutOfRangeException("length must be a positive value");
len = length;
}
public double Square(){ return len * len; }
public double Circle(){ return Math.PI * len * len; }
}
}
With ilpsycmd, let's look at the Circle method part:
Code:
// IL code: ThirdParty2
.class private auto ansi '<Module>'
{
} // end of class <Module>
.class public auto ansi beforefieldinit ThirdParty2.Area
extends [System.Runtime]System.Object
{
// Fields
.field private float64 len
...
// Methods
.method public hidebysig
instance float64 Circle () cil managed
{
// Method begins at RVA 0x2085
// Header size: 1
// Code size: 24 (0x18)
.maxstack 8
IL_0000: ldc.r8 3.141592653589793
IL_0009: ldarg.0
IL_000a: ldfld float64 ThirdParty2.Area::len
IL_000f: mul
IL_0010: ldarg.0
IL_0011: ldfld float64 ThirdParty2.Area::len
IL_0016: mul
IL_0017: ret
} // end of method Area::Circle
} // end of class ThirdParty2.Area
It says:
Load the constant of Pi
Load argument 0 (The hidden "this" object pointer)
Load field "float64 ThirdParty2.Area::len"
Multiply result (Pop from the last 2 entries in the stack and push result back into the stack.)
Load argument 0
Load field "float64 ThirdParty2.Area::len"
Multiply result
Return from method.
The IL_number part are the byte offsets disassembled by ilspycmd. By convention, the return value of a method is returned as the top entry of the stack. That's all to it, its not that hard.
Now let's look at a more complicated example to modify. The following code computes the factorial of an integer. We want to change it to a hyperfactorial, without needing to rewrite or copy all the existing logic.
C#:
using System;
namespace ThirdParty2 {
public static class Utils {
public static double Factorial(int fact){
double ret = 1;
foreach(int i in Enumerable.Range(1, fact)) { ret *= i; }
return ret;
}
}
}
Let's start by examining the metadata of the method, it takes a single int32 value called "fact", and there are 3 local unnamed variables (of types float64, IEnumerator<int32> and int32) in the static method (no this pointer).
Let's look at the loop starting at IL_0017, it jumps straight to IL_0025 to initialize the loop variables. It gets an integer from get_Current() and moves onto the next with MoveNext(). Within that, it stores the integer in local variable 2 (local variable 0 is our accumulator) and then later loads it back before converting it to a double before passing it to the multiply instruction.
In a hyperfactorial, we have to compute the powers of each iteration by itself before multiplying it with the rest of the product. So that means manipulating the value before the multiplication takes place. Luckily the conversion to double at IL_0022 with conv.r8 is unique enough that we can lock onto it with our transpiler. What we can do is to take the value right after it is converted and then pass it to Math.Pow.
C#:
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using ThirdParty2;
using HarmonyLib;
namespace Example {
class Start {
static void PatchUp() {
var h = new Harmony("Example.Start.Patch");
h.PatchAll();
}
static void Main(string[] args){
Console.WriteLine($"Factorial 5: {ThirdParty2.Utils.Factorial(5)}");
PatchUp();
Console.WriteLine($"Hyperfactorial 5: {ThirdParty2.Utils.Factorial(5)}");
}
}
[HarmonyPatch]
static class Hyperfactoria {
static IEnumerable<MethodBase> TargetMethods(){
yield return AccessTools.Method("ThirdParty2.Utils:Factorial");
}
static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions, ILGenerator generator){
MethodBase powers = AccessTools.Method(typeof(Math), nameof(Math.Pow));
foreach(var insn in instructions){
yield return insn;
if (insn.opcode == OpCodes.Conv_R8) {
yield return new CodeInstruction(OpCodes.Dup);
yield return new CodeInstruction(OpCodes.Call, powers);
}
}
}
}
}
Take note that Math.Pow takes 2 double arguments. We already conveniently have the value to give it on the stack right after the integer was converted to a double float. All we need to do is to duplicate the value on the stack for Math.Pow and then call it. Math.Pow would consume 2 stack entries and push the results back onto the stack, exactly we want it for the multiply instruction to consume, which the original code will then continue on as normal.
generator is unused for now and can be ignored, but it can be used to generate new metadata for jump/branch labels and local variables within the transpiler.
Code:
Factorial 5: 120
Hyperfactorial 5: 86400000
We can move onto more advanced transpilers corner cases next.