If you have played around with large applications, I'm sure you have been intrigued how they have been build to be extendable. The are multiple options
Obviously the 3rd is the best choice if you are developing a native (unmanaged) solution. The advantages are many because of low learning curve (any JScript programmer can write extensions), built in security, low-cost.
In this post I'll try to cover how you go about doing exactly that. I found little online documentation and took help of Kaushik from the JScript team to hack up some code to do this.
To host JScript you need to implement the IActiveScriptSite. The code below shows how we do that stripping out the details we do not want to discuss here (no fear :) all the code is present in the download pointed at the end of the post). The code below is in the file ashost.h
class IActiveScriptHost : public IUnknown { public: // IUnknown virtual ULONG __stdcall AddRef(void) = 0; virtual ULONG __stdcall Release(void) = 0; virtual HRESULT __stdcall QueryInterface(REFIID iid, void **obj) = 0; // IActiveScriptHost virtual HRESULT __stdcall Eval(const WCHAR *source, VARIANT *result) = 0; virtual HRESULT __stdcall Inject(const WCHAR *name, IUnknown *unkn) = 0; }; class ScriptHost : public IActiveScriptHost, public IActiveScriptSite { private: LONG _ref; IActiveScript *_activeScript; IActiveScriptParse *_activeScriptParse; ScriptHost(...){} virtual ~ScriptHost(){} public: // IUnknown virtual ULONG __stdcall AddRef(void); virtual ULONG __stdcall Release(void); virtual HRESULT __stdcall QueryInterface(REFIID iid, void **obj); // IActiveScriptSite virtual HRESULT __stdcall GetLCID(LCID *lcid); virtual HRESULT __stdcall GetItemInfo(LPCOLESTR name, DWORD returnMask, IUnknown **item, ITypeInfo **typeInfo); virtual HRESULT __stdcall GetDocVersionString(BSTR *versionString); virtual HRESULT __stdcall OnScriptTerminate(const VARIANT *result, const EXCEPINFO *exceptionInfo); virtual HRESULT __stdcall OnStateChange(SCRIPTSTATE state); virtual HRESULT __stdcall OnEnterScript(void); virtual HRESULT __stdcall OnLeaveScript(void); virtual HRESULT __stdcall OnScriptError(IActiveScriptError *error); // IActiveScriptHost virtual HRESULT __stdcall Eval(const WCHAR *source, VARIANT *result); virtual HRESULT __stdcall Inject(const WCHAR *name, IUnknown *unkn); public: static HRESULT Create(IActiveScriptHost **host) { ... } };
Here we are defining an interface IActiveScriptHost. ScriptHost implements the IActiveScriptHost and also the required hosting interface IActiveScriptSite. IActiveScriptHost exposes 2 extra methods (in green) that will be used from outside to easily host js scripts.
In addition ScriptHost also implements a factory method Create. This create method does the heavy lifting of using COM querying to get the various interfaces its needs (IActiveScript, IActiveScriptParse) and stores them inside the corresponding pointers.
So the client of this host class creates the ScriptHosting instance by using the following (see ScriptHostBase.cpp)
IActiveScriptHost *activeScriptHost = NULL; HRESULT hr = S_OK; HRESULT hrInit = S_OK; hrInit = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if(FAILED(hr)) throw L"Failed to initialize"; hr = ScriptHost::Create(&activeScriptHost); if(FAILED(hr)) throw L"Failed to create ScriptHost";
With this the script host is available through activeScriptHost pointer and we already have JScript engine hosted in our application
Post hosting we need to make it do something interesting.This is where the IActiveScriptHost::Eval method comes in.
HRESULT __stdcall ScriptHost::Eval(const WCHAR *source, VARIANT *result) { assert(source != NULL); if (source == NULL) return E_POINTER; return _activeScriptParse->ParseScriptText(source, NULL, NULL, NULL, 0, 1, SCRIPTTEXT_ISEXPRESSION, result, NULL); }
Eval accepts a text of the script, makes it execute using IActiveScriptParse::ParseScriptText and returns the result.
So effectively we can accept input from the console and evaluate it (or read a file and interpret the complete script in it.
while (true) { wcout << L">> "; getline(wcin, input); if (quitStr.compare(input) == 0) break; if (FAILED(activeScriptHost->Eval(input.c_str(), &result))) { throw L"Script Error"; } if (result.vt == 3) wcout << result.lVal << endl; }
So all this is fine and at the end you can run the app (which BTW is a console app) and this what you can do.
JScript sample Host q! to quit >> Hello = 7 7 >> World = 6 6 >> Hello * World 42 >> q! Press any key to continue . . .
So you have extended your app to do maths for you or rather run basic scripts which even though exciting but is not of much value.
Once we are past hosting the engine and running scripts inside the application we need to go ahead with actually building the application's DOM and injecting it into the hosting engine so that JScript can extend it.
If you already have a native application which is build on COM (IDispatch) then you have nothing more to do. But lets pretend that we actually have nothing and need to build the DOM.
To build the DOM you need to create IDispatch based DOM tree. There can be more than one roots. In this post I'm not trying to cover how to build IDispatch based COM objects (which you'd do using ATL or some such other means). However, for simplicity we will roll out a hand written implementation which implements an interface as below.
class IDomRoot : public IDispatch { // IUnknown virtual ULONG __stdcall AddRef(void) = 0; virtual ULONG __stdcall Release(void) = 0; virtual HRESULT __stdcall QueryInterface(REFIID iid, void **obj) = 0; // IDispatch virtual HRESULT __stdcall GetTypeInfoCount( UINT *pctinfo) = 0; virtual HRESULT __stdcall GetTypeInfo( UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) = 0; virtual HRESULT __stdcall GetIDsOfNames( REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) = 0; virtual HRESULT __stdcall Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) = 0; // IDomRoot virtual HRESULT __stdcall Print(BSTR str) = 0; virtual HRESULT __stdcall get_Val(LONG* pVal) = 0; virtual HRESULT __stdcall put_Val(LONG pVal) = 0; };
At the top we have the standard IUnknown and IDispatch methods and at the end we have our DOM Root's methods (in blue). It implements a Print method that prints a string and a property called Val (with a set and get method for that property).
The class DomRoot implements this method and an additional method named Create which is the factory to create it. Once we are done with creating this we will inject this object inside the JScript scripting engine. So our final script host code looks as follows
IActiveScriptHost *activeScriptHost = NULL; IDomRoot *domRoot = NULL; HRESULT hr = S_OK; HRESULT hrInit = S_OK; hrInit = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if(FAILED(hr)) throw L"Failed to initialize"; // Create the host hr = ScriptHost::Create(&activeScriptHost); if(FAILED(hr)) throw L"Failed to create ScriptHost"; // create the DOM Root hr = DomRoot::Create(&domRoot); if(FAILED(hr)) throw L"Failed to create DomRoot"; // Inject the created DOM Root into the scripting engine activeScriptHost->Inject(L"DomRoot", (IUnknown*)domRoot);
What happens with the inject is as below
map rootList; typedef map::iterator MapIter; typedef pair InjectPair; HRESULT __stdcall ScriptHost::Inject(const WCHAR *name, IUnknown *unkn) { assert(name != NULL); if (name == NULL) return E_POINTER; _activeScript->AddNamedItem(name, SCRIPTITEM_GLOBALMEMBERS | SCRIPTITEM_ISVISIBLE ); rootList.insert(InjectPair(std::wstring(name), unkn)); return S_OK; }
In inject we store the name of the object and the corresponding IUnknown in a map (hash table). Each time the script will encounter a object in its code it calls GetItemInfo with that objects name and we then de-reference into the hash table and return the corresponding IUnknown
HRESULT __stdcall ScriptHost::GetItemInfo(LPCOLESTR name, DWORD returnMask, IUnknown **item, ITypeInfo **typeInfo) { MapIter iter = rootList.find(name); if (iter != rootList.end()) { *item = (*iter).second; return S_OK; } else return E_NOTIMPL; }
After that the script calls into that IDispatch to look for properties and methods and calls into them.
By now we have seen a whole bunch of code. Let's see how the whole thing works together. Let's assume we have a extension written in in JScript and it calls DomRoot.Val = 5; this is what happens to get the whole thing to work
JScript sample Host q! to quit >> DomRoot.Val = 5; 5 >> DomRoot.Val = DomRoot.Val * 10 50 >> DomRoot.Val 50 >> DomRoot.Print("The answer is 42");The answer is 42
First of all the disclaimer. Let me get it off my chest by saying that the DomRoot code is a super simplified COM object. It commits nothing less than sacrilege. You shouldn't treat it as a sample code. I intentionally didn't do a full implementation so that you can step into it without the muck of IDispatchImpl or ATL coming into your way.
However, you can treat the script hosting part (ashost, ScriptHostBase) as sample code (that is the idea of the whole post :) )
The code organization is as follows
ashost.cpp, ashost.h - The Script host implementation DomRoot.cpp, DomRoot.h - The DOM Root object injected into the scripting engine ScriptHostBase.cpp - Driver
Note that in a real life example the driver should load jscript files from a given folder and execute it.
Download from here