NWScript JIT engine: JIT intrinsics, and JIT’d action service handler calls, part 2: Standard action calls

Last time, I outlined the general usage of the JIT intrinsics emitted by the MSIL backend for the NWScript JIT engine, and how they relate to action service calls. Today, let’s take a closer at how an action service handler is actually called in NWScript in the wild.

The MSIL backend currently supports three action call mechanisms (the ‘standard’ intrinsic, and the ‘fast’ intrinsic, and the (mostly) intrinsic-less ‘direct fast’ system); we’ll take a look at the ‘standard’ path first.

The standard action service path involves several the operation of at least one, but most probably several different intrinsics. In the standard path, the generated MSIL code is responsible for performing each fundamental step of the action service call operation distinctly; that is, the MSIL code pushes each parameter onto the VM stack in right to left order, making a call to the appropriate Intrinsic_VMStackPush function for each parameter type. Internally, these intrinsics place data on the ‘dummy’ VM stack object that will be passed to an action service handler.

Once all of the parameters are pushed on the stack, a call is made to Intrinsic_ExecuteActionService, which makes the transition to the action service handler itself. (Actually, it calls a dispatcher routine, which then calls the handler based on an index supplied, but we can ignore that part for now.)

Finally, if the action service handler had any return values, the generated MSIL code again invokes intrinsics to remove the return values from the VM stack and transfer them into MSIL locals so that they can be acted on.

Thus, the standard action service handler path is very much a direct translation into MSIL of the underlying steps the NWScript VM would take when interpreting the instructions leading up to an action call. If we look at the actual IL for an action call, we can see this in action (pardon the pun).

Consider the following NWScript source text:

void PrintHello()
{
	PrintString( "Hello, world (from NWScript)." );
}

The generated IL for this subroutine’s call to PrintHello looks something like as so (for NWN2):

.method private instance void
NWScriptSubroutine_PrintHello() cil managed
{
  // Code size       93 (0x5d)
  .maxstack  6
  .locals init (uint32 V_0,
           string V_1)

// ...

  IL_0025:  ldstr      "Hello, world (from NWScript)."
  IL_002a:  stloc.1
  IL_002b:  ldarg.0
  IL_002c:  ldfld      m_ProgramJITIntrinsics
  IL_0031:  ldarg.0
  IL_0032:  ldfld      m_ProgramJITIntrinsics
  IL_0037:  ldloc.1
  IL_0038:  callvirt   instance void
Intrinsic_VMStackPushString(string)
  IL_003d:  ldc.i4     0x1
  IL_0042:  conv.u4
  IL_0043:  ldc.i4     0x1
  IL_0048:  conv.u4
  IL_0049:  callvirt   instance void
Intrinsic_ExecuteActionService(uint32,
                               uint32)

In essence, the generated code makes the following calls:

String ^ s = "Hello, world (from NWScript)';
m_ProgramJITIntrinsics->VMStackPushString( s );
// PrintString is action ordinal 1,
// and takes one source-level argument.
m_ProgramJITIntrinsics->ExecuteActionService( 1, 1 );

If PrintString happened to return a value, we would have seen a call to VMStackPop* here (or potentially several calls, if several return values were placed on the VM stack).

While the standard call path is functional, it does have its downsides. Internally, each of the intrinsics actually goes through several levels of indirection:

  1. First the JIT code calls the .NET interface INWScriptProgram intrinsic method.
  2. The INWScriptProgram intrinsic’s ultimate implementation in the JIT core module, NWNScriptJIT.dll, calls into a C++-level interface, INWScriptStack or INWScriptActions, depending on the intrinsic. This indirection takes us cross-module from NWNScriptJIT.dll to the script host, such as NWNScriptConsole.exe or NWN2Server.exe.
  3. Finally, the implementation of INWScriptStack or INWScriptActions performs the requested operation as normal.

Most of these indirection levels are fairly thin, but they involve a managed/native transition, which involves marshalling and some additional C++/CLI interop expense (particularly when NWScript strings are involved).

The fast action service handler interface, which we’ll discuss next time, attempts to address the repeated managed/native transitions by combining the various steps of an action service call into one transacted managed/native transition.

Tags:

One Response to “NWScript JIT engine: JIT intrinsics, and JIT’d action service handler calls, part 2: Standard action calls”

  1. […] Nynaeve Adventures in Windows debugging and reverse engineering. « NWScript JIT engine: JIT intrinsics, and JIT’d action service handler calls, part 2: Standard … […]