65.9K
CodeProject 正在变化。 阅读更多。
Home

通过极简方法为您的应用程序添加脚本支持

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (24投票s)

2009年3月13日

CPOL

4分钟阅读

viewsIcon

59271

downloadIcon

660

一种非常简单的方式为您的应用程序添加脚本支持,无需安装或分发任何东西

 下载 ScriptHost.zip - 2.64 KB

介绍  

应用程序开发人员经常会发现,他们没有时间通过发布越来越新的可执行文件来扩展程序的功能。仅仅因为用户对新的但小的功能和更大的灵活性的渴望永无止境。并且了解用户社区本身可以多么有创造力,下一步的演变通常很清楚...

让我们使整个事情可编写脚本

我通常会尝试使用操作系统中已有的任何库。我们在这里非常幸运。自从 Windows 95 以来,Windows 就附带了 Internet Explorer,Internet Explorer 反过来不断更新以支持最新版本的 JavaScript 和 VBScript。现在有趣的是,jscript.dll 和 vbscript.dll 就像前面提到的那样,是每个 Windows 版本的一部分。而且它们也只是脚本引擎。这意味着它们被编写成完全独立于使用它们的应用程序。这种聪明的想法当时被称为 Active Scripting。有很多可下载的 dll 引擎,为它编写的引擎,例如 Ruby Lua Python Perl Haskell...还有一些比较深奥的引擎 ;D 最棒的部分是不需要更改代码,除了将字符串从例如“JavaScript”更改为“RubyScript”。但是回到引擎。如果我过于简单化,那么我可以说它们所能做的只是基本操作,例如 A+B=C 加上正则表达式。使用它们最多的三个应用程序是 Microsoft 的 Web 服务器、Windows 脚本宿主和 Internet Explorer。例如。在此 Api 中,Internet Explorer 只是简单地实现了脚本宿主接口 = 只是注册了他的函数和变量(A、B、C= 本例中的完整 DOM 模型),从那一刻起,他里面的所有东西都是可编写脚本的。微软的人对这种新的灵活性印象深刻,以至于他们对自己说。“让我们注册我们能找到的每一个 win32 api 和 com 内容,只是为了好玩,并且忘记繁琐的 c++ 编译时间” 然后 Windows 脚本宿主诞生了。现在您可能认为“该死的,那将是另一个过于复杂深奥的 com api 类大杂烩” 那么您会惊讶于它实际上有多么容易。 只需使用适当的字符串(如“JavaScript”)选择脚本引擎,然后在 GetIDsOfNames 中注册您的过程和变量

示例

除了注册简单的测试函数外,该示例还注册了大多数 win32 api,以便您从一开始就可以使用。 这也可以让您了解例如 Windows 脚本宿主在内部是如何工作的。 至于现在。 示例支持两种主要的调用约定 stdcall 和 cdecl(在脚本中的 api 名称前加上前缀 _)。 但是函数返回类型(int 或 WCHAR*)的检测只是基本的,错误处理也是如此,因此您可以更多地关注它的工作原理。

#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();
}; 

兴趣点 

最有趣的部分是 Invoke。 因为你在那里是从脚本中被调用的。 脚本将通过 Flags 参数告诉您他是否要获取/设置变量或调用过程。

    // DISPATCH_PROPERTYGET
    // DISPATCH_PROPERTYPUT
    // DISPATCH_METHOD 

正如我所提到的。 如果内置的 JavaScript 或 VBscript 不适合您,并且您不介意安装一些东西。 那么例如在这里查找 [文章中提到的其他引擎]

另一个我想到的事情是支持 thiscall 调用约定。 也就是说,调用已实例化对象的 C++ 成员。 好吧,我想让示例简单。 但是我解释了 call thicall 的工作原理,因为它很简单。 通过 &member 以相同的方式注册它,但在实际调用之前将实例指针放入寄存器 ecx

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

正如您所看到的,thiscall 只是标准的 cdecl(因此在脚本中使用 _ 前缀),但依赖于 ecx 中存在的 this 指针。
还要注意,静态类函数是正常的 stdcall。
并且使用 Borland C++ 编译器,调用成员的事情变得复杂,因为他们喜欢使用非标准的 fastcall(每个编译器都不同),它是 stdcall cdecl 和 thiscall 的混合,前三个参数在寄存器中传递,而且它们的顺序与其余参数不同。 但是您通常只需从声明中删除 __fascall,您将再次获得标准 thiscall。 有时我觉得这东西只是用来标记 borland 领地的 ;D
无论如何,我个人只使用标准的东西,例如 stdcall,因为它们在任何编译器中都有效,并且与其他编译器相比,加速几乎无法检测到。

© . All rights reserved.