MSTDK / RPC

Getting Started Guide

Introduction

This documentation is available to help you better understand what MSTDK RPC is and how to use remote procedure calls to communicate with your title in real time.

Overview

The RPC (Remote Procedure Call) library is a part of the MSTDK (Microsoft Studios Test Development Kit) suite, and is referred to simply as “RPC” for the remainder of this document. RPC allows you to remotely interact with your title for test automation and other custom tooling specific to your title. To use RPC, you create hooks in your server code; then, you call these hooks from your client code (e.g.: .NET-based test tools). What those hooks are and how they affect your game is entirely up to you.

Besides hooks used to interact with your title, RPC also supports event notification. Clients can subscribe to an event then wait for the server to notify the event handler when that event occurred on the server. Events are executed asynchronously and are used to receive information about an event from the server.

The two figures below illustrate a sequence diagram of a hook and one of an event. 

 

RPC diagram

 

image

This document will assist you in setting up hooks and events so that you can interact with your title through RPC.

Note:

To better scope and clarify certain terms for this doc, the following terms are defined as:

  • The term “server code” refers to code running within your title. This code could be a game or an app. The term “client code” refers to code running on your client machine.
  • The term “server” or “server-side” refers to any machines running your “server code” and the term “client” or “client-side” refers to any machines running your “client code”. In this case, server refers to the Xbox One console or a Windows PC where the title is launched. In the Windows PC case, if you are implementing an app, you could be launching your app and testing it all on the same machine. However, the concept discussed in this doc regarding “server” and “client” still applies since a launched app is considered to be running on a server-side environment while testing is done on a client side machine.
  • The term “title” refers to a game, an application, or whatever the project is you are developing.

Prerequisites

Before you get started, be sure you have the following items ready.

  • If your title targets the Xbox One console, you must have a 64-bit machine to build and run the project because TDK RPC is only built on the x64 codebase.
  • Know your server’s IP address. This could be an IP of your Xbox One console, in which case, it is the System IP address, or your Windows PC’s IP address where your server code is running. You need this for your client code to connect to your server code. You will also need to install the latest Durango XDK on your client machine if your title is being built for the Xbox One. RPC currently supports XDK QFE3.
  • If you plan to use TDK RPC in your title running on the Windows Server 2012 platform, then you must install the Windows Media Foundation feature. This is because the TDK RPC is leveraging the WinRT sockets API. To enable the Windows Media feature you can run the following command on the Windows Server 2012 machine:

dism /online /enable-feature /featurename:ServerMediaFoundation

Complete MSDN documentation can be found here:

http://msdn.microsoft.com/en-us/library/windows/apps/windows.networking.sockets.aspx

How to create RPC hooks

You must create hooks in your server code before you can call them from your client code. The RPC server-side comes as a static library and a collection of header files. To use the library you need to link it into your project then setup your project properties so that it can find the required headers. This scenario will walk you through the process of setting up your project and creating a hook in your title.

To create a hook, do the following.

1. Set up your project properties so you can integrate the library into your game. From Solution Explorer, right-click your server-side project and click Properties.

a. Under Configuration Properties, select VC++ Directories, add the path to the headers into the Include Directories property and add the path to the library into the Library Directories property.

b. Under Configuration Properties, expand Linker and select Input, add the name of the server-side library (e.g.: GamesTest.Rpc.NativeServer.lib) into the Additional Dependencies property.

Note:

For information on how to build relative paths to these libraries and header files, see http://msdn.microsoft.com/en-us/library/c02as0cs(v=vs.110).aspx

2. In your server code, add the following:

#define ENABLE_GAMESTEST_RPC

#include <RpcServer.h>

#include <RpcArchive.h>

NOTES:

  • Define ENABLE_GAMESTEST_RPC in development build configurations prior to including any RPC header files. This prevents all content within RPC headers from being compiled out of development build where the hook functionality is desirables. It’s also recommended that you enclose any code interacting with the RPC library by a precompiled conditional using the same symbol:

#ifdef ENABLE_GAMESTEST_RPC

// Code that uses RPC here

#endif

This mechanism makes it easy to exclude all RPC functionality from shipping builds by not defining ENABLE_GAMESTEST_RPC in those build configurations. RPC will also disable itself on “RETAIL” and “CERT” sandboxes as well as any sandboxes ending on ”.99” – no initialization such as spawning threads or opening sockets will be performed on these sandboxes. This makes it possible to leave RPC code in shipping builds where it will be disabled unless the shipping build is moved into another sandbox for troubleshooting, at which point the hooks start working again.

Warning: if you choose to include RPC in shipping builds, conduct a separate security review for each title to ensure the approval of all stakeholders.

The IsEnabled property can be used to check whether the RPC server is enabled or not (all methods return GAMESTEST_RPC_E_METHOD_DISABLED_INVALID_SANDBOX HRESULT code when RPC is disabled):

if (pServer->IsEnabled)

{

// Code that uses RPC here

}

  • The ws2_32.lib import library required on Durango and Windows platforms is automatically included using precompiled directives in RpcImports.h, but some project templates also put the same import library in the list of Libraries to Ignore. This cancels out the precompiled setting resulting in linker errors from missing ws2_32.lib. Remove this import library from the list of Libraries to Ignore to fix this issue.

3. Include this namespace:

using namespace Microsoft::Internal::GamesTest::Rpc::Server;

4. Create a top level object for the server

RpcServer* pServer = new RpcServer();

5. Define and implement your hooks.

The signature of a hook is defined as: a method that accepts an RpcArchive with input values and an RpcArchive to hold output values and returns an HRESULT. Here is the signature of an RpcHook:

typedef RpcHookCode (*RpcHook)(RpcArchive& input, RpcArchive& output);

For example, let’s say we have a simple “Add” method that takes two operands and adds them together then returns the result. That hook method may look something like this:

RpcHookCode Add(RpcArchive& input, RpcArchive& output)

{

__int16 operand1, operand2;

input.GetNamedValue(L"operand1", &operand1);

input.GetNamedValue(L"operand2", &operand2);

__int16 result = operand1 + operand2;

output.SetNamedValue(L"result", result);

return S_OK;

}

Note:

If your hook returns an error, it will show up on the client-side as an exception. Make sure your client code catches these exceptions. Otherwise return S_OK. Refer to the section Throwing custom exceptions for more information on how to return error codes from the server.

6. Register the hook and start the server. In your title’s initialization code, add the following lines of code.

#ifdef ENABLE_GAMESTEST_RPC

pServer->RegisterHookMethod(L"Add", ::Add);

pServer->Start();

#endif

The RegisterHookMethod method accept two parameters. The first is a hook label; the client will use this name to call this hook. The second is the name of the RpcHook method.

Register as many hooks as you want. Only methods that are registered are available to your client. For every hook you register, you must implement that method in your project for the hook to work.

Note:

  • While you may have different labels pointing to the same hook method, you may not have the same label pointing to different hook methods. That is, if you register the same label to different hook methods, only the last RegisterHookMethod call gets the mapping for that label.

7. Somewhere in your game loop, call the ProcessPendingRequests method.

// Somewhere in your game loop

#ifdef ENABLE_GAMESTEST_RPC

pServer->ProcessPendingRequests();

#endif

When requests from your client are received they will be placed into a queue and processed at a time of your choosing. You must call ProcessPendingRequests method somewhere in your game loop to process the queue otherwise hook requests from your client will never be processed.

Calling this method will flush the queue and process any hooks pending in the queue. This gives you the flexibility to execute your hooks on the appropriate thread and at the appropriate time to interact with the objects in your title.

8. Compile and run your title. You are now ready to call these hooks from your client.

How to use RPC to call my hook

Before you can call a hook on the server, make sure you have implemented the hook and have it registered with an RpcServer. Then, on the client-side, you will call the hook via two RPC objects:

  • RpcChannel: This object helps you get your data across the wires and back.
  • RpcArchive: This object stores your data.

Note that the RpcChannel object is thread-safe whereas RpcArchive object is not.

To call a hook from the client-side, do the following (assuming you are calling your simple Add method).

  1. Check to make sure the title is running.
  2. Create a new RpcChannel object and connect to it via the server’s IP address (e.g.: for Xbox One, it’s the console’s System IP address).

RpcChannel channel = new RpcChannel(IPAddress.Parse("10.124.150.208"));

channel.Connect();

The Connect method has an overload that accepts a timeout, useful for situations where the application containing the server has recently been started, and may or may not be listening at the time of the Connect call. Use a larger timeout to make RPC client code more resilient to failures due to slow startup times caused by network connections or other I/O.

A port number can also be passed into any of RpcChannel constructors, like this:

RpcChannel channel = new RpcChannel(IPAddress.Parse("10.124.150.208"), 4009);

Note that Xbox One applications need entries in their manifests when port numbers other than 4600 or 4601 are used (those two ports are reserved for debugging during app development by the system, specifically for things like RPC). The static function RpcServer::GetDefaultPort returns one of these reserved ports, which is also what RPC is using when a port number was not passed to a constructor during initialization.

Here are the tags that need to be added to server-side Durango app manifest to use ports other than 4600/4601 (to be placed within Package\Applications\Application, port number highlighted):

<Extensions>

<mx:Extension Category="windows.xbox.networking">

<mx:XboxNetworkingManifest>

<mx:SocketDescriptions>

<mx:SocketDescription Name="FuncTestSocket" SecureIpProtocol="Tcp" BoundPort="1234">

<mx:AllowedUsages>

<mx:SecureDeviceSocketUsage Type="Initiate" />

<mx:SecureDeviceSocketUsage Type="Accept" />

<mx:SecureDeviceSocketUsage Type="SendDebug" />

<mx:SecureDeviceSocketUsage Type="ReceiveDebug" />

</mx:AllowedUsages>

</mx:SocketDescription>

</mx:SocketDescriptions>

<mx:SecureDeviceAssociationTemplates>

<mx:SecureDeviceAssociationTemplate Name="FuncTestTraffic" InitiatorSocketDescription="FuncTestSocket" AcceptorSocketDescription="FuncTestSocket" MultiplayerSessionRequirement="None">

<mx:AllowedUsages>

<mx:SecureDeviceAssociationUsage Type="Default" />

</mx:AllowedUsages>

</mx:SecureDeviceAssociationTemplate>

</mx:SecureDeviceAssociationTemplates>

</mx:XboxNetworkingManifest>

</mx:Extension>

</Extensions>

To specify more ports, add more SocketDescription elements inside Extensions\Extension\XboxNetworkingManifest\SocketDescriptions, as well as respective SecureDeviceAssociationTemplate elements using the same socket name, under Extensions\Extension\XboxNetworkingManifest\SecureAssociationTemplates.

3. Create your RpcArchive input object and call SetNamedValue to set as many name/value pair as the hook method requires. In this case, the Add method accepts 2 name/value pair for the operation. See the sections below for samples on setting other data types.

RpcArchive input = new RpcArchive();

input.SetNamedValue("operand1", (short)45);

input.SetNamedValue("operand2", (short)32);

4. Using the RpcChannel object, invoke the hook method. Be sure to capture the response. Also, make sure you handle exceptions accordingly.

RpcArchive response = channel.InvokeRemoteMethod("Add", input);

Or to specify a specific timeout for just this call, you can call the following overload:

RpcArchive response = channel.InvokeRemoteMethod("Add", input, TimeSpan.FromSeconds(45));

Note: Exceptions thrown from this method include RpcException and System.TimeoutException. Mapping hook error codes as explained in the Throwing custom exceptions section will also throw your custom exceptions.

The System.TimeoutException is the most common exception to be thrown by this method. Possible reasons include:

  • The hook being called is blocking for longer than the timeout specified for the channel or the method call
  • A hook call made earlier is still executing and blocking the server from processing other requests
  • The server might not be processing requests (RpcServer::ProcessPendingRequests is not getting called)
  • The time between calls to ProcessPendingRequests on the server exceeds the timeout for the method call
  • The server is unable to handle the current volume of traffic because the blocking time of each request adds up to cause some of the requests to time out.

5. Parse the response object to get at what the server returns to you. In this case, your Add method returns a short named result. See the sections below for samples on getting other data types.

short result = response.GetNamedValue<short>("result");

6. After you are done, disconnect from the server and dispose of the channel.

channel.Disconnect();

channel.Dispose();

Using the RPC Client library from multiple threads

The RpcChannel object supports usage scenarios where hook invocation and event registration are done by multiple threads. The RpcArchive object is not thread-safe and should not be used by more than one thread at a time without some sort of manual locking mechanism.

As a result of the multi-threading support, multiple method invocations could be in progress at the same time. This means that if a low-level parsing error occurs and causes the internal RPC parsing thread to end, all requests will simultaneously fail, returning the original exception that caused the failure. The parsing errors could happen due to corrupted or otherwise unexpected data arriving from the server.

How to receive data on the server-side

In RPC, data is passed between the client and server via RpcArchive objects. Through these objects, RPC allows you to exchange multiple data types between the client and the server. These data types include:

  •        Primitive values
  •        Strings
  •        Collections of primitives or strings

Receiving strings and collections on the server is a bit more involved. Conceptually, receiving a primitive value on the server is as simple as calling the archive.GetNamedValue<T> method for the type of primitive value you are expecting. To receive a string, you will need to measure how long the string is and you need to create a buffer to hold the string. To receive a collection of strings, you will also need to measure how long the combined strings are and you will need two buffers instead of one; where, one buffer contains the combined list of strings and the other buffer contains a list of pointers into the string buffer. Your task is to decide and declare how these strings are stored; RPC library will do the rest.

This section will focus on helping you understand how String and Collections work and how to use them to receive data on the server.

Receiving strings on the server

Receiving a string on the server is a multi-step process. The following steps illustrate how to receive a string.

1. Determine the length of the incoming string so that you can be sure to have a buffer large enough to hold it. Note that the MeasureNamedStringValue method returns the length of the string including the null-termination character.

HRESULT hr;

unsigned long stringLength = 0;

hr = input.MeasureNamedStringValue(L"inputString", &stringLength);

if(FAILED(hr))

{

return STRING_SIZE_COULD_NOT_BE_OBTAINED;

}

2. Allocate a buffer to hold its content.

wchar_t* buffer = new wchar_t[stringLength];

3. Pass the buffer into RpcArchive to retrieve the string via the GetNamedStringValue method.

unsigned long numCharsWritten;

HRESULT hr = input.GetNamedStringValue(L"inputString", buffer, stringLength, &numCharsWritten );

if(FAILED(hr))

{

delete[] buffer;

return INPUT_FIELD_NOT_FOUND;

}

The steps above are a lot of code just to retrieve a string from an RpcArchive. Therefore, it is suggested that users create their own method that can retrieve the appropriate data types from an RpcArchive and return the appropriate type; in this case, an instance of the string class used by the game engine. For example, suppose that your game engine used the std::wstring class. You could write a method to retrieve a string from an RpcArchive like this:

HRESULT GetStringValue(const wchar_t* fieldName, const RpcArchive& archive, std::wstring* result)

{

HRESULT hr;

const int StaticBufferSize = 32;

wchar_t buf[StaticBufferSize];

wchar_t* bufPtr = buf;

unsigned long stringLength = 0;

unsigned long stringBufferLength = 0;

hr = archive.MeasureNamedStringValue(fieldName, &stringLength);

if(FAILED(hr))

{

return hr;

}

if(stringLength > StaticBufferSize)

{

bufPtr = new wchar_t[stringLength];

stringBufferLength = stringLength;

}

else

{

stringBufferLength = StaticBufferSize;

}

unsigned long numCharsWritten;

HRESULT hr = archive.GetNamedStringValue(fieldName, bufPtr, stringBufferLength, &numCharsWritten);

if(SUCCEEDED(hr))

{

*result = std::wstring(bufPtr, numCharsWritten);

}

if(bufPtr != buf)

{

delete[] bufPtr;

}

return hr;

}

Notes:

  • In the string implementation we do our best to avoid dynamic memory allocation for strings that are fairly small.
  • For strings that are smaller than the StaticBufferSize, the numCharsWritten is used instead because it holds the exact number of characters the string has (including the null-terminator character).

Receiving a collection of primitive values on the server

The process described in the section for string shows how to retrieve a string value. You can do the same to retrieve values for a collection of primitives. Since a string is stored in a buffer that is an array type, you can utilize the same construct to retrieve a collection of primitive values. RPC provides overloaded methods for each primitive type. Instead of calling this to get an array of characters:

archive.GetNamedStringValue(fieldName, bufPtr, stringBufferLength, &numCharsWritten);

You can call this method to get an array of primitive values:

archive.GetNamedCollection(fieldName, bufPtr, &numElements);

In this case, to receive a collection of primitive values, you need a pointer to the buffer that holds the combined values. You will also need to know the number of elements in the buffer. You can get the numElements by calling the MeasureNamedCollection method. As with a string, it is recommended that you create a method to retrieve these primitive values. You might also consider using the typename template so you can use one method for all the primitive types, like this:

template<typename fieldType>

HRESULT GetPrimitiveCollection(const wchar_t* fieldName, const RpcArchive& archive, fieldType* results)

{

HRESULT hr;

unsigned long numElements = 0;

fieldType* bufPtr = nullptr;

hr = archive.MeasureNamedCollection(fieldName, &numElements);

if(FAILED(hr))

{

return hr;

}

bufPtr = new fieldType[numElements]; //Allocating memory for the buffer

hr = archive.GetNamedCollection(fieldName, bufPtr, numElements);

if(SUCCEEDED(hr))

{

results = bufPtr;

}

return hr;

}

 

Receiving a collection of strings on the server

With the support of String and Collections, RPC can also support a collection of strings. Conceptually, a collection of strings consist of two data parts, where one stores the combined characters of all the strings (including their null-terminators) and the other stores a list of pointers that points to the beginning of each string in the combined array. In implementation details, you may choose to allocate memory for two separate arrays or you may allocate one continuous block of memory to keep track of the two data parts. Either way, the process is the same:

1. Determine the number of strings and total lengths of the combined strings by calling the MeasureNamedStringCollection method.

2. Create memory for the array, using either one or two allocations.

3. Retrieve the string values by calling the GetNamedStringCollection method.

Using two allocation of memory is simpler. For example, to receive a collection of strings using two arrays, you might do something like this:

unsigned long numStrings;

unsigned long numStringCharacters;

input.MeasureNamedStringCollection(L"stringCollection", &numStrings, &numStringCharacters);

wchar_t** stringBuffer = new wchar_t*[numStrings]; // Pointers to content

wchar_t* stringContents = new wchar_t[numStringCharacters]; // Combined string

archive.GetNamedStringCollection(L"stringCollection", stringBuffer, numStrings, stringContents, numStringCharacters);

Using one allocation of continuous memory is more complex. To receive a collection of strings using just one allocation of continuous block of memory, you might do something like this:

unsigned long numStrings;

unsigned long numStringCharacters;

input.MeasureNamedStringCollection(L"stringCollection", &numStrings, &numStringCharacters);

wchar_t** stringBuffer2 = (wchar_t**)malloc((numStrings * sizeof(wchar_t*)) + (numStringCharacters * sizeof(wchar_t)));

input.GetNamedStringCollection(L"stringCollection", stringBuffer2, numStrings, (wchar_t*)(stringBuffer + numStrings), numStringCharacters);

From there, to get to the individual strings, just iterate through the number of strings. For each, jump to the start of each string and traverse from there until a null-terminator is reached. That block of characters will give you the content of that entire string. For example:

for (int i = 0; i < numStrings; i++)

{

wchar_t* rawString = stringBuffer[i]; // or "stringBuffer2[i]"

std::wstring string(rawString); // This can be any string class

// Use "string" here

// ...

}

How to send data from the server-side to the client-side

While receiving data from client-side is done through the input RpcArchive object, sending data back to the client is done through the output RpcArchive object. The process of sending a string from the server to the client is pretty straightforward. Inside the hook, simply set the value you want to send back to the client. In the example below, we are sending back a string to the output archive:

RpcHookCode SendString(RpcArchive& input, RpcArchive& output)

{

HRESULT hr;

hr = output.SetNamedStringValue(L"outputString", L"outgoing string value");

if(FAILED(hr))

{

delete[] buffer;

return OUTPUT_FIELD_COULD_NOT_BE_SET;

}

return S_OK;

}

The same process can be used to send back any data you want, just be sure to call the appropriate set methods for each data type and make sure to set them in the output archive. For example:

  • To send a primitive value, call one of the overloaded “set” methods for the appropriate type:

hr = output.SetNamed[Type]Value(L"outputInt", 1204);

  • To send a collection of primitive values, call the appropriate overloaded “set” methods:

hr = output.SetNamed[Type]Collection(L"outputCollection", bufPtr, numElements);

  • To send a collection of strings, call the string version of the collection’s “set” method:

hr = output.SetNamedStringCollection(fieldName, bufPtr, numElements);

In these four cases, noticed that they are all setting values to a fieldName but depending on the type of values being set, the appropriate method is used, each receiving the appropriate parameter types.

  • Note that in the case of collections, even though bufPtr is used in both cases, it’s not the same object. That is, for primitive values, bufPtr is a pointer to an array of primitive values whereas for a collection of strings, bufPtr is a double pointers to an array of strings.

How to use RPC in a Windows Universal App

RPC server provides a Windows Runtime component for Windows Universal Apps. The use of it is very similar to using the server in native code.

1. Create a top level object for the server

Javascript

if (enableRpc) {

var rpcServer = new Microsoft.Internal.GamesTest.Rpc.Server.RpcServer();

}

C#

#if ENABLE_RPC

var rpcServer = new Microsoft.Internal.GamesTest.Rpc.Server.RpcServer();

#endif

C++

#ifdef ENABLE_RPC

auto rpcServer = ref new Microsoft::Internal::GamesTest::Rpc::Server::RpcServer();

#endif

2. Define and implement your hooks. The signature of an RpcHook in the WinRT component is:

public delegate RpcHookCode RpcHook(RpcArchive^ input, RpcArchive^ output);

 

The following is the example Add hook described above:

Javascript

function add(input, output) {

var operand1 = input.getNamedWordValue("operand1");

var operand2 = input.getNamedWordValue("operand2");

var result = operand1 + operand2;

output.setNamedWordValue("result", result);

return 0;

}

C#

public int Add(RpcArchive input, RpcArchive output)

{

ushort operand1 = input.GetNamedWordValue("operand1");

ushort operand2 = input.GetNamedWordValue("operand2");

ushort result = (ushort)(operand1 + operand2);

output.SetNamedWordValue("result", result);

return 0;

}

C++

int Add(RpcArchive^ input, RpcArchive^ output)

{

unsigned short operand1 = input->GetNamedWordValue("operand1");

unsigned short operand2 = input->GetNamedWordValue("operand2");

unsigned short result = operand1 + operand2;

output->SetNamedWordValue("result", result);

return S_OK;

}

3. Register the hook and start the server. In your title’s initialization code, add the following lines of code.

Javascript

if (enableRpc) {

rpcServer.registerHookMethodAsync("Add", add);

rpcServer.start();

}

C#

#if ENABLE_RPC

rpcServer.RegisterHookMethod("Add", Add);

rpcServer.Start();

#endif

C++

#ifdef ENABLE_RPC

rpcServer3->RegisterHookMethod(L"Add", ref new RpcHook(Add));

rpcServer3->Start();

#endif

4. Somewhere in your game loop, call the ProcessPendingRequests method.

Javascript

// Somewhere in your game loop

if (enableRpc) {

rpcServer.processPendingRequestsAsync();

}

C#

#if ENABLE_RPC

rpcServer.ProcessPendingRequests();

#endif

C++

#ifdef ENABLE_RPC

rpcServer->ProcessPendingRequests();

#endif

Notes:

  • Unlike the native version, the WinRT version doesn’t have a flag that can be used to disable the inclusion of RPC classes. It is still recommended however that you wrap you RPC code in a conditional block that can be used to disable RPC in build configurations that you don’t want to include it in.
  • RPC provides Async versions of RegisterHookMethod and ProcessPendingRequests. JavaScript apps for Windows Store must use the Async versions of these methods because they spawn extra threads to avoid blocking the UI thread (this avoids crashes in JavaScript Apps). For other apps we recommend using the non-Async versions to prevent the creation of extra threads and gain control over which thread the hook code is executed on (the thread that called ProcessPendingRequests).
  • The WinRT RPC methods don’t return HResults. Instead, an exception will be thrown.
  • WinRT doesn’t support signed 8-bit integers, as such, the WinRT version of RPC server doesn’t include methods for getting or setting signed 8-bit integers or collections of signed 8-bit integers. If you are expecting a signed 8-bit integer, you can use the methods for larger signed integers like GetNamedShortValue and then cast the values as needed.
  • Client Networks (Client & Server) capability must be enabled in the application manifest, otherwise any attempt to start a listening server will result in Access Denied exception.

o Find the manifest file for your Windows Store App

clip_image005

o Double-click it, and check the box for Client Networks (Client & Server) capability.

clip_image006

  • Desktop clients attempting to connect to a Windows Universal App and call hooks can run on the same machine as the app only if using a loopback server (an application running on a different computer that listens on a specific port and re-sends everything it receives there back to sender on the same port).

    This is necessary because there is a security restriction for Store Apps which dictates that they cannot accept a connection from another program on the same machine (they can only connect to another program on the same machine if they are not the ones listening). Thus loopback server works around this issue by having a program on local machine send traffic to an application on a different machine which then sends it to the Store App. The Store App only sees remote traffic coming from the loopback server so the security requirement is satisfied. We used a custom-written loopback server for automated testing on the same machine. In real-world scenarios it is easier to simply run the Store App on a different machine.

Sending and Receiving data in a Windows Universal App

The WinRT version of RpcArchive contains the same get and set methods as native interfaces. Unlike the native server, the get and set methods are not overloaded by type. Instead, the type is included in the name of the method; this is to make the component work with Javascript which doesn’t support overloading based on the type of the parameters. For example, the native side contains the method:

float fieldValue;

archive.GetNamedValue(fieldName, &fieldValue);

The WinRT side of the same method looks like:

float fieldValue = archive.GetNamedFloatValue(fieldName);

Getting collection using the WinRT component looks similar:

float[] collectionValue = archive.GetNamedFloatCollection(fieldName);

Getting strings values follow the same pattern:

string fieldValue = archive.GetNamedStringValue(fieldName);

string[] fieldValue = archive.GetNamedStringCollection(fieldName);

The set methods follow the same pattern.

float fieldValue = 1.5;

archive.SetNamedFloatValue(fieldname, fieldValue);

float[] collectionValue = { 0.2, 1.5 };

archive.SetNamedFloatCollection(fieldname, collectionValue);

string stringValue = "fieldName";

archive.SetNamedStringValue(fieldname, stringValue);

string[] stringCollection = { "str1", "str2", "str3" }

archive.SetNamedStringValue(fieldname, stringCollection);

Notes:

  • Since the WinRT version of RPC uses managed strings and collections, users don’t need to pass in either pre-allocated buffers or the number of elements when getting or setting fields.

How to send and receive data on the client-side

An RPC hook accepts two RpcArchive objects, one as an input and the other as output. Sending and receiving data on the client-side is merely a matter of getting and setting them in the appropriate RpcArchive objects. The input archive contains data to be sent from the client to the server while the output archive is the response the server sends back to the client. To put data into the archive, you call the appropriate “set” methods, and to receive data, you call the appropriate “get” methods. For example, the following code snippet sets a string and is expecting a string in return:

RpcArchive inputArchive = new RpcArchive();

inputArchive.SetNamedValue("inputString", "incoming string value");

RpcArchive outputArchive = channel.InvokeRemoteMethod("EchoStrings", inputArchive);

string result = outputArchive.GetNamedValue<string>("result");

