Hosting VBScript in your own Application

Environment: VC6, NT4 SP6, Win2000

Ever needed to let your customer extend the functionality of your application? Why not use the scripting technology also used by Internet Explorer! This article describes the use of the ActiveX scripting engine within your application. To hide implementation details, a singleton class (ScriptEngineFactory) has been implemented which can be used very easily to host scripting in your application.

The implementation of the Scripting Engine has been done inside a separate DLL, which allows to use it without recompilation. The DLL has been implemented as a MFC extension DLL. Therefore, you can use the exported class interface easily. The supplied example project shows how to do this.

The scripting engine is implemented as a singleton. For those of you who do not know what this is: A singleton has a static member, namely the one and only instance that may exist within a program. This instance is accessible through the Instance() member function. Th3:11 PM 12/13/2001is makes sure that the engine gets created only once, and one is sure to access always the same instance. To make sure no other instance can get created, the constructor of this class is declared private. The useable interface of ScriptEngineFactory consists of the following methods:

void AddExposedObjectPtr( CString newObjectName,
                          IUnknown* newExposedObject);
void InitializeScriptEngine();

BOOL RunScript(CString newScript);
BOOL LoadScript(CString newScript);
BOOL UnloadScript();
BOOL GetIDOfFunction(CString strFunctionName, DISPID* pID);
CStringArray* GetErrorMessages();

AddExposedObjectPtr( CString newObjectName, IUnknown* newExposedObject)

This method adds a CCmdTarget derived class interface to the namespace of the script to execute. If you want to make portions of your applications available to your script, you can add a CCmdTarget derivative with an OLE interface to your project. OLE methods and properties defined in this class are then available in the script. An example is in the demo project.

InitializeScriptEngine()

This method loads and initializes the VBScript engine from Microsoft. If not available, it returns FALSE and leaves an errormessage in the ErrorMessages array.

RunScript(CString newScript)

This method runs a script, specified in the newScript argument. The script gets loaded and executed directly. After this, the script engine is emptied again.

LoadScript(CString newScript)

This method loads a script, specified in the newScript argument. The script gets loaded and the main body is executed directly. After this, the script remains loaded, which makes it possible to execute functions, subs and/or access properties in the script.

UnloadScript(CString newScript)

This method unloads a script. The script engine needs to be initialized again with another, or the same script.

GetIDOfFunction(CString strFunctionName, DISPID* pID)

When a script is loaded (using LoadScript()), this function retrieves the DISPID corresponding to the given functioname. This DISPID can be used in your program to call InvokeHelper(), which actually calls the VBS method, or function. One must take care here to specify sufficient arguments and return type.

GetErrorMessages()

This method returns a pointer to a CStringArray, which contains error messages occurred along the way. It is emptied each time the ScriptEngine is initialized again.

Now, how do we get this all to work? Following is a code snippet of loading a script:

void CSimpleClientDlg::OnLoadBn() 
{
  CScriptUtils* pScriptUtils = new CScriptUtils;

  UpdateData(TRUE);
  ScriptEngineFactory::Instance()->InitializeScriptEngine();
  ScriptEngineFactory::Instance()->AddExposedObjectPtr("MyUtils",
    pScriptUtils->GetInterface(&IID_IUnknown));
  ScriptEngineFactory::Instance()->LoadScript(m_strScript);

  m_ctlUnloadBn.EnableWindow(TRUE);
  m_ctlLoadBn.EnableWindow(FALSE);
}
The above is the button handler for the "Load script" button. First, the script text adjacent to this button is saved in the m_strScript variable. Then, the script engine is initialized. An instance of a CCmdTarget class is instantiated and the IUnknown interface of it is given to the ScriptEngine. Then, the m_strScript is loaded into it. Thats all there is to it! Next code snippet shows how to call the "ButtonHandler" sub:
void CSimpleClientDlg::OnScriptHandledBn() 
{
  DISPID dispid;
  if (ScriptEngineFactory::Instance()->GetIDOfFunction(
                   "ButtonHandler", &dispid)) {
     long result;
     ScriptEngineFactory::Instance()->InvokeHelper( dispid,
                                                    DISPATCH_METHOD,
                                                    VT_EMPTY,
                                                    (void*) &result,
                                                    NULL);
  }
}
Since our script is loaded into the engine, the engine "knows" about the existence of Sub ButtonHandler. The GetIDOfFunction method retrieves the DISPID corresponding to it. This DISPID can then be used in the InvokeHelper method if the Engine. Note: InvokeHelper is actually there because the ScriptEngineFactory is inheriting from COleDispatchDriver.

Notes

Since I have very little time, the article is maybe not as elaborate as may be expected, but the used technology can give you much power in your applications. The sourcecode is documented, so there may not be big problems understanding it.

Downloads

Download demo project - 29 Kb
Download source - 17 Kb