Previously, I demonstrated how a simple NWScript subroutine could be translated into MSIL, and then to native instructions by the CLR JIT. We still have a large piece of functionality to cover, however, which is calling action service handlers (extension points) from JIT’d code. In order to understand how action service handlers work, we need to delve into a side-topic first — JIT intrinsics.
In certain circumstances, the MSIL backend for the NWScript JIT engine utilizes a series of NWScript JIT intrinsics in IL that it generates when producing the IL-level representation of a script program. Simply put, these JIT intrinsics faciliate operations that must either invoke native code or that are too complex or unwieldy to desirably inline in the form of IL instructions in the generated instruction stream. The bulk of the JIT intrinsics deal with interfacing with action service handlers, which as you recall, are the main I/O extension points used by the script program to communciate with the code running in the “outside world” (or at least the script host itself).
In order to understand why these intrinsics are useful, however, we need to understand more about how action service handlers are called. Using the NWScript VM that I wrote as a reference, an action service handler simply receives a pointer to a C++ object representing the current VM stack. The action service handler then pops any parameter values off of the VM stack, and pushes the return values of the action back, in accordance with the standard action calling convention defined by the NWScript ACTION opcode.
Now, were the action handler to be called by the NWScript VM, it would be passed the actual execution stack in use by the VM as the program’s main data store, and that would be that.
Recall, however, that the NWScript JIT engine is designed to be a drop-in replacement for the interpretive NWScript VM. That means that it must ultimately use the same VM-stack calling convention for action service handler calls. This is advantageous as there are a great number of action service calls exposed by the NWN2 API (over a thousand), and rewriting these to use a new calling convention would be a painful undertaking.
Furthermore, reusing the same calling convention allows each action service handler call to be used by both the JIT and the VM in the same program, which allows for possibilities such as background JIT with cutover, or simply a defense against the JIT having a bug or simply not being available (perhaps .NET 4.0 isn’t installed — the core server itself does not require it).
Thus, in order to make an action service handler call, the MSIL JIT backend needs to call various C++ functions to place items on a VM stack object that can be passed to an action service handler’s real implementation. (In the case of the JIT system, I simply create a ‘dummy’ VM stack that only ever contains parameters and return values for the current action service handler.)
However, the IL code emitted by the NWScript JIT cannot easily directly interface with the VM stack object (which is written in native C++). The solution I selected for this problem was to create the set of JIT intrinsics that I made reference to previously; these JIT intrinsics, implemented in C++/CLI code, expose the mechanisms necessary to invoke an action service handler to NWScript in the form of a safe/verifiable .NET interface. (Actually, the reality is a little bit more complex than that, but this is a close approximation.)
For performance reasons (recall that action service calls are to NWScript as system calls are to a native program), the NWScript JIT backend exposes three distinct mechanisms to call into an action service handler. Most of these mechanisms heavily rely on various special-purpose JIT intrinsics, as we’ll see shortly:
- A “standard” action service call mechanism, corresponding of a series of intrinsics for each VM stack operation (i.e. push a value on the VM stack, pop a value off the VM stack, call the action service handler). The standard action service call mechanism is invoked when an action service call has five or fewer combined parameters and return values, or if the action service call involves an engine structure.
- A “fast” action service call mechanism, corresponding of a single unified intrinsic that combines pushing parameters onto the VM stack, calling the action service handler, and popping any return values off the stack. If verifiable IL is desired, the fast action service call mechanism is invoked when an action service call has six or more combined parameters and return values and does not involve any engine structures.
- A “direct fast” action service call mechanism, which generates direct, devirtualized calls to the raw C++-level interface used by the NWScript host to expose action service handlers. The direct fast action service call mechanism is the fastest action call mechanism by a large margin, but the emitted IL is non-verifiable (and in fact specific and customized to the instance of the NWScript host process). Like the ordinary fast action service call mechanism, the direct fast action service call does not support action service calls that involve engine structures. If non-verifiable IL is acceptable the direct fast action service call mechanism is always used unless an engine structure is involved.
Why the distinction at six combined parameters and return values with respect to the “fast” action service call mechanism? Well, profiling determined that the fast mechanism is actually only faster than the standard mechanism — in the current implementation — if there are seven or more intrinsics being called at once (six parameter or return value VM stack operations, plus the actual action call intrinsic). We’ll get into more details as to why this is the case next time. All three action service handler invocation mechanisms perform the same effect at the end of the day, however.
For the most part, the .NET-level interface exposed by the JIT intrinsics system is relatively simple. There is an interface class (INWScriptProgram) that exposes a set of APIs along the line of these:
// // Push an integer value onto the VM stack (for an action call). // void Intrinsic_VMStackPushInt( __in Int32 i ); // // Pop an integer value off of the VM stack (for an action call). // Int32 Intrinsic_VMStackPopInt( ); // ... // // Execute a call to the script host's action service handler. // void Intrinsic_ExecuteActionService( __in UInt32 ActionId, __in UInt32 NumArguments ); // ... // // Execute a fast call to the script host's action service handler. // Object ^ Intrinsic_ExecuteActionServiceFast( __in UInt32 ActionId, __in UInt32 NumArguments, __in ... array< Object ^ > ^ Arguments );
When a piece of generated code needs to access some extended functionality present in a JIT intrinsic, all that needs to be done is to set up a call to the appropriate JIT intrinsic interface method on the JIT intrinsics interface instance that is handed to each main script program class. This allows complex functionality to be written in C++/CLI versus directly implemented as raw, emitted IL.
Aside from logic to support action service handler invocation, there are several additional pieces of functionality exposed as JIT intrinsics. Specifically, comparison and creation logic for engine structures is offloaded to JIT intrinsics, as well as a portion of the code to set up a saved state object for an I_SAVE_STATE instruction.
On that note, next time we’ll dig in deeper as to what actually goes on for a JIT’d action service handler call under the hood, including how the above JIT intrinics work and how they are used.