Add Harmony PostFix to Extension Method?

Started by SickBoyWi, May 22, 2019, 08:23:26 PM

Previous topic - Next topic

SickBoyWi

I want to add a postfix to an extension method. Here's what I coded up:

       
       [HarmonyPatch(typeof(WildManUtility), "IsWildMan")]
        static class Patch_WildManUtility_IsWildMan
        {
            static void Postfix(ref bool __result, Pawn p)
            {
                Log.Error("FACTION:" + ofPlayer.def.defName + ", PAWN KIND" + p.kindDef.defName);
                // Do some stuff.
            }
        }


It compiles fine, and the game accepts it without error on initialization.

The issue is: that code never gets hit, because my error log never comes out.

Turns out, that function I'm trying to PostFix (WildManUtility.IsWildMan), is an extension method to the Pawn class, defined like this:


                public static bool IsWildMan(this Pawn p)
{
return p.kindDef == PawnKindDefOf.WildMan;
}


That function is always referenced through the Pawn class, by calling pawnInstance.IsWildMan(pawn).

Is there a way to get something like this to work? Am I doing something dumb here somewhere that I'm missing?

Any advice is much appreciated.

LWM

So the question (in general) is: what class are you trying to patch?

The answer is, indeed, "WildManUtility."  It looks like it should be running?

Are you sure you're doing the Harmony PatchAll()?  It's an embarrassing mistake I've made before...

--LWM

Mehni

If you have exhausted all avenues of PEBKAC failure (missing PatchAll, typo in the method name, not having your harmony instantiated in the first place) then the most likely answer is this:

IsWildMan is a tiny method and most likely in-lined (read: optimised away) by the JIT compiler.

LWM

Is there any way to tell that sort of thing?  In this case, an assembly viewer clearly shows the function definition.

--LWM

SickBoyWi

#4
My patch is in a class that has several other patches in it, all of which work fine. If I rename the method I'm patching to something nonsense, I get an error during initialization as expected. The fact that the method shows up in my decompiled RW source code seems to indicate the method is there in the built version.

I appreciate the responses. I'll dig into it a little more this evening, and see if I can find the issue.

LWM

You can also use Harmony's bool Prepare() method to make sure it's getting incorporated.

Mehni

Quote from: LWM on May 23, 2019, 12:07:15 PM
Is there any way to tell that sort of thing?

No, not ahead of time. You don't know it until you test it. In-lining happens at run-time, as that is when the JIT (Just-In-Time) compiler does its work. Different configurations may even have different results regarding inlining.

Here's what erdelf dug up regarding it. Keep in mind this is not an exhaustive list:
Quote
regarding inlining, I gathered this general baseline at some point
Methods that are greater than 32 bytes of IL will not be inlined.
Virtual functions are not inlined.
Methods that have complex flow control will not be in-lined. Complex flow control is any flow control other than if/then/else; in this case, switch or while.
Methods that contain exception-handling blocks are not inlined, though methods that throw exceptions are still candidates for inlining.
Methods that have a larger struct as a formal argument, will not be inlined

Harmony 2.0 promises improvements regarding "don't mark this for in-lining" which looks good but that won't help you now.

From where I'm sitting, you've got the following options:
- Abandon mod.
- patch every method which uses the IsWildMan extension method to use IsWildManBySickBoyWi. You can use Harmony's MethodReplacer for that.
- Find some other way to achieve what you wish to achieve.

FWIW I don't think Harmony's Prepare() will help: the MethodInfo is there, it just never gets called. Read up on in-lining for a more in-depth technical analysis but the good enough explanation is this: To avoid overhead, method calls to short methods get replaced with the body of the method. That effectively means that instead of if (pawn.IsWildMan()) the JIT compiler optimises it to if (pawn.kindDef == PawnKindDefOf.WildMan) : the body of the method replaces the method call.

LWM

Quote from: Mehni on May 24, 2019, 03:58:11 AM
- patch every method which uses the IsWildMan extension method to use IsWildManBySickBoyWi. You can use Harmony's MethodReplacer for that.

Harmony's what now?

Is there documentation for that anywhere?

--LWM

SickBoyWi

So I've been messing around with this trying to figure out what's up, and haven't had much luck. Here's the current relevant parts of Harmony patch code I've written:

       
        [HarmonyPatch(typeof(WildManUtility), "IsWildMan")]
        static class Patch_WildManUtility_IsWildMan
        {
            static bool Prepare()
            {
                Log.Error("Prepare IsWildMan");
                return true;
            }

            static void Prefix(ref bool __result, Pawn p)
            {
                Log.Error("PREFIX IsWildMan: PAWN KIND" + p.kindDef.defName);

                // Do stuff.
            }

            static void Postfix(ref bool __result, Pawn p)
            {
                Log.Error("POSTFIX IsWildMan: PAWN KIND" + p.kindDef.defName);

                // Do stuff.
            }
        }

        [HarmonyPatch(typeof(WildManUtility), "AnimalOrWildMan")]
        static class Patch_WildManUtility_AnimalOrWildMan
        {
            static void Postfix(ref bool __result, Pawn p)
            {
                Log.Error("AnimalOrWildMan");
// THIS ONE ACTUALLY WORKS.

// Do stuff.
            }
        }

        [HarmonyPatch(typeof(WildManUtility), "NonHumanlikeOrWildMan")]
        static class Patch_WildManUtility_NonHumanlikeOrWildMan
        {
            static void Postfix(ref bool __result, Pawn p)
            {
                Log.Error("NonHumanlikeOrWildMan");
// THIS ONE ACTUALLY WORKS.

// Do stuff.
            }
        }


The patches for AnimalOrWildMan and NonHumanlikeOrWildMan both work, I see my logs from the postfix, no problem.

The issue is still IsWildMan. I see the log message from the Prepare function, that prints out during start up of the game. The prefix or postfix do absolutely nothing. I never see the logs from them. I don't even need the prefix, just the postfix; I added the prefix just to see if anything would actually print out.

Is Mehni's answer the correct one here? Perhaps this function gets optimized out?

LWM

Well, if it's clearly being patched (and the Prepare message says it is), then yeah, looks like you won't be able to manage that one via Harmony v1 :(

What were you trying to do?  Maybe there's another approach that would work?

--LWM