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


Comments

  • Good Work

    Posted by chitrack on 04/29/2005 01:23am

    Thanks for your effort and it is useful! Based on this example i also gave VBSCript support to my application, but i could send only one parameter to the script thru the InvokeHelper. I want to send more than one parameter but the no of parameters and its datatypes have to be decided dynamically. I am stuck here, Can you give some inputs on this?

    Reply
  • THANK YOU!

    Posted by Legacy on 01/29/2004 12:00am

    Originally posted by: Terry

    You made it SO simple to incorporate scripting into my application. THANK YOU! There are TONS of examples on the internet but YOUR's is very easy to understand. Adding new methods is also very easy using the Class Wizard. Can't wait to show this to the development staff at work!

    Reply
  • Loops don't work

    Posted by Legacy on 10/08/2003 12:00am

    Originally posted by: Erdep

    If I want something to hapen several times, let's say 10 times, the script does nothing at all!
    
    for example:

    For Val = 0 To 10
    InputBox "Type your type (" & (10-val) & " to go)", "Don't think, just DO"
    Next Val

    Gives an errormessage in the GetErrorMessages Array, wich tells me that it goes wrong at the "next val" part of the code

    Can anyone help me?

    Erdep

    Reply
  • Linker error: unresolved external symbol _IID_IActiveScriptParse

    Posted by Legacy on 01/18/2002 12:00am

    Originally posted by: Neville Franks

    I'm trying to build the app and I get the following linker error:

    ScriptEngineFactory.obj : error LNK2001: unresolved external symbol _IID_IActiveScriptParse

    I assume this is quite a basic problem but I haven't been able to work out how to resolve it so far.

    Any assistance much appreciated.

    Reply
  • JScript and other scripting engines.

    Posted by Legacy on 01/15/2002 12:00am

    Originally posted by: Ben Sleat

    Looking at how the code in the utilities.dll works and then reading up on the Scripting Interfaces docs in the MSDN library it is clear that this code can be made to work for other scripting engines with very little extra work (eg. JScript).

    Anyway, I'm developing an application in which we need to be able to allow the user to write thier own scripts... this article is very useful for this. However, we want to support all scripting languages currently installed on the machine, so thats normally VBScript and JScript, but some machines also have scripting versions of Perl and other languages.

    My question is this... is there a way to enumerate all the scripting engines currently installed on a system?

    Thanks
    Ben

    PS. If I'm asking this question in the wrong place then I appologies and please could you point me to an appropriate forum. Thanks.

    Reply
  • improvements?

    Posted by Legacy on 01/14/2002 12:00am

    Originally posted by: geo

    Hallo,

    as a first, I would address about the singleton issue. If it is really helpfull to have it as a singletone.
    As a second, I would add somethink to think about - I'm using another way of calling methods from script - if you look to the methods of IActiveScript, there is GetScriptDispatch method, which returns IDispatch of active script. So from the VB you may call it somehow like

    MyVBScriptEngineObject.Script.MyScriptMethod

    have some disadvantages over your GetIDOfFunction() - like not so dynamic, but might be also usefull


    geo

    Reply
  • improvements?

    Posted by Legacy on 01/14/2002 12:00am

    Originally posted by: geo

    Hallo,

    as a first, I would address about the singleton issue. If it is really helpfull to have it as a singletone.
    As a second, I would add somethink to think about - I'm using another way of calling methods from script - if you look to the methods of IActiveScript, there is GetScriptDispatch method, which returns IDispatch of active script. So from the VB you may call it somehow like

    MyVBScriptEngineObject.Script.MyScriptMethod

    have some disadvantages over your GetIDOfFunction() - like not so dynamic, but might be also usefull


    geo

    Reply
  • nice place to start, but looks not really good :)

    Posted by Legacy on 01/14/2002 12:00am

    Originally posted by: Andrey Koubychev

    Nice source to start designing your own implementation , but I wouldnt consider those sources as complete working tool. First of all becouse of memory leaks. Then more your execute scripts then more leaks you have. Here is a quick and dirty solution for those who want to use this src immediately. This is how it worked for me, probably author has something to add to this too:
    1. Add new function FreeInstance() to ScriptEngineFactory class with "delete m_Instance;"
    2. Change ActiveScriptSite::~ActiveScriptSite() to :
    {
    for (int i=0; i< m_ExposedObjects.GetSize () ; i++ ) delete m_ExposedObjects[i];
    m_ExposedObjects.RemoveAll ();
    }
    3. remove unload script button and all actions should be done at once in void CSimpleClientDlg::OnLoadBn() :
    ......
    ScriptEngineFactory::Instance()->LoadScript(m_strScript);
    /* we unload script after execution and free previously created object. We should make destructor public.
    I think this should be somehow done with reference counting but OnFinalRelease never was called, lets "hack" it :)
    */
    ScriptEngineFactory::Instance()->UnloadScript();
    delete pScriptUtils;


    Reply
  • Three Memory Leaks

    Posted by Legacy on 01/12/2002 12:00am

    Originally posted by: Werner Schaudin

    Memory leaks:

    1. Utilities DLL:
    The "ExposedObject" object(s) is/are not removed
    from memory
    -> m_ScriptSite.m_ExposedObjects

    2. SimpleClient Appl:
    Object "CScriptUtils" is not removed from memory

    3. Utilities DLL:
    Static object "ScriptEngineFactory::m_Instance"
    is not removed from memory


    Reply
  • Cool Demo

    Posted by Legacy on 01/12/2002 12:00am

    Originally posted by: Dave Goodman

    Thank you!
    
    

    I haven't explored it much, but I compiled and ran it, and it works nicely. Some hints to people trying the demo:

    * In the Post-Build Step of Project Settings for Utilities, you can add a line to copy the .dll to the SimpleClient directory for ease of testing.
    Example: copy Release\Utilities.dll ..\SimpleClient

    * In the Link tab of Project Settings for SimpleClient, add the library module.
    Example: ..\Utilities\Release\Utilities.lib

    * In line 8 of SimpleClientDlg.cpp modify the #include to point to the Utilities directory.
    Example: #include "..\Utilities\ScriptEngineFactory.h"

    * In line 69 of SimpleClientDlg.cpp initialize the edit field with a simple script to facilitate testing.
    Example: m_strScript = _T("Sub ButtonHandler()\r\n msg=MyUtils.DoSomething\r\n MsgBox msg\r\nEnd Sub\r\n");

    Keep up the good work.

    Dave Goodman
    http://www.dkgoodman.com

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • 10 Rules that Make or Break Enterprise App Development Projects In today's app-driven world, application development is a top priority. Even so, 68% of enterprise application delivery projects fail. Designing and building applications that pay for themselves and adapt to future needs is incredibly difficult. Executing one successful project is lucky, but making it a repeatable process and strategic advantage? That's where the money is. With help from our most experienced project leads and software engineers, …

  • IT decision support impacts all aspects of technology management, from governance and strategy to budgets and resource planning. IT decision support effectiveness often falls prey to data-driven challenges that make it difficult to understand the data in context. These challenges: overwhelming data volumes, heterogeneous data types, and growing data complexity. This Forrester Consulting Paper reports the three key findings from their study conducted, on behalf of BDNA, to test the hypothesis that data …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds