C# Scripting Direct3D Sample


The sample demonstrates one possible technique for using C# code as a 'script' in your unmanaged applications, and includes techniques to ensure scripts that are not deemed to be 'safe' cannot be executed. The sample includes three scripts, one simple 'stateless' script, one more advanced script which maintains state and reacts to the environment, and a final script that is emulating a 'bad' script that attempts to delete files from your hard drive.

Scripting sample

Supported Languages

Path

Source: (SDK root)\Samples\Managed\Direct3D\Scripting
Executable: (SDK root)\Samples\Managed\Direct3D\Bin\x86\Scripting.exe

Sample Overview

Having a robust scripting engine in your game titles can open up new avenues for development of your title. Many different aspects of the game could be controlled by these 'scripts' (although given these will actually be compiled and executed, 'script' is not entirely accurate). You could have your camera scripted for a 'cut scene' inside the game, game AI, or any other number of manipulations of your game environment. One of the more exciting prospects would be to have user added content (for example custom added units) that would use the scripts to have custom actions, and react to the environment appropriately.

This particular sample comes with three different types of scripts, each with a unique characteristic:

How the Sample Works

This sample uses the most direct technique possible to compile the C# code into scripts on the fly. It starts with the ShadowVolume C++ software development kit (SDK) Sample, and is modified to allow the character in the scene to be controllable by C# scripts. First, the sample was updated to be a mixed mode assembly by adding the /CLR compiler switch. This allows you to have your managed code that will control the scripting engine to be embedded directly into your application.

You'll notice that the unmanaged code file of the sample also includes a new #pragma unmanaged directive to let the compiler know that this code will only be unmanaged code. At startup the security policy is set and then while running, the user is allowed to pick one of the three scripts that ship with the SDK (or choose no scripts at all).

When one of the scripts is selected from the user interface, the work for compiling and loading the script's code happens. An instance of the CSharpCodeProvider Leave Site class is created, and the raw code of the script is fed into the class to be compiled. The assembly is compiled and stored in the temporary files where it is then loaded into the application. Once the assembly has been loaded, the main class of the script (which all scripts must implement) is loaded and stored; this will be used later to call the methods from the script.

Note: For this sample, any compile errors from one of the scripts will be ignored and the character will simply behave as if there are no scripts running.

Once a script has been compiled and loaded, a series of four potential methods will be called (depending on if the script implements them). The character's rotation (on each axis) can be modified, as well as the character's position.

Security Considerations

Allowing unknown code to run within your game engine can be a scary prospect to say the least. What would stop a script writer from deciding to write a script that emailed all of your important documents to everyone in your global address book, then formatted your hard drive? C# as a language is extremely powerful and could allow you to do either of these things in a fully trusted environment.

Luckily, managed code has something called Code Access Security, which was designed specifically for situations such as this. By default, any code running on the local machine (which most client applications will be) is granted Full Trust, which basically means it is allowed to do anything the language is capable of doing (like format the hard drive). To ensure these unsafe things aren't allowed the Application Domain's security policy is updated. During startup, the sample calls the Initialize() method on the script engine, which contains the following code snippet:

// Create a new, empty permission set so we don't mistakenly grant some permission we don't want
PermissionSet* pPermissions = new PermissionSet(PermissionState::None);

// Set the permissions that you will allow, in this case we only want to allow execution of code
pPermissions->AddPermission(new SecurityPermission(SecurityPermissionFlag::Execution));
                                                                
// Make sure we have the permissions currently
pPermissions->Demand();

// Create the security policy level for this application domain
PolicyLevel* pSecurityLevel = PolicyLevel::CreateAppDomainLevel();

// Give the policy level's root code group a new policy statement based
// on the new permission set.
pSecurityLevel->RootCodeGroup->PolicyStatement = new PolicyStatement(pPermissions);

// Update the application domain's policy now
AppDomain::CurrentDomain->SetAppDomainPolicy(pSecurityLevel);

The code above first creates a PermissionSet, using the PermissionState::None permission state, which is essentially saying 'Do not allow this code to do anything'. Obviously, this is a little too restrictive, since at the very least, you will want the code that is running the scripts to be executed. Given that, you can add a new permission to this list that you will allow, and as you see in the code snippet, the SecurityPermissionFlag::Execution is added (there are quite a few different security permissions that can be set in this manner). After the PolicyLevel is created, and the RootCodeGroup is updated, the application domain's security policy is updated. Any assemblies loaded after this point (such as the scripts later) will have this security policy enforced. Notice when you're running the sample, the final script fails to run due to the security violations.

Performance Considerations

In this sample, you'll notice that the InvokeMember() method is called in order to execute the script methods. While this method is quick to implement and works well, this technique also relies on Reflection to make this call, which can be an order of magnitude slower than simply calling the method directly. In the scenario where you are making one (or more) calls per frame as this sample is doing, you will probably want to implement a more efficient way to call these methods.

One of the easier implementations that has much better performance than this technique is by defining an interface that the scripts must implement. You would put this interface into a managed assembly which you would then add a reference to in your application. When compiling the script, you would get the Interface type you declared in the managed assembly and call the methods on that directly. This would remove the cost of the InvokeMember() method, and instead allow you to call into the compiled scripts directly.

Implementation Considerations

It might not be possible (or practical) for your application to be compiled with the /CLR switch to be compiled as a mixed mode assembly (one that contains both managed and unmanaged code) like this sample does. There are numerous other ways you could implement techniques similar to what this sample does such as:


Send comments about this topic to Microsoft. © Microsoft Corporation. All rights reserved.

Feedback? Please provide us with your comments on this topic.
For more help, visit the DirectX Developer Center