.NET Scripting Hosts, Part Four

So far we've looked at the steps to define the IActiveScript, IActiveScriptSite and IActiveScriptParse interfaces in C# for our .NET scripting host. This month we'll focus in on code to create a skeleton host using these interfaces.


May 27, 2005
URL:http://drdobbs.com/net-scripting-hosts-part-four/184406066

Scripting Chat

Of these three interfaces, the host needs to worry most about IActiveScriptSite as that is the only one of these three that the host implements itself -- the other two are handled by the scripting engine. Interestingly since JScript and VBScript are native (non-managed) blocks of code, they essentially go through COM Interop themselves when calling back into the host via IActiveScriptSite. This is handled automatically by COM Interop when the engine makes calls on methods on the interface it received when the host called IActiveScript::SetScriptSite and passed in its IActiveScriptSite interface. Something to keep in mind for a bit later.

Let's look at a basic implementation of IActiveScriptSite in C# for our host:

using System;
using System.Runtime.InteropServices;

namespace dotnet_scriptinghost
{
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    Guid("DB01A1E3-A42B-11cf-8F20-00805F2CD064")]
    interface IActiveScriptSite
    {
        void GetLCID(out uint id);
        void GetItemInfo(    string pstrName,
                    uint dwReturnMask,
                    [Out,MarshalAs(UnmanagedType.IUnknown)] 
out object item,
                    IntPtr ppti);
        void GetDocVersionString(out string v);
        void OnScriptTerminate(    ref object result,
ref stdole.EXCEPINFO info);
        void OnStateChange(uint state);
        void OnScriptError(
[In,MarshalAs(UnmanagedType.IUnknown)] object err);
        void OnEnterScript();
        void OnLeaveScript();

    }

    public class ScriptHost : IActiveScriptSite
    {
        public ScriptHost()
        {
        }

        #region IActiveScriptSite

        public void GetDocVersionString(out string v)
        {
            throw new NotImplementedException();
        }

        public void GetItemInfo(
[In,MarshalAs(UnmanagedType.BStr)] string pstrName,
            [In,MarshalAs(UnmanagedType.U4)] uint dwReturnMask,
[Out,MarshalAs(UnmanagedType.IUnknown)] out object item,
            IntPtr ppti)
        {
            item = null;
        }

        public void GetLCID(out uint id)
        {
            throw new NotImplementedException();
        }

        public void OnEnterScript()
        {
        }

        public void OnLeaveScript()
        {
        }

        public void OnScriptError([In,MarshalAs(UnmanagedType.IUnknown)]
 object err)
        {
        }

        public void OnScriptTerminate(ref object result,
ref stdole.EXCEPINFO info)
        {
        }

        public void OnStateChange(uint state)
        {
        }

        #endregion

}
}

The definition of the IActiveScriptSite interface is the same as we looked at previously -- most the parameters are just normal .NET types but we have to take care with those typed as object to make sure they are attributed to be COM IUnknown interfaces so the marshalling layer will know what to do with them. If you neglect this crucial step, you'll see message boxes like this when you run your host:

In fact, if you see this message box while testing and debugging your .NET scripting host, it's likely you forgot some attributes somewhere on an interface. The message isn't too helpful in diagnosing this problem, but now that you know what to look for and what to do about it this part should go more smoothly -- yeah, right.

You'll notice that some of the methods throw an exception named System.NotImplementedException. This takes the place of a C++ COM method implementation returning the HRESULT E_NOTIMPL which indicates the method is not implemented to the scripting engine. The methods that throw these exceptions follow the normal Active Scripting conventions defined for this interface as to when the exception should be thrown and when it shouldn't.

The class defining the scripting host derives from the C# interface IActiveScriptSite just as a normal C++ class would derive from the IDL defined IActiveScriptSite interface. Implementing the host this way will make it easy to pass the script host IActiveScriptSite interface to the scripting engine. This brings us to the next step -- creating an instance of a scripting engine and beginning to interact with it via IActiveScript and IActiveScriptParse.

Let's create a method in the host that will be called to fire up the host in the first place:

public void Run()
{
    try
    {
        JScript engine = new JScript();
        IActiveScriptParse iap = engine as IActiveScriptParse;
        iap.InitNew();

        IActiveScript ias = engine as IActiveScript;
        ias.SetScriptSite( this );

        ias.Close();
    }
    catch( ExecutionEngineException e )
    {
        MessageBox.Show( e.Message,"dotnet_scriptinghost" );
    }
}

In the first block of code in this method, an instance of the JScript engine is created. Question is: where does the JScript engine class come from? Well, it's not defined in the .NET framework anywhere. We're going to create it ourselves using another nifty COM Interop attribute -- COM Import.

The COM Import attribute tells .NET that I want to define a class that gets its actual definition from an external COM class -- this includes any interfaces it might define. It allows us to create instances of the .NET class and interact with them as .NET objects but which are in fact COM objects. Pretty sweet. Only trick is knowing how to define the class using COM Import because it's not the most intuitive:


    [ComImport, Guid("F414C260-6AC0-11CF-B6D1-00AA00BBBB58")] 
    public class JScript  
    { 
    }

The class name itself -- in this case .JScript. -- can be anything you like. I just happened to choose the name JScript since that is the Active Scripting engine I'm wrapping with .NET. The Guid attribute is also very important. It defines the COM CLSID of the COM object that I want to wrap/create. You can get the CLSID for engines by converting COM ProgIDs to CLSIDs, or by using a tool like OleView from the Win32 SDK to inspect the CLSIDs of the engines. Either way, you'll need the GUID value to create the scripting engine just like you would in C++.

In the next couple of lines of code, I cast the newly created engine as an IActiveScriptParse interface using the C# keyword .as.. Then I call the standard IActiveScriptParse::InitNew method to initialize the engine. With the .NET wrapping of Active Scripting at a basic level now in this example, I can begin to interact with Active Scripting just as I would have in a typical C++ scripting host.

Near the end of the method, I indicate I want to catch exceptions of type System.ExecutionEngineException. These are thrown by the .NET runtime when it incurs a catastrophic error such as when problems happen with badly defined C# interfaces that mimic COM ones. It's a good general practice to handle exceptions as close to where they occur as possible since it's unlikely a caller will know what to make of this kind of exception or why it happened. For now, I'm just showing the error message in a message box -- the result of the exception getting thrown and displayed can be seen in the message box graphic earlier in this article.

The last bit of code is where I tell the engine about my IActiveScriptSite interface and how to talk to my host. Notice that since I derived my host from IActiveScriptSite in the first place, I can cast my host's this pointer to the interface and just pass it on through to the engine. Finally, I close down the scripting engine via IActiveScript::Close as usual.

If you paste this code into a new C# WinForms project in Visual Studio .NET 2003 and compile it, it should run just fine although at this point it doesn't do anything really interesting. For that, you'll have to wait until next month.

Do you have a passion for Scripting?

If you want to access the extensive exchanges between other developers about Active Scripting, head over to Google's archive of Usenet newsgroups and read through the microsoft.public.scripting.hosting group. If you're working with the newer .NET version of scripting, you might look through microsoft.public.dotnet.scripting.

Also, if you have questions about how to use Active Scripting, I'd encourage you to read through my Active Scripting FAQ document that covers a wide range of problems other developers have had and many useful answers from their experiences. You can find it here.

Finally, a rich source of material on Active Scripting, JScript, and VBScript from a true Microsoft insider is Eric Lippert's blog.

Looking for back issues of the Active Scripting newsletter?

Head over to the Dr. Dobb's Journal site to read any of the back issues on-line. The Active Scripting newsletter has now been in publication for over 5 years and you will find a wealth of information, commentary, and insight that will make using and understanding Active Scripting easier and more productive.

Final Thoughts

I hope you find this newsletter a timely and helpful addition to your knowledge base about Active Scripting. Please feel free to email me at [email protected] and let me know what you think of it, ways to improve on content, and how this newsletter is helping you. < / p > < p > U n t i l n e x t month...

Cheers!
Mark

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.