Share via


Scripting and .NET: Visual Studio for Applications Provides Customized Scripting Facilities for Your .NET Project

MSDN Magazine

Scripting and .NET

Visual Studio for Applications Provides Customized Scripting Facilities for Your .NET Project

Chris Sells

This article assumes you're familiar with Visual Studio .NET and JScript+

Level of Difficulty     1   2   3 

Download the code for this article: VisualStudioforApplications.exe (55KB)

SUMMARY In the past, the Microsoft Active Scripting architecture has allowed you to make your applications extensible. But it came with some drawbacks. You couldn't call into DLLs; you had to use COM interfaces. The scripts were not compiled, so they ran more slowly than they otherwise might, and a number of objects had to ride along with the script. To solve these problems and make extensibility easier to attain, Visual Studio for Applications was introduced. Presented here is an introduction to VSA that covers language support, types, events, and much more.

Extensibility matters. For example, once every year or so, I play DOOM again, front to back. And not just DOOM, but DOOM 2, and Ultimate DOOM as well. Every time I play it, I marvel at the fact that I can run it under Windows NT® even though it was developed for MS-DOS®. Of course, the MS-DOS version hasn't run on my machine since I switched from Windows® 9x to Windows NT. Instead, I'm using a front end called WinDOOM that runs the original id code in the protected Windows NT environment. The ability of someone to write WinDOOM and the hundreds of other add-ons that fans have contributed to DOOM (and Quake) over the years put id software at the top of their particular heap (according to legend, at its peak DOOM was the second most commonly installed software for MS-DOS behind MS-DOS itself). And all of that was due to the extensibility hooks that DOOM provided.
      When id provided extensibility hooks into their software, they did it in proprietary ways that varied from version to version. Each new version required learning new libraries and/or languages. Id could get away with this because most of their users were teenagers with plenty of time on their hands. My guess is that you're not so lucky. Your customers are likely real people with real lives that need to use your software to get real work done. On the other hand, you probably don't get e-mail from your customers that contains phrases like "d00d ur $oftwarz roolz!," so count your blessings.
      To make your application extensible, the model should be simple but flexible. It should use familiar programming techniques, languages, and libraries. It should integrate closely into your application. And finally, it should have a minimum of overhead and a maximum of efficiency.
      The Microsoft® Active Scripting architecture was a COM-based solution that met many of these criteria, but it wasn't perfect. For example, while the list of COM components that work with script is long, the library of standard components to make scripting easy and flexible was very small. In order to compensate for this, each script would need access to a variety of objects that needed to be deployed with the script itself and registered before use. And scripts were not allowed access to DLLs or the Win32® API directly, requiring even more COM components. In addition because script code was interpreted these scripts ran much more slowly than compiled components.
      The Microsoft .NET platform has a set of classes that offers the benefits of Active Scripting without the drawbacks. This set of classes, named Visual Studio® for Applications (VSA), provides a pluggable IDE and debugger for Visual Basic® .NET.
      But support for a hosted design-time environment is only half of the VSA story. The other half is the runtime. It doesn't come with an IDE or a debugger, but it is freely available with the standard .NET platform and has been engineered as the .NET replacement for the COM-based Active Scripting architecture.

The Sample

      To demonstrate the VSA runtime, I've built a sample called ClockBox, which allows the user to keep track of multiple time zones (see Figure 1). The main window is merely a frame (a box, if you will) of MDI child windows. Creating a new window is simply a matter of choosing File | Open ClockBox Script. The File| Open dialog allows the user to choose a JScript® or Visual Basic file to serve as the driver for a clock shown in an MDI child window.

Figure 1 ClockBox
Figure 1 ClockBox

      The script file may use any of the objects from the hosting application or types from the .NET base class libraries (BCL) to affect the caption of its window and the actual time shown. For example, Figure 2 shows the code needed to implement the Pacific Time window. The rest of this article shows how to make these kinds of scripts possible for the ClockBox sample and for your apps.

