The three essential interfaces for developing an Active Scripting host -- whether in C++ or C# -- are IActiveScript, IActiveScriptParse (both implemented by the scripting engine), and IActiveScriptSite (implemented by the host). Last month we looked at IActiveScript and how to declare the interface in .NET. This month we'll look at the interface declarations for the other two essential interfaces.
The IActiveScriptParse interface is exposed by the scripting engine and provides support for adding script text to the engine for processing. It has a somewhat limited use in a host compared to IActiveScript, but you can't get along without it. Laying out the .NET declaration for IActiveScriptParse is similar to the steps for IActiveScript. First, let's look at how the interface appears in Visual Studio's Object Browser (basically, how COM interop sees the interface without any tweaking):
Figure: IActiveScriptParse interface declaration as seen by Object Browser
Notice that the declaration is for IActiveScriptParse32 not IActiveScriptParse. The interface name is actually IActiveScriptParse32, but the header files created by MIDL.EXE for C++ developers remap the name for Win32 development to IActiveScriptParse. For those brave souls building under 64-bit Windows, the interface would be IActiveScriptParse64 but would still map in the end to IActiveScriptParse. For my purposes, I'll just refer to it as IActiveScriptParse.
Now, let's look at the interface again but this time in C#:
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("BB1A2AE2-A4F9-11cf-8F20-00805F2CD064")] interface IActiveScriptParse { void InitNew(); void AddScriptlet( string defaultName, string code, string itemName, string subItemName, string eventName, string delimiter, uint sourceContextCookie, uint startingLineNumber, uint flags, out string name, out stdole.EXCEPINFO info); void ParseScriptText( string code, string itemName, IntPtr context, string delimiter, uint sourceContextCookie, uint startingLineNumber, uint flags, IntPtr result, out stdole.EXCEPINFO info); }
As with IActiveScript, this interface derives from IUnknown, not IDispatch. The UUID specified with the Guid attribute is the actual UUID of the interface. The string parameters will get automatically converted to BSTR data types by COM interop when a call is made to the method. Finally, the methods return "void" not HRESULT since exceptions will get thrown if errors occur.
Careful readers will notice that the 3rd and 8th parameters differ from the others -- they are defined as generic System.IntPtr objects. The advantage of the IntPtr object is that we can give it a zero value as in:
System.IntPtr p = System.IntPtr.Zero;
We can then use this zeroed value as a proxy for a classic 'C' style NULL pointer. Since some parameters for the IActiveScriptParse interface (as well as others) take NULL as a potential value, the IntPtr typed parameter makes this easy. It's also handy if we need to back pointers to non-standard objects such as structures that aren't defined (or re-defined) in C#, but I digress.
So far so good.
Next up is IActiveScriptSite which is the first and often the only interface that developers bother to implement when creating a basic, simple Active Scripting host in C++. It differs from IActiveScript and IActiveScriptParse because this interface acts as a callback site for the scripting engine to inquire how the host wishes to handle various things. This interface must be implemented by the host in order to create a valid host -- no exceptions (and no pun intended).
Here is how the IActiveScriptSite interface looks in C#:
[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(); }
Looking at this interface declaration, at first glance it seems similar -- the base type is assumed to be a COM IUnknown interface and the UUID is the value of the IActiveScriptSite interface as defined by Microsoft. The methods looks pretty much the same as before also. What gets interesting and in this case, really, really important are the two places where we are working with a COM interface that has to be marshaled -- one is in GetItemInfo and one is in OnScriptError.
Of the two, GetItemInfo is the far more interesting one.
This is the workhorse method for the host whereby the engine requests access to the interfaces for objects (COM Automation) that were previously added to the engine's namespace via a call to IActiveScript::AddNamedItem (or IActiveScript.AddNamedItem if you like the C# style better for illustrations like this). When the engine encounters a name in script that is being executed that matches up with a name that was added, it asks the host (via IActiveScriptSite.GetItemInfo) for an interface to the object (or its type library if IActiveScript.AddTypeLib was called instead).
Looking at the definition of IActiveScriptSite in the Object Browser (as seen by COM Interop), here is what we see in Visual Studio:
Figure: IActiveScriptSite interface declaration as seen by Object Browser
Notice that the object browser is showing GetItemInfo as having a parameter "out object" as the 3rd parameter. This is the parm that returns back a COM interface to the engine. If you declare the IActiveScriptSite interface in your code and try to write a host around it, here is what you'll likely see once it starts running:
Figure: Error message when using a badly defined IActiveScriptSite interface
In my case, this happened when trying to start the engine up via IActiveScript.SetScriptState(SCRIPTSTATE_CONNECTED). Basically what my GetItemInfo method returned was non-usable by the engine.
So much for the hyped "it just works" philosophy.
It works, but it needs help. If you recall from the earlier articles in this series, I mentioned that COM interop is cool, indispensable, and tricky to work with.
Next time, we'll look at the IActiveScriptParse and IActiveScriptSite interfaces as well as the initial code to constructing a .NET scripting host object.
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.
Until next month...
Cheers!
Mark