Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Adding scripting support to your Application using minimalistic aproach

4.66/5 (29 votes)
6 Apr 2009CPOL4 min read 58.5K   660  
One very simple way to add scripting support to your app without need to install or distribute anything

 Download ScriptHost.zip - 2.64 KB

Introduction  

Quite often application developers tend to find out that thay no longer have time to extend functionality of  program by releasing newer and newer executables.Simply because hunger for new but small features and more flexibility by users never ends. And knowing how creative user comunity itself can be, next evolution step is often clear ...

Lets' make the whole thing scriptable

I usually try to use whatever library that is already available as part of operating system. And we are quite lucky here. Ever since windows 95 Windows ships with Internet Explorer which in turn is continuously updated to support Latest version of JavaScript and VBScript. Now what is Interesting is that jscript.dll and vbscript.dll are as mentioned before part of every windows version out there. And also fact that they are just script engines. Meaning that they were written to be completely independent from application that uses them. This smart idea was at that time named Active Scripting. There are many downloadable dll engines writen for it like Ruby Lua Python Perl Haskell... and few of esoteric ones too ;D And the best part is no code changes are needed except changing string from let's say "JavaScript" to "RubyScript" But back to engines. If I oversimplify it then I could say that all that they can do are elementary operations like A+B=C plus Regular Expressions. Three most known applications using them are Microsofts Web server , Windows scripting host, and Internet Explorer. For example. In this Api Internet explorer simply implements Scripting Host interface = just registers his functions and variables(A, B, C= complete DOM model in this case) and from that moment on everything inside him is scriptable. Folks from Microsoft were so impressed with this new flexibility that they said to them self. "Lets register every win32 api and com thing we can find just for fun and forget tedious c++ compilation times" And Windows Scripting Host was born. Now you probably think yourself "Damn that will be another overcomplicated esoteric com api class galore" Well you will be surprised how easy it really is.  Just Select script engine with proper string like "JavaScript" and Register your procedures and vars in  GetIDsOfNames  

The Sample  

 The sample itself except registering simple test function also registers most of win32 apis so that you have something to play with right from the start. This also can give you an glimpse on how for example Windows Scripting Host internally works. As for now. Sample supports two major calling conventions stdcall  and cdecl (put prefix _ in front of api name in script ).  But detection of function return type (int or WCHAR*) is just basic and so is error handling so you can focus more on how it works.  

#define INITGUID

#include <windows.h>
#include <activscp.h>
#include <stdio.h>

HMODULE dll[1000];

// this example proc will be called and passed text from script
int WINAPI test( char* text ) {
    printf("my app proc called from script with param \"%s\" ",text);
    // and this return variable in turn passed to script
    return atol(text+strlen(text)-1)+1;
 // return L" test result "   you can return text to script too
}

struct ScriptInterface : public IDispatch {
    DWORD cdcl;
    long  WINAPI QueryInterface( REFIID riid,void ** object) { *object=IsEqualIID(riid, IID_IDispatch)?this:0;         return *object?0:E_NOINTERFACE; }         
    DWORD WINAPI AddRef ()                                                                { return 0; }         
    DWORD WINAPI Release()                                                                { return 0; }
    long  WINAPI GetTypeInfoCount( UINT *)                                                { return 0; }
    long  WINAPI GetTypeInfo( UINT, LCID, ITypeInfo **)                                   { return 0; }
    // This is where we register procs (or vars)
    long  WINAPI GetIDsOfNames( REFIID riid, WCHAR** name, UINT cnt ,LCID lcid, DISPID *id) { 
        for(DWORD j=0; j<cnt;j++) {
            char buf[1000]; DWORD k; WideCharToMultiByte(0,0,name[j],-1,buf,sizeof(buf),0,0);
            // two loops. one for sdcall second for cdecl ones with prefix _ added in script
            for(k=0; k<2;k++) {
                // first check our app procs (test) 
                if(strcmp(buf+k,"test")==0) { id[j]=(DISPID)test; break ; } else
                // then win32 api in known dlls
                for(int i=0; (dll[i]||dll[i-1]);i++) { if((id[j]=(DISPID)GetProcAddress(dll[i],buf+k))) break; } if(id[j]) break;
            }
            cdcl=k;
            if(!id[j]) return E_FAIL;
        }
        return 0; 
    }
    // And this is where they are called from script
    long  WINAPI Invoke( DISPID id, REFIID riid, LCID lcid, WORD flags, DISPPARAMS *arg, VARIANT *ret, EXCEPINFO *excp, UINT *err) { 
        if(id) {
            // we support stdcall and cdecl conventions for now
            int i= cdcl?arg->cArgs:-1, result=0, stack=arg->cArgs*4; char* args[100]={0};
            while((cdcl?(--i>-1):(++i<arg->cArgs))) {
                DWORD  param=arg->rgvarg[i].ulVal; 
                // we convert unicode string params to ansi since most apis are ansi
                if(arg->rgvarg[i].vt==VT_BSTR) { 
                    WCHAR* w=arg->rgvarg[i].bstrVal; 
                    WideCharToMultiByte(0,0,w,-1,args[i]=new char[wcslen(w)+1],wcslen(w)+1,0,0); param=(DWORD)args[i]; 
                }
                _asm push param; 
            }
            // for cdecl we push params in reverse order and cleanup the stack after call
            _asm call id
            _asm mov result, eax
            if (cdcl) _asm add esp, stack
            i=-1; while(++i<arg->cArgs) if(args[i]) delete args[i];
            // return value to script (in this case we support just unsigned integer and unicode string types )
            if(ret) ret->ullVal=result; char*c=(char*)result;
            if(ret) ret->vt=VT_UI4; __try { if(!c[1]&&(*c<'0'||*c>'9')) ret->vt=VT_BSTR; ret->bstrVal=SysAllocString((WCHAR*)c); } __except(EXCEPTION_EXECUTE_HANDLER){}
            return 0; 
        }
        return E_FAIL;
    }
}; 

struct ScriptHost : public IActiveScriptSite  { 
    ScriptInterface Interface;
    long  WINAPI QueryInterface( REFIID riid,void ** object) { *object=(IsEqualIID(riid, IID_IActiveScriptSite))?this:0; return *object?0:E_NOINTERFACE;  }                                                                                 
    DWORD WINAPI AddRef ()                                                                { return 0; }         
    DWORD WINAPI Release()                                                                { return 0; }
    long  WINAPI GetLCID( DWORD *lcid )           {  *lcid = LOCALE_USER_DEFAULT;           return 0; }
    long  WINAPI GetDocVersionString( BSTR* ver ) {  *ver  = 0;                             return 0; }
    long  WINAPI OnScriptTerminate(const VARIANT *,const EXCEPINFO *)                     { return 0; }
    long  WINAPI OnStateChange( SCRIPTSTATE state)                                        { return 0; }
    long  WINAPI OnEnterScript()                                                          { return 0; }
    long  WINAPI OnLeaveScript()                                                          { return 0; }
    long  WINAPI GetItemInfo(const WCHAR *name,DWORD req, IUnknown ** obj, ITypeInfo ** type)  { 
        if(req&SCRIPTINFO_IUNKNOWN) *obj=&Interface; if(req&SCRIPTINFO_ITYPEINFO) *type=0;  return 0; 
    }
    long  WINAPI OnScriptError( IActiveScriptError *err )                                 { 
        EXCEPINFO e; err->GetExceptionInfo(&e); MessageBoxW(0,e.bstrDescription,e.bstrSource,0);
        return 0; 
    }
}; 

void main() {
    HRESULT hr;  CoInitialize(0); 

    // In this sample we can call all nonunicode apis (for sample it's enough I suppose) from following dlls. Add more if you wish
    char* name[]={"ntdll","msvcrt","kernel32","user32","advapi32","shell32","wsock32","wininet","<",">",0}; 
    for(int i=0; name[i];i++) dll[i]=LoadLibrary(name[i]); 

    // Here we can chose betwen installed  script engines. Default engines shipped with windows are JavaScript and VbScript.
    GUID guid; CLSIDFromProgID( L"JavaScript" , &guid );

    IActiveScript      *script; hr = CoCreateInstance(guid, 0, CLSCTX_ALL,IID_IActiveScript,(void **)&script);  if(!script) return;
    IActiveScriptParse  *parse; hr = script->QueryInterface(IID_IActiveScriptParse, (void **)&parse);

    ScriptHost host;
    script->SetScriptSite((IActiveScriptSite *)&host);
    script->AddNamedItem(L"app",  SCRIPTITEM_ISVISIBLE|SCRIPTITEM_NOCODE );

    // sample JavaScript demonstrating regular expression, call of our own proc (+ exchanging params) and external dll api call (win32 api subset in this case)
    WCHAR* source = L" number = 'text 1'.match(/\\d/); result = app.test('hello'+' world '+number); "
                    L" app.MessageBoxA(0,'WIN'+32+' or any dll api called from JavaScript without need to install anything','hello world '+result,0)";

    hr = parse->InitNew(); 
    hr = parse->ParseScriptText( source ,0,0,0,0,0,0,0,0);
    script->SetScriptState( SCRIPTSTATE_CONNECTED);
    script->Close();
}; 

Points of Interest 

Most interesting part is Invoke. Since there you are called from script. Script will tell you via  Flags parameter whether he wana Get / Set variable or call procedure.

    // DISPATCH_PROPERTYGET
    // DISPATCH_PROPERTYPUT
    // DISPATCH_METHOD 

As I mentioned. If build-in JavaScript or VBscript is not for you and you don't mind installing something. Then for example look here for [other engines mentioned in aticle]

Another think that comes in my mind is support of thiscall  calling convention. That is calling C++ members of instanced objects. Well I wanted to keep sample simple. But I explain how call thicall works since it's simple. register it the same way via &member but put instance pointer in register ecx before actuall call

// thiscall -> C++ member call
_asm mov ecx, pointer_to_instance
_asm call id

As you can see thiscall is just standard cdecl (therefore use _ prefix in script) but relaing on this pointer being present in ecx.
Also beware that static class functions are normal stdcall.
And with Borland C++ compillers things with calling members are complicated since they love to use nonstandard fastcall (different with every compiller) which is mix of stdcall cdecl and thiscall with first three params passed in registers and on top of it they are in different order then rest of params. But you can usually just remove __fascall from declarations and you will get standardized thiscall again. Sometimes I feel like this thing is just used to mark borland territory ;D
Anyway I personally use just standard ones like stdcall since they work in any compiller plus speedup with others is virtually undetectable.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)