Why does rewriting a compiled unit's own method body reach calls that inserting a stand-in in front of it cannot?
answer
- reach follows the reference
- who hands out the object
- a stand-in must be routed to
- rewriting edits the body itself
- no second object to bypass
basics
~20 sRewriting edits the method body itself, so the added code runs for every call that reaches that body, whatever reference the caller holds. A stand-in only sees calls routed through it, so anything holding the original object bypasses it entirely.
solid answer
~40 sA stand-in works only when the caller obtains the stand-in rather than the target, and only for members it is allowed to intercept. A dependency that builds its own collaborators never consults your wiring, so those instances are the originals and your wrapper is not in the path. Rewriting attacks the other end: it reads the already-compiled definition of the type, splices instructions into the method body, and hands the runtime a unit that *is* the type. There is no second object and no extra reference to route, so the inserted code runs for every caller, including instances the dependency created for itself. The price is a toolchain that must match the target's compiled shape, and a running artifact that no longer matches the source anyone reviewed.
code
pseudocode · 11 lines// the compiled method, before the rewrite
function fetch(id)
return store.lookup(id)
// the same unit after its body is rewritten
function fetch(id)
started = clock()
try
return store.lookup(id)
finally
record("fetch", clock() - started)go deeper
Remember the one-line difference: a wrapper is a second object callers must be given, while rewriting changes the original method's own body.
Be able to explain why routing matters — the wrapper is only in the path when whoever created the instance handed out the wrapper instead of the target.
Name the concrete case that forces the rewrite: a collaborator the dependency constructs internally, where no wiring of yours is ever consulted, and state what the extra reach costs to maintain.
Frame it as a trade of reach against reviewability, and decide when the team is allowed to buy reach at the cost of shipping an artifact nobody read.
## The setting You need a timing measurement around one method that lives inside a dependency you consume as a **compiled artifact**: you have the built unit, not the source, and you cannot wait for its maintainer to add the hook. Two mechanisms are available. You can put something **in front of** the object — a generated stand-in, a subtype, a hand-written wrapper — or you can **edit the compiled unit** that defines the method. The second reaches calls the first cannot, and the reason is entirely about *where the added code lives*. ## What a stand-in depends on A stand-in is a second object that forwards. For it to observe a call, three things must all hold: - **The caller must hold the stand-in, not the target.** Every path that hands out an instance has to be routed through whatever creates the stand-in. This is easy for objects your own wiring produces and impossible for objects it never sees. - **The member must be interceptable.** A stand-in built by subtyping can only affect members the original unit permits to be redefined; one built against a declared contract can only affect members that contract publishes. Anything outside that set is untouched. - **The dependency must not construct its own collaborators.** A library that creates a helper internally and calls it directly never asks anyone for an instance, so there is no seam to insert into. In the timing scenario above, the third condition is usually the one that fails: the method you want to measure is called on an object the dependency made for itself, deep inside its own code. ## What rewriting changes instead Rewriting operates on the compiled unit rather than on object graphs. A rewriter reads the already-compiled definition of the type, inserts instructions at the start and end of the chosen method body, and yields a unit the runtime treats as *the* definition of that type. Consequences: - The inserted code runs for **every call that reaches the body**, no matter which reference the caller holds or where the instance came from. - It applies to instances the dependency created for itself, because nothing had to be routed anywhere. - It can touch members a stand-in cannot be attached to, because it is not redefining anything — it is editing the definition. - It applies to **every instance at once**: a method body belongs to the unit, not to the object. The only nuance is in-process replacement, where a frame already executing the old body finishes under the old body. - It needs no source. The rewriter works from the compiled shape — the declared name, the parameter and return shapes, the instruction sequence. ## Where each one lands | Property | Stand-in in front of the target | Rewritten compiled unit | |---|---|---| | Where the added code lives | in a separate object | inside the method body | | Reaches instances the dependency created itself | no | yes | | Reaches members that cannot be redefined | no | yes | | Needs every caller routed through it | yes | no | | Needs the target's source | no | no | | Visible in the code under review | yes | no, unless recorded | | Checked by the runtime before it runs | not specially | yes, as a loaded unit | ## What the reach costs The extra reach is bought, not free: 1. **A toolchain.** Something must match the target's compiled shape and perform the edit, and it must be maintained against every upstream release. 2. **Silence when it drifts.** If the shape changes and the rule matches nothing, the instrumentation quietly disappears and the build still succeeds — unless the rule is configured to fail when it matches nothing. 3. **A review gap.** The artifact that runs is not the artifact anyone read. Diagnosis later starts from a false premise unless the rewrite is recorded somewhere the next engineer will look. 4. **Structural rejection.** A malformed edit is refused when the unit is loaded, and the complaint names the unit, not the tool that produced it. ## What an interviewer listens for The strong answer is one sentence of mechanism plus one concrete case: *a wrapper only intercepts calls that travel through the wrapper, and the call I need is on an object the library built for itself, so nothing travels through anything*. The weak answer treats the two as the same technique with different ergonomics, or assumes rewriting needs the dependency's source.
- The rewrite runs, the build is green, but the timing never appears for one method. What do you check first?Whether anything was edited at all. The rule matches a compiled shape, so an upstream rename, a changed parameter shape or a member folded into its caller leaves nothing to match, and a rule that ignores a miss reports success. Then check that the unit that actually loaded is the one you rewrote, rather than a second copy.
- Does rewriting one unit help when the call you want to measure crosses into a different compiled unit?No. The edit changes only the unit you rewrote; a call made from it into another unit is measured only if you also rewrite the callee, or wrap the call site inside the body you already edited. This is why rewrite rules are normally written as a pattern over many units rather than naming one member.
A wrapper is a receptionist: only visitors who come through the lobby are logged. Rewriting repaints the room itself, so it counts everyone who is in it however they got there.
saying these in an interview costs you the question
- Claims a wrapper intercepts every call made to the target object
- Thinks rewriting a compiled unit requires the dependency's source
- Says rewriting is just a wrapper that a tool generates for you
- Assumes any member can be intercepted by subtyping the target
- Believes instances created before the rewrite keep the old behaviour