Engines and Sites

      The VSA runtime architecture is shown in Figure 3. Your VSA host application provides a site that implements a callback interface. This interface is used to communicate state changes in the scripting engines and for the scripting engines to request information from the site. The scripting host loads VSA engines to compile and run your users' code. The code is compiled to a native .NET assembly and executes as native code, pulling in external types and objects as well as intrinsic types and objects provided by the host. As interesting things happen in the host, events are fired that the script can handle as it chooses. Eventually, the host tires and returns to rest in its hard drive home, turning off the script engines before it goes.

Figure 3 VSA Runtime Architecture
Figure 3 VSA Runtime Architecture

      To begin scripting, you first need to determine which engine to load. That's completely up to the hosting application. The ClockBox sample uses the extension, either .js or .vb, to determine which engine to load. ASP.NET, on the other hand, uses the language attribute in the @page directive to determine in which language the page is written. Your application may decide to support only one VSA engine. Whatever the case, as of right now your application has only two choices: Visual Basic .NET and JScript .NET. Both of these languages are exactly the same ones you would use if you were programming in Visual Studio .NET or compiling from the command line via vbc.exe or jsc.exe, which means that anyone who knows Visual Basic .NET or JScript already knows the scripting language your application is exposing.

Late Binding

      You may have noticed that C# is not part of the list of languages supported in VSA, in spite of its popularity among .NET programmers. C# as it stands today is not particularly well-suited for scripting. It's a bit too complex and simplicity is important for a scripting language. One feature that makes Visual Basic .NET and JScript .NET desirable as scripting languages is their support for typeless programming via late binding. For example, if you had a plain old Object reference in C# and no compile-time type information, you'd have to use reflection to program against it.
      On the other hand, in JScript or Visual Basic the language allows you to make method calls against the object directly, in spite of the fact that the type is not known at compile-time. This is shown here in Visual Basic .NET:

  ' From callrand.vb: Call System.Random.Next(0, 100)
  

Dim type As Type = Type.GetType("System.Random")
Dim obj = Activator.CreateInstance(Type)

' Access members without explicit reflection
Dim ret = obj.Next(0, 100)
Console.WriteLine(ret)

And here's an example in JScript .NET:

  // From callrand.js: Call System.Random.Next(0, 100)
  
import System;

var type : Type = Type.GetType("System.Random");
var obj = Activator.CreateInstance(type);

// Access members without explicit reflection
var ret = obj.Next(0, 100);
Console.WriteLine(ret);

      Of course, unlike the COM Active Scripting engines, VSA supports typed programming, so C# support could be added later.

.NET Scripting

      A scripting engine is a .NET component that implements the IVsaEngine interface, as defined in the Microsoft.Vsa assembly and namespace (see Figure 4). Loading a scripting engine is a matter of picking the type you'd like and initializing it (see Figure 5).
      In addition to creating an instance of the appropriate engine, I'm setting a root moniker, setting the site, initializing the engine, and setting a root namespace. The root moniker is a unique name that's passed back to the host occasionally to uniquely identify that engine in case the host has loaded more than one. It must be in the form protocol://path, where the protocol is not a known protocol like HTTP or gopher and is unique to the host. Likewise, the moniker itself must be unique as a whole for each host. Other than that, you can put anything you like in the moniker name.
      The site interface is implemented by the host and is called when something interesting happens in the script engine:

  public interface Microsoft.Vsa.IVsaSite {
  
void GetCompiledState(out Byte[] pe, out Byte[] debugInfo);
object GetEventSourceInstance(string itemName, string
eventSourceName);
object GetGlobalInstance(string name);
void Notify(string notify, object info);
bool OnCompilerError(IVsaError error);
}

