Sebastian Barth
Sebastian Barth

Reputation: 4551

Injecting method calls using Mono.Cecil draws InvalidProgramException in constructor

I use Mono.Cecil to inject two method calls into a constructor. Somehow the construction always fails with the following error:

InvalidProgramException: Invalid IL code in Networking.ServerController:.ctor (): IL_0028: callvirt  0x0600017f

What I basically do is to inject a method call to the following method (from two base-classes above) for every stfld while passing the name of the method (without class) and the current field value (boxed) as object:

protected void LocalNetworkVariableChange(string variableName, object value) {
  // Fails no matter what i put here
}

The IL code before injection:

IL_0000: ldarg.0
IL_0001: newobj System.Void System.Collections.Generic.Dictionary`2<System.UInt16,Networking.ServerController/ConnectedPlayer>::.ctor()
IL_0006: stfld System.Collections.Generic.Dictionary`2<System.UInt16,Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByClientId
IL_000b: ldarg.0
IL_000c: newobj System.Void System.Collections.Generic.Dictionary`2<Util.NuID,Networking.ServerController/ConnectedPlayer>::.ctor()
IL_0011: stfld System.Collections.Generic.Dictionary`2<Util.NuID,Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByPlayerId
IL_0016: ldarg.0
IL_0017: call System.Void Networking.PeerController`1<Networking.ServerController>::.ctor()
IL_001c: nop
IL_001d: ret

After Injection:

IL_0000: ldarg.0
IL_0001: newobj System.Void System.Collections.Generic.Dictionary`2<System.UInt16,Networking.ServerController/ConnectedPlayer>::.ctor()
IL_0006: stfld System.Collections.Generic.Dictionary`2<System.UInt16,Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByClientId
IL_000b: ldarg.0
IL_000c: newobj System.Void System.Collections.Generic.Dictionary`2<Util.NuID,Networking.ServerController/ConnectedPlayer>::.ctor()
IL_0011: stfld System.Collections.Generic.Dictionary`2<Util.NuID,Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByPlayerId
IL_0016: ldarg.0
IL_0017: call System.Void Networking.PeerController`1<Networking.ServerController>::.ctor()
IL_001c: nop
IL_0000: ldarg.0
IL_0000: ldstr "_connectedPlayersByClientId"
IL_0000: ldfld System.Collections.Generic.Dictionary`2<System.UInt16,Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByClientId
IL_0000: callvirt System.Void Networking.NetworkBehaviour::LocalNetworkVariableChange(System.String,System.Object)
IL_0000: ldarg.0
IL_0000: ldstr "_connectedPlayersByPlayerId"
IL_0000: ldfld System.Collections.Generic.Dictionary`2<Util.NuID,Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByPlayerId
IL_0000: callvirt System.Void Networking.NetworkBehaviour::LocalNetworkVariableChange(System.String,System.Object)
IL_001d: ret

(!) The labels in front of the instructions are updated lazy when writing out the instructions to file via Mono.Cecil. This is why the seem to have a wrong offset and map all to IL_0000.

The final IL code looks like this:

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       62 (0x3e)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  newobj     instance void [netstandard]System.Collections.Generic.Dictionary`2<uint16,class Networking.ServerController/ConnectedPlayer>::.ctor()
    IL_0006:  stfld      [netstandard]System.Collections.Generic.Dictionary`2<uint16,class Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByClientId
    IL_000b:  ldarg.0
    IL_000c:  newobj     instance void [netstandard]System.Collections.Generic.Dictionary`2<class Util.NuID,class Networking.ServerController/ConnectedPlayer>::.ctor()
    IL_0011:  stfld      [netstandard]System.Collections.Generic.Dictionary`2<class Util.NuID,class Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByPlayerId
    IL_0016:  ldarg.0
    IL_0017:  call       instance void class Networking.PeerController`1<class Networking.ServerController>::.ctor()
    IL_001c:  nop
    IL_001d:  ldarg.0
    IL_001e:  ldstr      "_connectedPlayersByClientId"
    IL_0023:  ldfld      [netstandard]System.Collections.Generic.Dictionary`2<uint16,class Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByClientId
    IL_0028:  callvirt   instance void Networking.NetworkBehaviour::LocalNetworkVariableChange(string,
                                                                                               object)
    IL_002d:  ldarg.0
    IL_002e:  ldstr      "_connectedPlayersByPlayerId"
    IL_0033:  ldfld      [netstandard]System.Collections.Generic.Dictionary`2<class Util.NuID,class Networking.ServerController/ConnectedPlayer> Networking.ServerController::_connectedPlayersByPlayerId
    IL_0038:  callvirt   instance void Networking.NetworkBehaviour::LocalNetworkVariableChange(string,
                                                                                               object)
    IL_003d:  ret
  } // end of method ServerController::.ctor

The two fields look like this:

[NetworkVariable(SyncOnChange = false)]
private readonly Dictionary<ushort, ConnectedPlayer> _connectedPlayersByClientId = new
  Dictionary<ushort, ConnectedPlayer>();
[NetworkVariable(SyncOnChange = false)]
private readonly Dictionary<NuID, ConnectedPlayer> _connectedPlayersByPlayerId = new
  Dictionary<NuID, ConnectedPlayer>();

Any idea?

Upvotes: 0

Views: 101

Answers (1)

canton7
canton7

Reputation: 42330

The first problem is that box System.Collections.Generic.Dictionary.... You can't box a reference type.

The second problem is the ldfld. From the docs:

The stack transitional behavior, in sequential order, is:

  1. An object reference (or pointer) is pushed onto the stack.
  2. The object reference (or pointer) is popped from the stack; the value of the specified field in the object is found.
  3. The value stored in the field is pushed onto the stack.

In other words, ldfld needs to know the object to read the field from, and it consumes that object parameter from the stack.

So you need:

ldarg.0   // Load the 'this' for the method call
ldstr ... // Load the string to pass to the method
ldarg.0   // Load the 'this' for the ldfld
ldfld ... // Load the dictionary to pass to the method
callvirt ...

Upvotes: 1

Related Questions