Introduction
Writing applications that host a script engine to enable people to write
scripts to customize and extend applications has proven to be very successful.
There are thousands of developers using the Windows® Script engines in their
applications and make no mistake about it, the Microsoft implementation is quite
adequate if you want to add script (macro) capabilities to your application. The
one good choice is to use the Active Scripting technology. First of all, Active
Scripting technology uses existing scripting language, so you don't need to
learn any new language. If you know how to program with VBScript, JavaScript or
even PerlScript, that is all you need. In this article, I will present a simple
alternative that allows you to add Scripting support to your application (even
existing one).
Description
The Active Scripting architecture consists of a family of COM interfaces that
defines a protocol for connecting a scripting engine to a host application. In
the world of Active Scripting, a script engine is just a COM object that's
capable of executing script code dynamically in response to either direct
parsing or loading of script statements, explicit calls to the script engine's
IDispatch interface, or outbound method calls (events) from the host
application's objects. The host application can expose its automation interfaces
to the script engine's namespace, allowing the application's objects to be
accessed as programmatic variables from within dynamically executed scripts. A
client application that needs to add script support only needs to implement Host
portion of this technology. Different vendors may implement their own
implementation of Engine, giving you now alternative to use other language that
you already know. A good example is the PerlScript engine. A company may decide
to use it instead of using JavaScript or VBScript in order to maintain existing
code base.
Figure 1: Active Scripting architecture
Figure 2: Active Scripting COM Interaction
Figure 1 shows the basic architecture of Active Scripting, and Figure 2 shows
sequence diagram detail of the COM interfaces defined by the architecture. Your
client application that needs to use Active Scripting technology, creates and
initializes a scripting engine based on the scripting language you want to
parse, and you connect your application to the engine via the
SetScriptSite
method. You can then feed the engine script code that
it can execute either immediately (not a function) or at some point in the
future (function call), based on the script engine content and its state. For
example, the following script text contains only global statements, and
therefore could execute at parse time:
ScriptHost.Display("Hello CodeProject guru around the world.");
This statement would force the application to display a message box with the
provide text (using the MFCScriptHost.exe Application) but:
function HostDisplay()
{
ScriptHost.Display("Hello CodeProject guru around the world.");
}
would force the application to display this message only when
HostDisplay()
is called. But the good news is that this new method
can also be accessed by your application whenever you want. To execute this
function, your client application (Site object) needs to call
GetIDsOfNames
and Invoke
of the IDispatch
pointer of the script engine being used to force the execution of this function.
Another cool feature of the Active Scripting technology is that you can add any
automation object to the script engine items list and access its methods and
properties from your script. In fact, these features are being used inside of
Microsoft Office applications, Internet Explorer and Visual Studio. For example,
you could have an item named 'Documents' and expose the list of opened documents
in your application. Your implementation of the Script Site would call
AddNamedItem("Documents")
on the script engine interface pointer.
For example, in our last example, the script engine gets a dispatch pointer of
the "ScriptHost" named-item and call the "Display" method. But internally a lot
of this process depends on the state of the Scripting Engine. That is the state
of the engine must be started (SCRIPTSTATE_STARTED
). At this point
(when the engine is started), the engine would query the ActiveScriptSite object
to resolve a named-item to a IDispatch
interface pointer. It will
then access that interface properties and methods by calling
GetIDsOfNames
and Invoke. This new item then becomes just like an
internal variable that can be accessed whenever it needs it. Also, it becomes
apparent how simple it is for the script engine to access the named-item
properties and methods thanks to these two IDispatch
interface
methods.
But connecting event function to the script engine is a bit more tricky for
many reasons. The more apparent reason is that the script engine must be able to
do late-binding event support on a named-item. To support binding events to host
named-item, Active Scripting engines use connection points to map outbound
method calls/events from the host application's objects onto script functions.
The way the named-item event function is called depends on the script language.
VBScript uses a whole different approach to bind event call than JavaScript.
Andrew Clinick's Scripting Events article give a lot more details than what I
could cover here, so you may want to check it out.
I think this may be enough to get you start, now if you want to learn more,
please check first the following References at the bottom of this article. This
article was updated to show how the ScriptHost object can trigger event to the
script.
Adding Scripting Support to your application
Now I have built this procedure that you can use to add scripting support to
new or existing MFC application.
- The first step consists to creating an .ODL file (if your application
doesn't have one). Advanced MFC developers could also fake this process by some
others means (since we will not register this library) but I will not cover this
here. You will have to generate a GUID by using GUIDGen.exe (available in
tools folder of Visual Studio). A typical .ODL file would look like this
[ uuid(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX), version(1.0) ]
library YourAppName
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
};
- Use ClassWizard (Ctrl+W) to create an Automation object derived from
CCmdTarget
that will be the host object (ActiveScriptSite's client
object). Define methods that you want to make available from your host. For
example, you may have methods like:
CreateActiveX(strProgID)
,DisplayMessage(strMessage)
,
etc. Typically, these new dispatch methods would be called from a script.
- In your new class files, replace all references of
CCmdTarget
with CActiveScriptHost
. At this point you will have to include
ActiveScriptHost.h/cpp to your project.
- *UPDATED* Override the virtual function
HRESULT GetClassID( LPCLSID
pclsid )
. It must return successfully the CLSID of your host object. The
CSLID can be found in the .ODL file once you create the automation object
in step 2. Typical implementation will look like this: HRESULT CHost_Proxy::GetClassID( LPCLSID pclsid )
{
*pclsid = CLSID_Host_Proxy;
return S_OK;
}
Note also that at this point our object is really a COM object and
ActiveX control but we will not register as we would normally do.
- *UPDATED* You will need to declare the type library that you are using. This
step is tricky but using MFC macro, it is a lot easier. Just add
DECLARE_OLETYPELIB(CYourHost_Proxy)
in the header file of your
proxy class and IMPLEMENT_OLETYPELIB(CYourHost_Proxy, _tlid, _wVerMajor,
_wVerMinor)
in the .cpp file. _tlid
is the GUID of the
typelibrary (step 1) and _wVerMajor
/_wVerMinor
represent the version number of your typelibary. Also, use the resource include
editor to add these directives.#ifdef _DEBUG
1 TYPELIB "Debug\\YourAppName.tlb"
#else
1 TYPELIB "Release\\YourAppName.tlb"
#endif
- *NEW* Now Add an event-source object, for example:
[ uuid(740C1C2D-692F-43F8-85FF-38DEE1742819) ]
dispinterface IHostEvent
{
properties:
methods:
[id(1)] void OnRun();
[id(2)] void OnAppExit();
};
[ uuid(F8235A29-C576-439D-A070-6E7980C9C3F6) ]
coclass Host_Proxy
{
[default] dispinterface IHost_Proxy;
[default, source] dispinterface IHostEvent;
};
As you can see in this example, our Host now supports two events that we
can trigger directly from our code by using
COleControl::FireEvent
function. Such functions are very simple.
For example:void FireOnRun()
{FireEvent(eventidOnRun,EVENT_PARAM(VTS_NONE));}
void FireOnAppExit()
{FireEvent(eventidOnAppExit,EVENT_PARAM(VTS_NONE));}
- Create an instance of your host object (can also be a dynamically-created
class by using MFC macro) and call
CYourHostProxy::CreateEngine( 'Language
ProgID' )
which can be 'JavaScript' or 'VBScript' if you want to use
these engine.
- Add implementation code to your proxy methods to do what you wish to let the
advanced users do with your application.
- Add any additional named-item object that you wish to access from script
language
- Provide way for the user to create script or load script text from disk.
CActiveScriptHost
class provide helper functions that you may want
to reuse based on functionalities that you want to give in your application. By
the way, it is not safe to let user create Inproc-ActiveX object but
Local-server is generally good. One good reason not to let the user create
ActiveX control is that, if a crash occurred inside of the ActiveX, your
application should not. Local-server give you freely this kind of
safety.
Revision History
References