The first method you'll implement is OnCompilerError because that's how you'll notify your users of syntax errors:

  class ScriptableClock : IVsaSite {
  
•••
bool IVsaSite.OnCompilerError(IVsaError e) {
MessageBox.Show(string.Format("Error of severity {0}
on line {1}: {2}", e.Severity, e.Line, e.Description));
return true; // Continue to report errors
}
}

      The root namespace sets the namespace that all types created by the engine will be nested into. When you feed a VSA engine a script, it will compile it into an in-memory assembly that you'll have full access to, including types created by the script itself. To access those types, you'll need to use the namespace you specified when creating the engine to qualify the type names you're after.

Code, References, and Global Objects

      Once you've created and initialized the engine, you're ready to execute some script. First, add an item to the engine. There are three kinds of items to add: code, assembly references, and global objects. To get going, you'll need to create a single code item with the source code of the script, like so:

  IVsaItems    items = _engine.Items;
  
IVsaCodeItem codeItem;
codeItem = (IVsaCodeItem)items.CreateItem("Script",
VsaItemType.Code,
VsaItemFlag.None);
codeItem.SourceText = "import System.Windows.Forms;" +
"MessageBox.Show('hi";

      Note that the ClockBox sample loads the code from a file, but it's interesting to see a hosted JScript engine's "Hello, World" code. Once you've added the code item, you need to compile and run:

  _engine.Compile();
  
_engine.Run();

When you're done, shut down the engine:

  _engine.Close();
  

Assembly References

      If you've been coding along with this article, you may be disappointed to learn that it doesn't quite work yet. While you did import the System.Windows.Forms namespace, you didn't import the assembly—you saved some keystrokes, which didn't give the VSA engine enough type information to compile and run your code. The host is responsible for pulling in the needed assemblies before attempting to compile the code. This is done by creating assembly reference items, as shown in Figure 6.
      Notice that this code adds the two most critical .NET assemblies, System.dll and mscorlib.dll, along with System.Windows.Forms so that your minimal sample will work. It certainly doesn't add anywhere near the number of common assemblies your users are likely to want, like System.Data or System.Xml. It's a good idea to add the references that your customers will most often need, but be prepared to allow them to add their own via some kind of setting either separate from the script or embedded into the script in a way that you parse before passing the script along to the engine—in a comment with a special format, for instance. Otherwise, programmers will be limited to only those types provided by the assemblies you reference in the host, which limits the flexibility of the extensibility hooks your host is providing.

Calling into Script

      Now there's enough for a minimal sample to work in JScript .NET, but not yet in Visual Basic .NET. Luckily, JScript .NET will execute code at a global scope, outside of any method or type. .NET doesn't support this directly, so JScript .NET will actually wrap the global code inside a method on a type that it constructs from whole cloth. Visual Basic .NET doesn't allow this. To give Visual Basic .NET code the opportunity to run code when it is loaded, you'll need to specify a well-known static entry point for your users to implement so that you know how to access initialization code for their Visual Basic .NET code. Main is a common entry point for this purpose:

  imports System.Windows.Forms
  

' The name of the module must match the name of the code item
' created in the hosting application via CreateItem.
module Script
sub Main()
MessageBox.Show("hi")
end sub
end module

To call this entry point after you've loaded the Visual Basic .NET engine, use reflection, as shown in Figure 7.
      With this code, you can now load and initialize both JScript .NET and Visual Basic files. Reflection can be used to execute any code defined in the loaded script, not just entry points, but it's much easier to use events.

Events

      Each ClockBox child form has its own timer. When the time has advanced by a second, the form lets the ScriptableClock class hosting the engine know by calling the TimerTick method. The clock could let the script know using reflection, as you did in the case of the Visual Basic Main function, but that is unwieldy and you don't even know if the script cares. Instead, use an event.
      An event is a feature in .NET that allows any number of listeners to register interest in a notification provided by an object. An event is defined in Visual Basic .NET like so:

  Public Event TickEvent()
  

And defined in C# like so:

  public delegate Tick();
  
public event Tick TickEvent;

The clock fires the event like this:

  public void TimerTick() {
  
Now = DateTime.Now;
if( TickEvent != null ) TickEvent();
_parent.Invalidate();
}

      If the script cares that the tick event has been fired, it will need to handle that event. Handling an event in Visual Basic .NET requires a named source providing events. You provide it by calling the IVsaEngine method AddEventSource:

  IVsaCodeItem codeItem = items.CreateItem("Script", ...);
  
codeItem.SourceText = code;

// NOTE: JScript doesn't support event sources
if( et != EngineType.JScript ) {
// Add the "Clock" event source
codeItem.AddEventSource("Clock", "ClockBox.ScriptableClock");
}

Notice the two parameters to AddEventSource. The first is the name of the event source that Visual Basic .NET will use to handle the event. The second is the type of the event source. This is required so that the Visual Basic .NET script engine knows what events have been exposed for its consumption and what their method signatures are. However, you'll need two more things.
      The Visual Basic .NET script engine will need an actual instance of the object exposing the events so that it can add the engine to the list of parties interested in events the script handles. Do this by implementing the IVsaSite method GetEventSourceInstance:

  object IVsaSite.GetEventSourceInstance(
  
string itemName, string eventSourceName) {
switch( eventSourceName ) {
case "Clock": return this;
default: return null;
}
}

      The Visual Basic .NET script engine will call GetEventSourceInstance, passing the name of the code item that the event source was associated with and the name of the event source you're after. Since the site is also the ScriptableClock and the thing firing the events, it is what is returned. With larger object models, this won't always be the case.
      The final thing you need to do for Visual Basic .NET to subscribe to events that you fire is to make sure that the event itself is public. Otherwise, subscription will fail at run time.
      Once the event source has been exposed, you can handle the events in your Visual Basic .NET code as follows:

  module Script
  
sub Clock_Tick() handles Clock.TickEvent
dim utc as DateTime = Clock.Now.ToUniversalTime()
Clock.Now = utc.AddHours(-8)
end sub
end module

      As of the initial release of .NET, JScript .NET supports event handling, but not via the same mechanism that Visual Basic .NET uses. Instead, JScript .NET code must manually subscribe to the event. It does this via a call to add_eventName:

  // From CentralTime.js
  
class EventHandlers {
static function Tick() {
var utc : DateTime = Clock.Now.ToUniversalTime();
Clock.Now = utc.AddHours(-6)
}
}

Clock.Caption = "Central Standard Time";
Clock.add_TickEvent(EventHandlers.Tick);

For this to work, JScript needs something you didn't need for Visual Basic .NET—to allow script access to objects that you provide in the host so that the JScript .NET code can call back into your code, either from global code or from an event handler.

Calling Out of Script

      The reason that your application is hosting script engines is to provide an extensibility hook for programmers interested in adding "mods" to your application. To make this happen, the host will need to expose its own types and objects for the script to program against. A host can expose its types as easily as adding itself as one of the reference items:

  // Add this assembly's types
  
// NOTE: Visual Basic doesn't support EXEs as sources of types
string assemName = Assembly.GetExecutingAssembly().Location;
refItem = (IVsaReferenceItem)items.CreateItem(assemName,
VsaItemType.Reference,
VsaItemFlag.None);
refItem.AssemblyName = assemName;

      Now any public types provided by the hosting assembly are immediately available to the hosting script. This allows your hosted JScript .NET to have the required metadata to call the ScriptableClock.add_TickEvent method to add itself as a subscriber to the tick event, along with the Caption property and Now field for adjusting the behavior of each clock. It also allows access to any of the other public types in the assembly, either to create new instances or to access static methods. However, support for access to your instances in JScript is not complete.
      While accessing the host's types is useful, it's also useful for the script to have access to the live objects already hydrated by the host. These objects can be passed into a method invoked via reflection or by an event that the host fires. Both of these are kind of a push model where the host decides what the script needs and passes it explicitly. Another way to handle this is by using a pull model, where the host advertises what it has and lets the script access it as needed. This is accomplished using the third and final VSA item type: a global item.
      The global item is like the Visual Basic .NET event source you've already seen. In fact, once you've added an instance of an object as an event source for the Visual Basic .NET script, all of the global item object's other public instance members are also available to the script. However, since JScript .NET doesn't support named event sources, you have to explicitly add instances to it (see the code shown in Figure 8).
      Adding a global item is almost the same as adding an event source. The difference is that JScript .NET doesn't support event sources and Visual Basic .NET requires an object to be added as an event source to handle events. As with adding an event source, when you add a global instance you must be prepared to resolve the name when the engine comes knocking, this time on GetGlobalInstance:

  object IVsaSite.GetGlobalInstance(string name) {
  
switch( name ) {
case "Clock": return this;
default: return null;
}
}

      Now that you've been able to add types and instances to both flavors of the script engine, it's my responsibility to reiterate the importance of proper protection on the types in the assemblies you're exposing to the scripting engines. Anything marked public will be accessible. Anything marked protected will be accessible to new derived types defined in the scripts. It's easy to get sloppy in an executable and make everything public, but remember that your scripts will have access to absolutely everything in that case, which could easily lead to unexpected behavior and support calls. I recommend heavy use of internal protection to guard against such things. If you'd like to restrict the scripts your application hosts even further, I recommend Keith Brown's article "Security in .NET: Enforce Code Access Rights with the Common Language Run-time".
      Now with the final piece in place, your scripts will be able to execute code when they're first loaded, they'll have access to types from the .NET assemblies you provide access to (including your own custom assemblies), they'll handle events, and they can call back into your host's object model. Feel free to experiment with the .js and .vb files in the scripts directory that is included in the code download.

Engine Differences

      Language differences aside, the initial implementations of the JScript and Visual Basic .NET scripting engines leave something to be desired in terms of consistency. A lot of branching code was needed to support the various features in each. And these are really just the tip of the iceberg. For example, in the sample I've also shown how to route the JScript .NET print function's output to a file. This is handy for JScript .NET, but there's no corresponding functionality in Visual Basic .NET that I've found. Also, while both engines implement IVsaSite (somewhat differently, as you saw), there's an unfortunate back door in this interface for getting and setting name/value pairs unique to each engine called GetOption and SetOption. Instead of exposing these properties as type-safe interfaces that any engine could implement (and therefore any host could request compliance with), each engine supports a list of values that the host needs to just know about. Getting or setting one of these options where it's not supported results in an exception. Because of the differences between each engine, as summarized in Figure 9, if you plan on plugging both into your applications I recommend adding another layer of abstraction around each engine to smooth over these differences.

Conclusion

      Keith Brown read an early draft of this article and asked, "Why do I need to use the VSA engines? Why can't I just use the batch compilers that come with .NET?" Darn that Keith for his astuteness, for indeed he's right. Not only do JScript .NET and Visual Basic .NET provide support in the base class libraries for running their compilers in batch mode programmatically, but so does C#, removing one of the current limitations in the VSA runtime. These compilers are exposed via a set of CodeDOM (code document object model) classes that consume code and produce assemblies.
      However, obtaining an assembly is only a small part of what VSA does. It also provides for passing in live objects, supports named event handling and IDE hooks in Visual Basic .NET, print redirection in JScript .NET, and a whole host of other things. Of course, you can do all of these things with the CodeDOM classes directly, but VSA vastly simplifies the task of adding extensibility to your applications. In fact, you can think of VSA as a wrapper around the lower-level CodeDOM classes. Feel free to dip into them if VSA doesn't have what you want, like C# support, but I'd recommend looking into VSA first.
      The VSA runtime in .NET provides the next generation in scripting and extensibility for your .NET applications and components. In addition to your host's custom types and objects, .NET allows scripts to access all of the .NET BCL, including the interop layer. The scripts are compiled to native code before they're executed to provide extra efficiency, and the languages themselves are full-fledged members of the .NET community, not scripting "subsets." While the VSA runtime is a standard part of the freely available .NET BCL, if you'd like to scale up to IDE and debugging, just build up from your VSA runtime code.

For related articles see:
Scripting Clinic: Script Happens .NET
JScript .NET Articles
What's New in JScript .NET
Visual Studio for Applications (VSA)
Microsoft.Vsa Namespace
For background information see:
https://vsip.summsoft.com/vsa
Security in .NET: Enforce Code Access Rights with the Common Language Runtime

Chris Sells is a consultant, specializing in in .NET and COM, as well as an instructor for DevelopMentor. Author of ATL Internals (Addison-Wesley, 1999), he's currently working on books about Windows Forms and Visual Studio .NET. See https://www.sellsbrothers.com.