Skip to content

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:

  1. Replace the returned enumerator with our own, wrapping the original
  2. 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.