Here is a complete implementation of the solution outlined by others on this page here and here. This code allows you to import, or "hard-code" any live object reference you are able supply into the IL
stream of a DynamicMethod
as a permanently burned-in 32- or 64-bit "literal" reference.
Please note that this is obviously n̲o̲t̲ a recommended technique, and is shown here for educational and/or experimental purposes only
As noted in one of the comments, you don't need to pin the GCHandle
; it's perfectly fine for GC to move the object around normally since the numeric value of the handle won't change so long as the handle remains alive. The real reason you might need to hold a GCHandle
here is that the completed DynamicMethod
instance will not be holding a reference to (nor in fact have any knowledge of) the handle embedded within itself. Without the GCHandle,
the instance could be collected when/if all other references to it go out of scope.
This code below takes an overly cautious approach by intentionally abandoning the GCHandle
struct (i.e., by not freeing it) after using it to extract the object reference. This means that the handle--and thus also the target instance--will never be collected. If your application has other ways or means of keeping the handle alive, feel free to rely on those, as long as you can guarantee that the the handle survives the lifetime of the DynamicMethod
it's emitted into via this technique; in this case you would free the GCHandle
(code shown commented out) after using it to obtain the handle value.
/// <summary>
/// Burn a reference to the specified runtime object instance into the DynamicMethod
/// </summary>
public static void Emit_LdInst<TInst>(this ILGenerator il, TInst inst)
where TInst : class
{
var gch = GCHandle.Alloc(inst);
var ptr = GCHandle.ToIntPtr(gch);
if (IntPtr.Size == 4)
il.Emit(OpCodes.Ldc_I4, ptr.ToInt32());
else
il.Emit(OpCodes.Ldc_I8, ptr.ToInt64());
il.Emit(OpCodes.Ldobj, typeof(TInst));
// Do the following only if you can elsewhere ensure that 'inst'
// outlives this DynamicMethod
// gch.Free();
}
A contribution of this answer not mentioned by others is that you should use the Opcodes.Ldobj
instruction to coerce the proper runtime Type
onto the newly hard-coded literal, as shown above. It's easy to verify that this is a good practice with the following test sequence. It produces a bool
indicating whether the System.Type
of the freshly-imported instance is what we expect to be, and it only returns true
when the Opcodes.Ldobj
is instruction is present in the extension method shown above.
TInst _inst = new MyObject();
// ...
il.Emit_LdInst(_inst); // <-- the function shown above
il.Emit(OpCodes.Isinst, typeof(TInst));
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Ceq);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Xor);
What doesn't seem to be necessary after our rude Ldc_I4
/ Ldc_I8
shove is Conv_I
, and this seems to be true even if we drop the IntPtr.Size
checking and just use Ldc_I8
to always load a long
, even on x86. This is again thanks to Opcodes.Ldobj
smoothing over such misdeeds.
Here's another use example. This one checks for reference equality between the embedded instance (imported at DynamicMethod-creation time) and whatever reference-type objects might variously be supplied when calling that method any time in the future. It only returns true
when its long-lost progenitor shows up. (One wonders how the reunion might go...)
il.Emit_LdInst(cmp);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ceq);
Finally, a remark about the where TInst : class
constraint on the extension method shown at the top. For one thing, there's no reason to import a value type in this manner, since you can just import its fields as literals instead. You may have noticed however, that the crucial Opcodes.Ldobj
which makes the import work more reliably is documented as being intended for value-types, and not reference types as we are doing here. The simple key to this is to remember that really, an object reference is a handle which is itself just a pattern of 32- or 64- bits which is always copied by-value. In other words, basically a ValueType
.
I've tested all of this pretty widely on both x86 and x64, debug and release, and it works great with no problems as of yet.
SomeObject
is handed out elsewhere, etc. – Deontologyobject someObj = ... Action action = () => Function(someObj);
), that is, instead, the equivalent ofAction action = () => Function(new someClass())
- which isn't quite what you asked in the original question (where you had an object created outside of the function) – Deontology