C# Scripting Sample
Description
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 'state-less' 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.
|
Path
Source: |
Samples\Managed\\Direct3D\Scripting |
Executable: |
Samples\Managed\\Direct3D\bin\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:
- Script #1 - A simple state-less script that makes the character move slowly
around the scene while rotating.
- Script #2 - A more advanced script that maintains state, and uses this information
to deteremine which action to perform. The character can bounce off walls, and the rotation
will change when it does so.
- Bad Guy Hacker Script - A script that is designed to emulate a "Bad Guy Hacker" trying to
delete files from your computer. Due to the security settings set on the scripts that are allowed
to run, this will (of course) not be allowed.
How Does the Sample Work?
This sample uses the most direct technique possible to compile the C# code into
scripts on the fly. It started with the ShadowVolume C++ SDK Sample, and modified it 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 (see below)
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 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 script's must implement) is loaded and
stored, since 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:
- Separate mixed mode assembly that contains both the Script Engine code (like this example has) as
well as an unmanaged interopability layer that your unmanaged code will call.
- Separate purely managed assembly (for example a C# assembly) that contains the ScriptEngine code
where the classes are marked as
[ComImport]. This method requires much more 'setup' time, since the managed assembly will need
to be registered for COM use via the
RegAsm tool.
- Require the script code themselves to implement the
[ComImport] attribute. This is definitely the worst of the scenarios.
Copyright (c) Microsoft Corporation. All rights reserved.
|