To send a collection of data, simply call the collection method for getting and setting these collection values. Making use of templates construct, we can create a method that would handle any data types that the server may returned:

private IEnumerable<T> SetAndGetNamedCollection<T>( string inFieldName, string outFieldName, IEnumerable<T> expectedCollection)

{

RpcArchive inputArchive = new RpcArchive();

inputArchive.SetNamedCollection(inFieldName, expectedCollection);

RpcArchive outputArchive = channel.InvokeRemoteMethod("EchoCollection", inputArchive); // send it across the wire

return = outputArchive.GetNamedCollection<T>(outFieldName);

}

In the sample code snippet above, we created a generic method to get and set collection values. The expectedCollection is an IEnumerable<T> and can be any data type that is either a collection of primitive values, or collection of strings. The data type in each collection must be of the same type.

Accessing the archive type information

The Fields member of the RpcArchive class can be used to enumerate the archive fields and retrieve the name and type of each field. This is useful for archives returned from server hooks as it’s the only way to determine what the archive contains without an implicit schema shared between the server and the client. Information about each field is provided in Name and DataType properties of the RpcArchiveField class:

RpcArchive inputArchive = new RpcArchive();

inputArchive.SetNamedValue("inputString", "incoming string value");

RpcArchive outputArchive = channel.InvokeRemoteMethod("EchoString", inputArchive);

foreach (RpcArchiveField field in outputArchive.Fields)

{

Console.WriteLine(field.Name);

Console.WriteLine(field.DataType.ToString());

}

Transferring large amounts of binary data with RPC

RPC provides limited support for transferring binary large objects (blobs) between client and server with the following constraints:

  • The blob must be small enough to be loaded into memory in its entirety without causing Out of Memory exceptions. This precludes from transferring crash dumps or entire game builds.
  • Because the data will be encoded with Base64 binary-to-text encoding scheme, the actual transfer size will be 37% larger on average. The blob and several of its encoded copies (created by intermediate steps of transforming bytes into JSON) must all fit into available memory.
  • Sending an RpcArchive containing a blob with InvokeRemoteMethod will block the calling thread for the duration required to send the entire blob over the network. RPC library users can work around this by wrapping the call into an asynchronous task.

To take advantage of this binary transfer support, insert RpcBinaryData objects representing blobs into an RpcArchive which is then sent using the InvokeRemoteMethod call. Note that using SetNamedCollection<byte> template overload will cause the byte array to be sent as a string of two-digit bytes separated by commas and spaces. This results in 300% size increase as opposed to 137% when using RpcBinaryData specifically tailored for sending blobs.

Insert a blob into an archive like this (client-side) to be sent using InvokeRemoteMethod:

byte[] blobContents = ReadBinaryFile();

archive.SetNamedValue<RpcBinaryData>(fieldName, new RpcBinaryData(blobContents));

Server-side, binary data can be sent inside of a hook like this:

unsigned int blobLength = 0;

std::shared_ptr<unsigned __int8> blobContents = ReadBinaryFile(&blobLength);

RpcBinaryData blob(blobContents, blobLength);

HRESULT hr = archive.SetNamedValue(fieldName, blob);

Alternative server-side examples for WinRT apps:

Javascript

var blobContents = readBinaryFile();

archive.SetNamedBlobValue(

fieldName,

new Microsoft.Internal.GamesTest.Rpc.Server.RpcBinaryData(blobContents));

C#

IBuffer blobContents = ReadBinaryFile();

archive.SetNamedBlobValue(

fieldName,

new RpcBinaryData(blobContents));

C++

IBuffer^ blobContents = ReadBinaryFile();

archive.SetNamedBlobValue(

fieldName,

ref new RpcBinaryData(blobContents));

One way to get the data out of an IBuffer in WinRT apps is using CryptographicBuffer.CopyToByteArray method (Windows.Security.Cryptography namespace). Likewise, the CreateFromByteArray method can be used to create an IBuffer from a byte array.

Receiving binary data on the client similarly involves the use of the RpcBinaryData class:

RpcBinaryData blob = archive.GetNamedValue<RpcBinaryData>(fieldName);

byte[] blobContents = blob.GetData();

Receiving binary data server-side looks similar:

RpcBinaryData blob;

HRESULT hr = archive.GetNamedValue(fieldName, &blob);

unsigned char* data = blob.GetData();

unsigned int length = blob.GetLength();

Alternative server-side examples for WinRT apps:

Javascript

var blob = archive.GetNamedBlobValue(fieldName);

var blobContents = blob.Data;

C#

RpcBinaryData blob = archive.GetNamedBlobValue(fieldName);

IBuffer blobContents = blob.Data;

C++

RpcBinaryData^ blob = archive.GetNamedBlobValue(fieldName);

IBuffer^ blobContents = blob.Data;

Creating RpcServer with a custom queue size

You can specify a maximum queue size for incoming and outgoing messages when creating an instance of RpcServer. This will allow you to know, through error handling, that RPC is correctly queuing requests. For example, when the queue is full, calling RaiseEvent on the server or InvokeRemoteMethod on the client will return GAMESTEST_RPC_E_PENDING_REQUESTS_QUEUE_FULL server error (translated to RpcRequestQueueFullException on the client).

To support this functionality, several RpcServer constructors that accept a queue size parameter are provided. Also, a DefaultMaxQueueSize static property has been added to RpcServer. This value is used to initialize the server whenever it is instantiated with a constructor that doesn’t accept a queue size.

The following table shows how you can construct the RpcServer instance with a specific queueSize:

Native C++

unsigned int queueSize = RpcServer::DefaultMaxQueueSize;

GUID guid = { 0x456f8f5c, 0xb90, 0x465b, { 0x8d, 0x4e, 0xd1, 0xc, 0xc8, 0xd6, 0x90, 0xc0 } }; 

pRpcServer = new RpcServer(queueSize);

pRpcServer = new RpcServer(port, queueSize); 

pRpcServer = new RpcServer(guid, queueSize); 

pRpcServer = new RpcServer(guid, port, queueSize);

 

Javascript

var queueSize = Microsoft.Internal.GamesTest.Rpc.Server.RpcServer.defaultMaxQueueSize;

var guid = "{456F8F5C-0B90-465B-8D4E-D10CC8D690C0}";

rpcServer = new Microsoft.Internal.GamesTest.Rpc.Server.RpcServer(

   guid, port, queueSize);

C#

uint queueSize = RpcServer.DefaultMaxQueueSize;

Guid guid = Guid.Parse("456F8F5C-0B90-465B-8D4E-D10CC8D690C0");

rpcServer = new RpcServer(guid, port, queueSize);

C++

unsigned int queueSize = RpcServer::DefaultMaxQueueSize;

GUID guid = { 0x456f8f5c, 0xb90, 0x465b, { 0x8d, 0x4e, 0xd1, 0xc, 0xc8, 0xd6, 0x90, 0xc0 } };

rpcServer = ref new RpcServer(guid, port, queueSize);

NOTES:

  • The RtServer has limited support for constructor overloads so callers cannot construct RpcServer with just the queueSize specified. Pass in default values for any parameters that you don’t intend to specify (e.g.: use defaultPort and defaultServerId members of Microsoft.Internal.GamesTest.Rpc.Server.RpcServer for port number and server ID).
  • The DefaultMaxQueueSize has been chosen to work in most situations; however, you are free to define your own max queue size that best meets your specific needs.

Enumerating hooks registered on the server

You can find out which hooks are registered on the server by using the GetRemoteMethods method of the RpcChannel. This returns an enumerable collection of RpcRemoteMethod objects, each one containing information about a registered hook. Currently, only the Name property is supported to query the names of registered hooks.

The collection returned by GetRemoteMethods can be easily searched by Linq or transformed into a string array:

if (channel.GetRemoteMethods().Where(

m => m.Name == "MyMethod").SingleOrDefault() == null)

{

// Method not found!

}

string[] methodNames = channel.GetRemoteMethods().Select(m => m.Name).ToArray();

The server is queried over the network to enumerate the hooks each time GetRemoteMethods() is called, so applications may wish to limit how often they call this API and cache results.

How to update a hook

While you are free to update any aspect of your hook, understand that updating a hook will require you to redeploy your server. This may also cause version mismatch between your server code and previous versions of your client code. For more information on versioning, see the Versioning best practices section.

To update a hook, do the following.

1. Find the method in your server code where the hook is implemented.

2. Make your changes to the method.

3. Compile and run your title.

4. Update the client code to reflect your changes so your code are in sync.

Versioning best practices

Versioning is a complex challenge that can get out of hand relatively quickly if you do not have a strategy to handle the different versions of your title. As it relates to RPC, we see two primary versioning challenges:

1. New server code with old client code

2. Old server code with new client code

Any time you make a change to your hook, you must also make similar updates to your client code. When the server and client are out of sync, you need to ensure that your code still functions in a safe manner. We suggests the following list of things (a.k.a.: best practices) you should consider when updating your hooks.

Description

Server side

Client side

If you want to change RpcArchive members

Create new hook

Call the newly created hook and pass in data using the new types.

If you want to change the name of members in the RpcArchive

In the implementation of the hook use the RpcArchive::HasNamedValue method to check for the existence of the parameter with the new name and the parameter with the old name.

Populate the outgoing RpcArchive using the new names.

If you want to remove RpcArchive members

In the implementation of the hook, remove code that tries to retrieve the unused parameter from the RpcArchive.

Remove the code that places the values into the outgoing RpcArchive.

If you want to change the implementation of a hook and have old clients calling the old implementation and new clients calling the new implementation

1. Create a new hook.

2. Create a new RpcServer object using a new GUID.

3. Register the new implementation with the server using the new GUID. Continue to register the old implementation with the previous server.

4. Register all other hooks with both servers.

See example below.

1. Instantiate the RpcChannel using the new GUID.

2. Call the new method the same way as any other method.

If you want to change the name of a hook

Create a new hook with a new name. Change the implementation of the old hook to return an error code indicating it has been deprecated.

Call the hook using the new name.

If you want to remove a hook

Change the implementation to indicate the hook has been deprecated.

Remove calls to the hook.

Changing the implementation of a hook

If you want to change the implementation of a hook and have old clients calling the old implementation and new clients calling the new implementation than consider using the following pattern.

Assume we have a method named “Add” that is responsible for adding two integers and returning their result.

RpcHookCode Add(RpcArchive& input, RpcArchive& output)

{

__int16 operand1, operand2;

input.GetNamedValue(L"operand1", &operand1);

input.GetNamedValue(L"operand2", &operand2);

__int16 result = operand1 + operand2 + operand2;

output.SetNamedValue(L"result", result);

return S_OK;

}

RpcServer* s1 = new RpcServer();

s1->RegisterHookMethod(L”Add”, ::Add);

We can see that we have a bug in the code above because operand2 is being included in the sum twice. However, it is possible that some older clients are relying on this behavior and we don’t want to break them. The solution is to create another method with the correct implementation and register it with a different RpcServer that uses a different server ID.

RpcHookCode AddCorrectly(RpcArchive& input, RpcArchive& output)

{

__int16 operand1, operand2;

input.GetNamedValue(L"operand1", &operand1);

input.GetNamedValue(L"operand2", &operand2);

__int16 result = operand1 + operand2;

output.SetNamedValue(L"result", result);

return S_OK;

}

RpcServer* s1 = new RpcServer();

s1->RegisterHookMethod(L”Add”, ::Add);

static const GUID secondServerId = { 0x25bd0a4d, 0x9532, 0x4f9c, { 0xab, 0x1, 0x8f, 0xe, 0xa9, 0xc5, 0x7c, 0x88 } };

RpcServer* s2 = new RpcServer(secondServerId);

s2->RegisterHookMethod(L”Add”, ::AddCorrectly);

Now both implementations can live side-by-side on the server-side.

On the client-side, simply instantiate your RpcChannel object using the second server ID.

RpcChannel c1 = new RpcChannel(Guid.Parse(“{25BD0A4D-9532-4F9C-AB01-8F0EA9C57C88}”);

c1.InvokeRemoteMethod(“Add”, inputArchive);

The RpcServer and RpcChannel constructors that accept server GUID also accept a port number.

Note:

  • To create the second server, you must provide it a new GUID. You can create your own GUID or you can use the Create GUID tool in VS to generate one for you.

How to work with events

TDK RPC supports event handling where multiple clients can subscribe to events on the server. Once the server raises and processes events, all the subscribed clients will get notified. The event driven process works something like this:

1. A client subscribes to an event. The client must connect to the server for the subscription to take effect and to start receiving event notifications.

2. The server raises events as they occur. At some point in time, the server processes the events and clients listening to processed events get notified.

3. The client handles event notification in the delegate method it specified when subscribing to an event. The delegate methods are called on a thread different than main.

The event notification process remains active as long as the client is connected to the server. There are several cases that may affect the event handling process:

1. The client can subscribe to as many events as it wants even before making a connection to the server.

2. The client must make a connection to the server for event subscriptions to take effect.

3. If the connection is lost or if the client disconnects from the server, the client can no longer receive notifications from subscribed events. However, their subscriptions are still valid. In the case the client reconnects again, it does not need to re-subscribe to those events. In addition, reconnecting does not allow the client to retrieve event notifications that occurs while the client was disconnected.

4. When a client no longer wants to receive notification from an event it subscribed to, it can unsubscribe to that event.

5. When the last client unsubscribes to an event, that event will be removed from the list of notified events on the server.

6. When the server processes an event, if there are no clients listening to that event, the notification is canceled.

How to subscribe and unsubscribe to an event from the client-side

Event handling is done through the RpcChannel object. To subscribe to an event, you need an event handler that matches the signature of EventHandler<RpcEventArgs> and an event name to subscribe to. The EventHandler<RpcEventArgs> method is the method that gets called when the event is raised. Once the subscription is completed successfully, it returns a GUID associated with this subscription. Use this GUID when you want to unsubscribe from the event later.

The following code snippet shows how to subscribe and unsubscribe from an event on the client-side:

// Subscribe to an event

using (RpcChannel channel = new RpcChannel(IPAddress.Parse("10.124.150.208")))

{

EventHandler<RpcEventArgs> eventHandler = this.RunRpcEventHandler;

Guid id = channel.Subscribe(eventName, eventHandler);

channel.Connect();

...

channel.Unsubscribe(id);

...

channel.Disconnect();

}

...

// Signature of the event delegate.

// This method is called when events are processed.

// eventArgs contains RpcArchive data pass back from the server

private void RunRpcEventHandler(object sender, RpcEventArgs eventArgs)

{

// Handle the event by processing the information in the eventArgs parameter

}

How to send event notifications from the server-side

As events occurred on the server, you can raise them using one line of code:

HRESULT hr = server.RaiseEvent(eventName, archive);

This allows you to raise an event at any time. You will need to know which event to raise and package the event data into the RpcArchive object. Event data can be any data type that the RpcArchive supports (e.g.: Primitive, String, and Collection). This archive will be sent back to the client when events are processed. At some point in time, the server must call ProcessPendingRequests to send these events to the client:

server.ProcessPendingRequests();

Sending and Receiving User Defined Types

It is possible for users of TDK RPC to exchange their own custom types between the client and the server, however the process is a little bit different in the native code than the managed code.

Note: TDK RPC does not currently support 64 bit integers in user-defined types. Users that wish to send and receive 64 bit integers are encouraged to use strings instead.

Managed code

The requirements for sending and receiving a custom managed type using TDK RPC is very similar to doing the same thing with WCF. All managed types that are to be exchanged between the client and server need to be decorated with the DataContract attribute. Additionally, each property needs to be decorated with the DataMember attribute. Consider this sample class:

[DataContract]

class Player

{

[DataMember(Name = "health")]

public float Health { get; set; }

[DataMember(Name = "name")]

public string Name { get; set; }

[DataMember(Name = "score")]

public int Score { get; set; }

}

Sending an instance of this class to the native side is as simple as adding it to an RpcArchive.

Player p = new Player();

p.Health = 25.2f;

p.Name = "Frankenstein";

p.Score = 78;

RpcArchive inputParameters = new RpcArchive();

inputParameters.SetNamedValue("playerData", p);

channel.InvokeRemoteMethod("SetPlayer", inputParameters);

Retrieving an instance of the class that had been sent by the native side can be done in two ways. If the entire value returned by the server represents an instance of the Player class then you can convert the incoming RpcArchive using the ToType<T> method like this:

RpcArchive empty = new RpcArchive();

RpcArchive returnedArchive = channel.InvokeRemoteMethod("GetPlayer", empty);

Player returnedPlayer = returnedArchive.ToType<Player>();

Alternatively, if the Player instance is an element within an RpcArchive it can be retrieved using the GetNamedValue method. Suppose that the Player object of interest is stored in the archive with the name playerData. It can be retrieved like this:

RpcArchive returnedArchive = channel.InvokeRemoteMethod("GetPlayerInsideArchive", new RpcArchive());

Player returnedPlayer = returnedArchive.GetNamedValue<Player>("playerData");

Nested objects are supported as well. Consider another class like this:

[DataContract]

class Weapon

{

[DataMember(Name = "damage")]

public float Damage { get; set; }

}

Including an instance of the Weapon class within the Player class is as simple as adding a field. None of the other code above changes.

[DataContract]

class Player

{

[DataMember(Name = "health")]

public float Health { get; set; }

[DataMember(Name = "name")]

public string Name { get; set; }

[DataMember(Name = "score")]

public int Score { get; set; }

[DataMember(Name = "selectedWeapon")]

public Weapon SelectedWeapon { get; set; }

}

Native code

If users would like to send an instance of their custom type they first need to convert their object into an instance of RpcArchive. Typically, this can be done by storing each of the fields of interest into an RpcArchive. Let’s look at this class as an example:

class Player

{
public:
float health;
wchar_t* name;
int score;
};

One approach might be to include an implicit cast operator that can turn an object of type Player directly into an RpcArchive. Using this approach, the Player class would now look like this:

class Player

{

public:

float health;

wchar_t* name;

int score;

operator RpcArchive()

{

RpcArchive output;

output.SetNamedValue(L"health", health);

output.SetNamedStringValue(L"name", name);

output.SetNamedValue(L"score", score);

return output;

}

};

Using this new class we can easily return a Player object from a hook method like this:

HRESULT GetPlayer(RpcArchive& input, RpcArchive& output)

{

Player p;

p.health = 5.0f;

p.score = 4000;

p.name = L"Dracula";

output = p;

return S_OK;

}

TDK RPC also supports nesting types within types, so long as all types can be converted to an RpcArchive. Let’s assume we have another class that looks like this:

class Weapon

{

public:

float damage;

operator RpcArchive()

{

RpcArchive weaponData;

weaponData.SetNamedValue(L"damage", damage);

return weaponData;

};

};

And we add a field to a Player class to indicate their currently selected Weapon like this:

class Player

{

public:

float health;

wchar_t* name;

int score;

Weapon selectedWeapon;

operator RpcArchive()

{

RpcArchive output;

output.SetNamedValue(L"health", health);

output.SetNamedStringValue(L"name", name);

output.SetNamedValue(L"score", score);

output.SetNamedValue(L"selectedWeapon", selectedWeapon);

return output;

};

};

Notice that we simply added one more line to the cast operator to include the Weapon instance. When that line executes then the RpcArchive cast operator will be called on selectedWeapon and it will be converted into an RpcArchive and placed into the output archive.

The process of receiving an instance of the Player class would follow a similar pattern, just in reverse. A conversion constructor can be defined that accepts an RpcArchive as its sole parameter. For example, the Player and Weapon classes can be further modified as such:

class Weapon

{

public:

float damage;

Weapon()

{

damage = 0.0f;

}

Weapon(const RpcArchive& weaponData)

{

weaponData.GetNamedValue(L"damage", &damage);

}

operator RpcArchive()

{

RpcArchive weaponData;

weaponData.SetNamedValue(L"damage", damage);

return weaponData;

}

};

class Player

{

public:

float health;

wchar_t* name;

int score;

Weapon selectedWeapon;

Player()

{

health = 0.0f;

name = nullptr;

score = 0;

}

Player(const RpcArchive& playerData)

{

playerData.GetNamedValue(L"health", &health);

unsigned long nameLength = 0;

playerData.MeasureNamedStringValue(L"name", &nameLength);

name = new wchar_t[nameLength];

playerData.GetNamedStringValue(L"name", name, nameLength, NULL);

playerData.GetNamedValue(L"score", &score);

RpcArchive weaponData;

playerData.GetNamedValue(L"selectedWeapon", &weaponData);

selectedWeapon = Weapon(weaponData);

}

operator RpcArchive()

{

RpcArchive output;

output.SetNamedValue(L"health", health);

output.SetNamedStringValue(L"name", name);

output.SetNamedValue(L"score", score);

output.SetNamedValue(L"selectedWeapon", selectedWeapon);

return output;

}

~Player()

{

if(name != nullptr)

{

delete[] name;

}

}

};

With the conversion constructors, writing a hook that accepts a Player object as input looks like this:

RpcHookCode SetPlayer(RpcArchive& input, RpcArchive& output)

{

RpcArchive playerArchive;

input.GetNamedValue(L"playerData", &playerArchive);

Player p(playerArchive);

// Modify in-game values ...

p.score += 100;

p.health -= 25.2f;

output = p;

return S_OK;

}

Auto-generating native code

The biggest drawback to the native-side process of sending custom types is the need to create and maintain all of the serialization and deserialization code. The process is time-consuming, tedious and error prone. One possible solution is to use the preprocessor to generate the necessary code. Using a technique called X-Macros can greatly simplify the process. Defining the original Player class from above along with its members, implicit cast operator and conversion constructor could be done as simply as this:

#define DEFINE_PLAYER_CLASS(primitive, string) \

primitive(int, score) \

primitive(float, health) \

string(name)

CREATE_RPC_CLASS(Player, DEFINE_PLAYER_CLASS)

The tricky part is the definition of the CREATE_RPC_CLASS macro and its dependencies. Presented here is just one possible solution to be considered. If you wish to utilize this approach, it is highly recommended that you examine the macros and ensure fitness for your application.

#define STRUCT_MEMBER(type, member) type member;

#define STRUCT_MEMBER_STRING(member) wchar_t* member;

#define SERIALIZE_MEMBER(type, member) \

archive.SetNamedValue(L#member, member);

#define SERIALIZE_MEMBER_STRING(member) \

archive.SetNamedStringValue(L#member, member);

#define DESERIALIZE_MEMBER(type, member) \

archive.GetNamedValue(L#member, &member);

#define DESERIALIZE_MEMBER_STRING(member) \

unsigned long member##StringLength = 0; \

archive.MeasureNamedStringValue(L#member, &member##StringLength); \

this->member = new wchar_t[member##StringLength]; \

archive.GetNamedStringValue(L#member, member, member##StringLength, NULL);

#define CREATE_RPC_CLASS(className, expansionDefinition) class className \

{ \

public: \

className(){} \

RpcArchive archive; \

expansionDefinition(STRUCT_MEMBER, STRUCT_MEMBER_STRING) \

{ \

operator RpcArchive() \

expansionDefinition(SERIALIZE_MEMBER, SERIALIZE_MEMBER_STRING) \

return archive; \

} \

\

className(RpcArchive& archive) \

{ \

expansionDefinition(DESERIALIZE_MEMBER, DESERIALIZE_MEMBER_STRING) \

} \

};

Here’s a tip. If you’re working with macros and you want to see their output after expansion, you can tell Visual Studio to write the output of the pre-processed file to disk and skip compiling the results. Simply right-click the desired .cpp file in Visual Studio and choose Properties. Under C/C++ -> Preprocessor, look for “Preprocess to a File” and set its value to “Yes”.

clip_image008

Now the preprocessed file will be written to the intermediate directory (where the .obj files go) and will have a file name extension of “.i”. Open the “.i” file in your text editor of choice.

Throwing custom exceptions

It is possible for users of TDK RPC to throw their own custom exceptions between the server and the client. The principle behind this is simple: Both the server and the client agrees on the same set of error codes. On the server side, an error code is returned by the hook. On the client side, it checks for the return code and throws the exception accordingly. The sections below will walk you through the process for setting it up in your test tool so that you can throw custom exceptions in your hooks.

Native code

To throw custom exceptions in your test tool, on the server side, define the custom return code. Then in the hooks, return one of these custom codes as appropriate.

To define custom return code, place the following #define statements somewhere in your game’s code:

// Hook return code

#define PLAYER_NOT_FOUND MAKE_HOOK_ERROR(10)

#define LEVEL_NOT_LOADED MAKE_HOOK_ERROR(11)

To return a custom return code from a hook, consider the following example hook for the PLAYER_NOT_FOUND:

RpcHookCode GetPlayerId(RpcArchive& input, RpcArchive& output)

{

Actor* player = g_HeroSystem.GetDefaultLocalHero();

if (!player)

{

return PLAYER_NOT_FOUND;

}

int uid = player->GetUid();

output.SetNamedValue(L"UID", uid);

return S_OK;

}

Notes:

  • The custom error code uses an unsigned 16-bit int and starts at 0 to about 65,535.
  • For any hook methods in existing code with return types of HRESULT, they should now be returning an RpcHookCode instead. If the server is returning an RpcHookCode but the client is receiving it into an HRESULT, the error code will resolved to an unknown exception.

Managed code

From the client side, to handle custom return codes from a hook, you must provide an implementation of the IExceptionFactory interface. The CreateExceptionForErrorCode method is responsible for translating an error-code received from the server into an Exception to be thrown by the client. If no such mapping exists, or the server-side code represents SUCCESS, then this method can return null. If an exception is thrown, it is recommended that it inherit from RpcException.

Here is an example of such an exception factory class:

using ExceptionCreator = System.Func<string, RpcArchive, RpcArchive, System.Exception>;

public class CustomExceptionFactory : IExceptionFactory

{

private Dictionary<RpcHookCode, ExceptionCreator> codesToExceptions;

 

// Defining the error code that matches those from the server side

public CustomExceptionFactory()

{

codesToExceptions = new Dictionary<RpcHookCode, ExceptionCreator>();

codesToExceptions.Add(new RpcHookCode(10), (method, input, output) =>

{

return new PlayerNotFoundException(method);

});

codesToExceptions.Add(new RpcHookCode(11), (method, input, output) =>

{

return new LevelNotLoadedException(input, output);

});

}

// Mapping the error code to exception methods

public Exception CreateExceptionForErrorCode(string methodName, RpcHookCode errorCode, RpcArchive input, RpcArchive output)

{

ExceptionCreator exceptionCreator;

if (codesToExceptions.TryGetValue(errorCode, out exceptionCreator))

{

throw exceptionCreator(methodName, input, output);

}

return null;

}

}

To catch an RpcException from a hook, you must register the exception factory with the channel object then you can catch RpcException as you would standard exceptions. Take the following hook invocation code for instance:

using (RpcChannel rpcChannel = this.CreateRpcChannel())

{

rpcChannel.RegisterExceptionFactory(new CustomExceptionFactory());

try

{

RpcArchive response = rpcChannel.InvokeRemoteMethod("GetPlayerId", new RpcArchive());

int playerId = response.GetNamedValue<int>("UID");

// Do something with the playerId ...

}

catch (PlayerNotFoundException)

{

// Your custom error messages here

}

}

Remarks

  • For more information about the classes referenced in this doc, see the RPC API reference in RPC.chm.
  • Nearly all methods in the native-code portion of RPC return an HRESULT to indicate success or failure.  It is highly recommended that you catch these exceptions so you can handle errors gracefully.  Custom error codes defined by the RPC library can be found in RpcErrorCodes.h. Also see Throwing custom exceptions section for more details.

See also

XboxConsole – Getting Started Guide

Last edited Sep 26, 2015 at 1:03 AM by amccalib, version 8