Hooking IEnumerators
IEnumerator methods are quite special. A simple IEnumerator method such as this:
public static class ContainingClass{ public static IEnumerator<string> GetMessages() { yield return "Hello from 1st MoveNext!"; yield return "Hello from 2nd MoveNext!"; }}
Will actually be turned into something like this by the C# compiler:
public static class ContainingClass{ public static IEnumerator<string> GetMessages() { return new _GetMessages_d__0(0); }
[CompilerGenerated] private sealed class _GetMessages_d__0 : IEnumerator<string>, IEnumerator, IDisposable { private int __1__state; private string __2__current;
string IEnumerator<string>.Current => __2__current; object IEnumerator.Current => __2__current;
public _GetMessages_d__0(int __1__state) { this.__1__state = __1__state; }
void IDisposable.Dispose() { }
private bool MoveNext() { switch (__1__state) { default: return false; case 0: __1__state = -1; __2__current = "Hello from 1st MoveNext!"; __1__state = 1; return true; case 1: __1__state = -1; __2__current = "Hello from 2nd MoveNext!"; __1__state = 2; return true; case 2: __1__state = -1; return false; } }
bool IEnumerator.MoveNext() => MoveNext();
void IEnumerator.Reset() => throw new NotSupportedException(); }}
The initial version was just syntactic sugar C# gives for creating methods that can be iterated, even if the implementation behind the scenes isn’t as simple.
Understanding IEnumerators
To understand all this, we need to know that
IEnumerator<T>
is an interface which supports a simple iteration over a generic collection. There also exists the
non-generic counterpart,
IEnumerator
.
While this might be new to you, you are actually making use of the IEnumerator
interfaces
when using foreach
loops. The foreach loop can take in anything that implements
IEnumerable
or
IEnumerable<T>
which are interfaces to get the IEnumerator
object of that type using the GetEnumerator()
method.
Now, let’s get to enumerating the GetMessages
method. We can’t use the foreach loop here
because our GetMessages()
method doesn’t return IEnumerable<string>
, but it instead it
directly returns IEnumerator<string>
. So we will enumerate it manually using the interface:
IEnumerator<string> enumerator = ContainingType.GetMessages();
while (enumerator.MoveNext()){ Console.WriteLine(enumerator.Current);}// Prints:// Hello from 1st MoveNext!// Hello from 2nd MoveNext!
Mistakes When Hooking
Based on the above, you hopefully understand why adding a PostfixDetour
to the
ContainingType.GetMessages()
won’t work as you’d first expect. That hook would run
as soon as the method is called since that method simply
returns the enumerable object.
So, how do we hook the end of the IEnumerator
object?
Hooking The IEnumerator
There are two main ways to hook an enumerator:
- Replace the returned enumerator with our own, wrapping the original
- Hook MoveNext directly
The first option would be more straightforward due to compiler generated enumerator classes being unspeakable in C#. However, it has a major issue: inlining. This is why MonoDetour goes with option 2 as the built-in solution.
Let’s go through both solutions anyways:
1. Wrap The Enumerator
Because of how small methods that construct and return an enumerator class usually are, the JIT compiler uses an optimization where it essentially copies the small method to a calling method. This happens if the a method which calls the enumerator getter method gets compiled before you hook the enumerator getter method.
The issue here is that if the target method is inlined, hooks applied to it won’t run where the target method has been inlined.
To combat the issue, you could ILHook the method that got the method call inlined to recompile it after you’ve hooked the enumerator getter method. It’s also worth nothing that MonoMod disables inlining for hooked methods, although that won’t help when the inlining happens before hooking.
Alternatively you could attach an [MethodImpl(MethodImplOptions.NoInlining)]
attribute to the target method before the target assembly is loaded by the runtime. You could do this for example in a BepInEx preloader patcher using Mono.Cecil.
Solution: Wrap The Enumerator
The ContainingType.GetMessages()
method returns the
enumerable object. We can actually hook the end of that method and take the
IEnumerator
object the method would have returned, and instead we make the
method return our own IEnumerator
!
What this means is essentially this:
public static class ContainingClass{ public static IEnumerator<string> GetMessages() { IEnumerator<string> originalEnumerator = new _GetMessages_d__0(0); // Insert our postfix here: IEnumerator<string> ourEnumerator = MyHooks.Wrapper_GetMessages(originalEnumerator); return ourEnumerator; } // ...}
// The following would be in your code:static class MyHooks{ internal static void InitHooks() { // This hook inserts a call to our wrapper method like in the above demonstration. On.Namespace.ContainingClass.GetMessages.Postfix(Wrapper_GetMessages); }
// This postfix hook takes the original enumerator and // returns its own enumerator object which wraps the original. static void Postfix_GetMessages(ref IEnumerator<string> returnValue) { returnValue = Wrapper_GetMessages(returnValue); }
static IEnumerator<string> Wrapper_GetMessages(IEnumerator<string> original) { while (original.MoveNext()) { yield return original.Current; } }}
And that’s it! Remember that our IEnumerator
method also becomes a state machine,
and on MoveNext()
it will execute the following:
private bool MoveNext(){ switch (__1__state) { // Normally this would be for tracking the state, // but our IEnumerator method only has a single while loop. default: return false; case 0: __1__state = -1; break; case 1: __1__state = -1; break; }
// Call the original enumerator's MoveNext method if (enumerator.MoveNext()) { // Set state and return with the result. __2__current = enumerator.Current; __1__state = 1; return true; }
// If the original enumerator's MoveNext method // returned false, we also return false. return false;}
2. Hook MoveNext Directly
In this solution, we find the enumerator object’s MoveNext
method and hook that. This is unlikely to suffer from inlining, but as mentioned above, the class is unspeakable. In reality, the example class above is named <GetMessages>d__0
, not _GetMessages_d__0
. This is not valid C# however. So the best we can do is reference the instance as an interface it implements, for example IEnumerator<string>
.
The obvious issue here is that we have no easy way of accessing the enumerator class’ fields. We can try working around that issue with Reflection though. And in fact, MonoDetour does its best to make this experience rather seamless.
Solution: Hook MoveNext Directly
The PrefixMoveNext
and PostfixMoveNext
methods simply apply the hook to the MoveNext method of the enumerator. The only magic MonoDetour does here is the SpeakableEnumerator<string> self
argument which wraps the IEnumerator<string>
representation of the enumerator instance in this case. This allows accessing the normally-unspeakable fields of the type.
Additionally, the ref bool continueEnumeration
parameter on the postfix is just returnValue
but named to be more appropriate for the MoveNext method.
On.Namespace.ContainingClass.GetMessages.PrefixMoveNext(Prefix_GetMessages_MoveNext);On.Namespace.ContainingClass.GetMessages.PostfixMoveNext(Postfix_GetMessages_MoveNext);// ...static void Prefix_GetMessages_MoveNext(SpeakableEnumerator<string> self){ // Return if state is not 0, aka // the enumeration had already stated. if (self.State != 0) { return; }
// Code here runs only at the beginning of enumeration...}
static void Postfix_GetMessages_MoveNext( SpeakableEnumerator<string> self, ref bool continueEnumeration){ // Return if MoveNext returned true, // aka enumeration will continue if (continueEnumeration) { return; }
// MoveNext returned false, we are // now at the end of the enumeration!
// Code here runs only at the end of enumeration...
}
Conclusion
Enumerator methods can be hooked multiple ways, but hooking the MoveNext method is recommended by MonoDetour.
- Prefix on the MoveNext method:
On.Namespace.ContainingClass.GetMessages.PrefixMoveNext(Prefix_GetMessages_MoveNext);// ...static void Prefix_GetMessages_MoveNext(SpeakableEnumerator<string> self){ // This runs at the beginning of every MoveNext call}
See Solution: Hook MoveNext Directly for only running once at the start of the whole enumeration.
- Postfix on the MoveNext method:
On.Namespace.ContainingClass.GetMessages.PostfixMoveNext(Postfix_GetMessages_MoveNext);// ---static void Postfix_GetMessages_MoveNext( SpeakableEnumerator<string> self, ref bool continueEnumeration){ // This runs at the end of every MoveNext call}
See Solution: Hook MoveNext Directly for only running once at the end of the whole enumeration.
- ILHook the MoveNext method:
On.Namespace.ContainingClass.GetMessages.ILHookMoveNext(ILHook_GetMessages_MoveNext);// ...static void ILHook_GetMessages_MoveNext(ILManipulationInfo info){ // Manipulate the MoveNext method here, // for example change the strings that are returned.}
For manipulating methods with ILHooks, see Introduction to ILHooking.