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






4.66/5 (24投票s)
一种非常简单的方式为您的应用程序添加脚本支持,无需安装或分发任何东西
下载 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,因为它们在任何编译器中都有效,并且与其他编译器相比,加速几乎无法检测到。