Original article: http://www.codeproject.com/Articles/17038/COM-in-plain-C-part-8
Download routine-419kb
Content
- Introduction
- Script code persistence
- Script code and "Naming item"
- Call a specific function in the script
- Query/set the variable value in the script
- Query/set the variable value in the script
Introduction
In the previous chapter, we learned how to create an ActiveX Script Host. Although these chapters cover most of the writing of a script host, there are other features that your script host may want to use more esoteric (esoteric, confidential. This section describes some of the confidential features in detail.
Script code persistence
In the previous Script Host example, we userunScript
Function to open the script engine, run the script, and then close the engine. In this function, the script engine is loaded/analyzed, executed, and then released (after the execution is complete ).
However, sometimes you want to add some scripts to the engine and keep them in the engine even if these scripts are not executed. Possibly, you want these scripts to be called by any other scripts that can be recognized by iactivescript of the same engine. In fact, you may also want to use these scripts as a set of "Ram-based macros. ActiveX script engine makes this possible. But we also need to change our approach from the following two aspects:
- When we add a script as a macro, we need
ParseScriptText
The function specifies the scripttext_ispersistent flag. This tells the engine to keep the scripts that have been analyzed and loaded internally, even after parsescripttext is returned.
- We cannot release the iactivescript object of the engine before all macros are used. If this is done, the macros will eventually be detached.
The best practice is to add these macros when the engine is in the initialized state, but before the engine is set to the started or connected state.ParseScriptText
Instead of trying to run these scripts, we analyze their syntax andParseScriptText
Save the scripts internally. These scripts reside in the engine until we release the iactivescript object of the engine. Even in the middle, we call other scripts or methods.
InScripthost7Directory, you will find an example to illustrate this. We add a VB script to the engine and specify the scripttext_ispersistent flag. For simplicity, this VB script is used as a global variable and is embedded in our EXE as follows:
wchar_t VBmacro[] = L"Sub HelloWorld\r\nMsgBox \"Hello world\"\r\nEnd Sub";
The above code is a "hello World" subroutine of VB. It only pops up a message box.
Next, we embed the second VB script. This VB script only calls the first loaded helloworld script program.
wchar_t VBscript[] = L"HelloWorld";
When loading the second script, we do not specify the scripttext_ispersistent flag.
To ensure that persistent scripts can work, the runscript thread must always maintain the iactivescript object of the VB engine until the program is terminated. To accomplish this, we start the thread at the beginning of the Program (instead of starting the thread when running the script ). This thread remains valid until the main program ends. First, the engine calls cocreateinstance to obtain the iactivescript of the VB engine, then calls the QueryInterface of iactivescript to obtain the iactivescriptparse object of the engine, then calls the initnew initialization engine, and finally calls setscriptsite
Iacticescriptsite is passed to the engine. The initialization section is the same as the examples in the previous chapters.
Runscript will callParseScriptText
To load our "VB macro" to the engine. This is almost the same as the example program. Except for the scripttext_ispersistent flag:
activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &VBmacro[0], 0, 0, 0, 0, 0, SCRIPTTEXT_ISPERSISTENT, 0, 0);
After this script is added, the runscript thread waits until the main thread sets an event semaphore to wake up and then runs the Second Script (it will call the first script we loaded ).
The main window has a "run script" button. When you click, the main thread sets the event semaphore.
// Let the script thread know that we want it to run a scriptSetEvent(NotifySignal[0]);
The running thread is called after it is awakened.ParseScriptText
The function loads the Second Script and then callsSetScriptState
Function, set the status of the VB Script Engine to scriptstate_connected. This causes the second script to run. The second script calls the "Hello World" subprogram in the macro to bring up a message box. After you close the message box, the first script stops running,SetScriptState
Function return.
At this time, we alsoNoRelease (Rlease) iactivescriptparse and iactivescript objects, nor close the engine. We callSetScriptState
Set the status to scriptstate_initialized. In this way, the Second Script is uninstalled, but the macro script is not uninstalled because it is persistent.
The running thread sleep again. Wait for the user to click "run script" again. In this event, the running thread repeatedly loads/runs the second script. But note that we do not need to reload the macro script, it is always kept in the engine.
This is the "script loop" in the running thread ":
for (;;){ // Wait for main thread to signal us to run a script. WaitForSingleObject(NotifySignal, INFINITE); // Have the script engine parse our second script and add it to // the internal list of scripts to run. NOTE: We do NOT specify // SCRIPTTEXT_ISPERSISTENT so this script will be unloaded // when the engine goes back to INITIALIZED state. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &VBscript[0], 0, 0, 0, 0, 0, 0, 0, 0); // Run all of the scripts that we added to the engine. EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript, SCRIPTSTATE_CONNECTED); // The above script has ended after SetScriptState returns. Now // let's set the engine state back to initialized to unload this // script. VBmacro[] remains still loaded. EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript, SCRIPTSTATE_INITIALIZED);}
Script code and "Naming item"
In the above example, we add a macro script to the same "Naming item" as the second script. (Note: We didn't specify a specific naming item, so the engine uses its default global item ). However, you can load scripts under different "Naming items.
In the previous chapter, you should remember that you can create a naming item (using the addnameditem function of the iactivescript object of the engine) so that the script can call our own C function.
However, this is not the only use of naming items. We can also group created named items. This is our example:
Assume that we have two C source files:File1.cAndFile2.c. The content of file1.c is as follows:
// File1.cstatic void MyFunction(void){ printf("File1.c");}static void File1(void){ MyFunction();}
This is the content of file2.c:
// File2.cstatic void MyFunction(const char *ptr){ printf(ptr);}static void File2(void){ MyFunction("File1.c");}
Note the following:
- Because of static keyword modification
MyFunction
And in file2.cMyFunction
It's different. We can compile and connect two source files together without any problems (that is, there is no name conflict ).
- Because of the static keyword modification, functions in file1.c cannot call functions in file2.c, and vice versa.
When we create a naming item (in the script code we want to load), we will regard it as creating a C source file. To create a naming item, we callAddNamedItem
. Suppose we have a script engine implemented in C language. First, we need to call addnameditem twice. For the first time, we createFile1.cAs the name. The second time we createFile2.cAs the name. This creates two "source files" in the engine, and then we will call parsescripttext to load the content of file1.c
File1.c. Therefore, we must pass the name of the name item to parsescripttext as the third parameter. Then, we load the content of file2.c to file2.c. Our specific practices are as follows:
// Here's the contents of file1.cwchar _ t file1 [] = l "static void myfunction (void) {printf (\" file1.c \ ");} static void file1 (void) {myfunction () ;}"; // here's the contents of file2.cwchar _ t file1 [] = l "static void myfunction (const char * PTR) {printf (PTR);} static void file2 (void) {myfunction (\ "file1.c \") ;}"; // create the file1.c named item. error-checking omitted! Engineactivescript-> lpvtbl-> addnameditem (engineactivescript, "file1.c", 0); // create the file2.c named item. engineactivescript-> lpvtbl-> addnameditem (engineactivescript, "file2.c", 0); // Add the file1.c contents to the file1.c named parameters-> lpvtbl-> parsescripttext (activescriptparse, & file1 [0], "file1.c", 0, 0, 0, 0, 0, 0); // Add the file2.c contents to the file2.c named objectact Ivescriptparse-> lpvtbl-> parsescripttext (activescriptparse, & file2 [0], "file2.c", 0, 0, 0, 0, 0, 0, 0, 0 ); now we place the VB "macro script" in the created naming item. Name this item as mymacro. Our specific practices are as follows: // The Name Of The named itemwchar_t mymacroobjectname [] = l "mymacro"; // create the mymacro named itemengineactivescript-> lpvtbl-> addnameditem (engineactivescript, & mymacroobjectname [0], role | role); // Add the contents of vbmacro to the mymacro named itemactivescriptparse-> lpvtbl-> parsescripttext (activescriptparse, & vbmacro [0], & mymacroobjectname [0], 0, 0, 0, 0, scriptitem_isvisible | scripttext_ispersistent, 0, 0 );
You will notice the signature parameters we passed to addnameditem. We have specified scriptitem_ispersistent, because we do not want the engine to delete this name item (and its content) When we reset it to the initialized state ). We also set the scriptitem_isvisible flag, because we want this naming item to be accessible by the default global item (that is, the Second Script obtains the place where the added item is located. Setting the scriptitem_isvisible flag is equivalent to deleting the static keyword of the function in the C language engine example. This allows a function of a naming item to be called by a function of another naming item. If there is no scriptitem_isvisible,
A function can be called by itself, but cannot be called by any other function.
We must modify the second VB script. Now, when it calls the helloworld subroutine, it needs to reference the naming item. In VBScript, this is done by using its name as an object:
wchar_t VBscript[] = L"MyMacro.HelloWorld";
One more thing. When addnameditem is called to create "mymacro", the engine callsGetItemInfo
And pass its name "mymacro" as the parameter. We need to obtain and return an idispatch pointer for this naming item. Where does this idispatch come from? We call the iactivescript objectGetScriptDispatch
Function. This is what our iactivescriptsiteGetItemInfo
Function:
STDMETHODIMP GetItemInfo(MyRealIActiveScriptSite *this, LPCOLESTR objectName, DWORD dwReturnMask, IUnknown **objPtr, ITypeInfo **typeInfo){ if (dwReturnMask & SCRIPTINFO_IUNKNOWN) *objPtr = 0; if (dwReturnMask & SCRIPTINFO_ITYPEINFO) *typeInfo = 0; // We assume that the named item the engine is asking for is our // "MyMacro" named item we created. We need to return the // IDispatch for this named item. Where do we get it? From the engine. // Specifically, we call the engine IActiveScript's GetScriptDispatch(), // passing objectName (which should be "MyMacro"). if (dwReturnMask & SCRIPTINFO_IUNKNOWN) return(EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript, objectName, objPtr)); return(E_FAIL);}
After the above modification, the macro script now has a naming item. What are the benefits? First, we can have a "hello World" subroutine in our second script, which will not conflict with the "Hello World" subroutine of mymacro. Therefore, we can now exclude the naming conflicts between macro scripts and the second script code. In addition, if there are more macro scripts, we can put them in their respective naming items. In this way, macro scripts can have subprograms/functions with the same name, but there is no name conflict between them. The script engine knows that subroutine/function is called, because the name of the naming item specifies the name of the subroutine/function.
Note:**************************************** * **************** Differences Between Routines and functions ------------------------- routines: private subABC(A as integer, B as integer, C as integer) 'your code ....'C = a + BEnd sub function: Private functionC(A as integer, B as integer) as integer 'your code ....'C = a + BEnd function -----------------------------************************************* ********************
To sum up, using a naming item can avoid naming conflicts when adding subprograms/functions and global variables to the engine.
Call a specific function in the scriptIn the preceding exampleGetScriptDispatch
To obtain the idispatch of a specific naming item, we simply return the idispatch to the engine.
However, in addition, we can use this idispatch to directly call (in a specific naming item) the VB subroutine/function. To call the subroutine/function, we need to callGetIDsOfNames
AndInvoke
Function. This is the sameIexampleapp3In the example, when we use the invoke of idispatch to call functions in the COM object, it is similar. Maybe you should read the example carefully to remember how we did it. Now, we need to directly call the helloworld subroutine in the mymacros naming item. First, we need to use the iactivescript objectGetScriptDispatch
Function to obtain the idispatch of the naming item. Then callGetIDsOfNames
Get
The engine is used to identify the unique dispid of the function we want to call.
// NOTE: Error-checking omitted!IDispatch *objPtr;DISPID dispid;OLECHAR *funcName;DISPPARAMS dspp;VARIANT ret;// Get the IDispatch for "MyMacro" named itemEngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript, "MyMacro", &objPtr);// Now get the DISPID for the "HelloWorld" subfuncName = (OLECHAR *)L"HelloWorld";objPtr->lpVtbl->GetIDsOfNames(objPtr, &IID_NULL, &funcName, 1, LOCALE_USER_DEFAULT, &dispid);// Call HelloWorld.// Since HelloWorld has no args passed to it, we don't have to do// any grotesque initialization of DISPPARAMS.ZeroMemory(&dspp, sizeof(DISPPARAMS));VariantInit(&ret);objPtr->lpVtbl->Invoke(objPtr, dispid, &IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dspp, &ret, 0, 0);VariantClear(&ret);// Release the IDispatch now that we made the callobjPtr->lpVtbl->Release(objPtr);
InScripthost8Is an example of adding the following VB script (including the main subroutine) to the VB engine:
wchar_t VBscript[] = L"Sub main\r\nMsgBox \"Hello world\"\r\nEnd Sub";
Then we call this main program directly. One thing you should note is that we did not create/specify any specific naming items when calling parsescripttext. Therefore, this script code is added as the default "Global naming item. Therefore, we need to obtain its idispatch from the global naming item. How can we do this? Let's pass a 0 value to getscriptdispatch as the name. This is a specified value, telling getscriptdispatch to return the idispatch of the global naming item.
Query/set the variable value in the scriptTo query or set a variable (in the specified name), we have to do almost the same thing as above. The only difference is that the invoke call specifies the dispatch_propertyget flag when obtaining the value. When setting the value, we set the dispatch_propertyput flag. This is an example of setting the value of "myvariable" in the "mymacro" Naming item:
// NOTE: Error-checking omitted!IDispatch *objPtr;DISPID dispid, dispPropPut;OLECHAR *varName;DISPPARAMS dspp;VARIANT arg;// Get the IDispatch for "MyMacro" named itemEngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript, "MyMacro", &objPtr);// Now get the DISPID for the "MyVariable" variable (ie, property)varName = (OLECHAR *)L"MyVariable";objPtr->lpVtbl->GetIDsOfNames(objPtr, &IID_NULL, &varName, 1, LOCALE_USER_DEFAULT, &dispid);// Set the value to 10.VariantInit(&arg);ZeroMemory(&dspp, sizeof(DISPPARAMS));dspp.cArgs = dspp.cNamedArgs = 1;dispPropPut = DISPID_PROPERTYPUT;dspp.rgdispidNamedArgs = &dispPropPut;dspp.rgvarg = &arg;arg.vt = VT_I4;arg.lVal = 10;objPtr->lpVtbl->Invoke(objPtr, dispid, &IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, &dspp, 0, 0, 0);VariantClear(&arg);// Release the IDispatch now that we made the callobjPtr->lpVtbl->Release(objPtr);
InScripthost9In this example, we first set the value of myvariable, and then call the mian subroutine to display this variable.
Interaction in different languagesScripts written in one language can also call scripts written in another language. For example, suppose we have the following VB Function to display a message box:
Sub SayHello MsgBox "Hello World"End Sub
Let's assume that we use the following JScript function to call the above VBScript function:
function main(){ SayHello();}
First, because we will use two different languages of scripts, JScript and VBScript, we need to callCoCreateInstance
; Iactivescript of the JScript engine and iactivescript of the VBScript engine. Of course, we need to save the two pointers to the two variables respectively (respectively:JActiveScript
AndVBActiveScript
).
We also need to get the iactivescriptparse of each engine. At the same time, we also need to callSetScriptSite
To pass iactivesscriptsite to the engine (we can pass different iactivesscriptsite for each engine separately, but here we pass it to the same iactivesscriptsite for each engine, because we will not run scripts in two languages at the same time, the VB engine is used only when the JScript engine calls the VBScript function ).
In other words,runScript
You must complete the initialization required to use the script engine, but each engine only needs to be initialized once.
Then, we need to callParseScriptText
Add the preceding JScript code to the JScript engine, and callParseScriptText
Add the above VBScript code to the VBScript engine. We will add the code to the global naming items of the two engines respectively.
To facilitate JScript to call VBScript, we need to create a naming item in the JScript engine to interact with VBScript. This should be done before adding the script to the engine. We can name this name as "VB ".
JActiveScript->lpVtbl->AddNamedItem(JActiveScript, L"VB", SCRIPTITEM_GLOBALMEMBERS|SCRIPTITEM_ISVISIBLE);
Let's take a look at what happens when the JScript engine runs the above JScript code. The engine checks all the JScript functions we load in JScript and does not find the JScript function "sayhello. Because we have added some naming items to the JScript engine (set the getidsofnames flag), the engine will say to itself:"Er... maybe the sayhello function is in a naming item. I need to get the idispatch corresponding to this naming item, and query the dispid of sayhello by calling its getidsofnames function. If the idispatch succeeds, the system returns the dispid, I will call the invoke of idispatch to call the sayhello function ".
But how does the engine obtain the idispatch of the naming item? So far, you should know that by calling our iactivescriptsiteGetItemInfo
. In this case, the JScript engine transmits a "VB" parameter for the name of the name ". This is a bit confusing. When our getiteminfo finds the specified item, we call the VBScript engine.GetScriptDispatch
To obtain the global name of the VBScript engine, which is exactly what we want to return to the JScript engine.
Yes, you are not mistaken. When the JScript engine requests the idispatch of the "VB" Naming item, we actually return the idispatch of the global naming item of the VBScript engine. Why? Because our VBScript sayhello function is added to the global naming item of the VB engine, rather than in the "VB" Naming item. In other words, we use the "VB" name In JScript asProxy (placeholder). The jscript engine does not need to know the "VB" Naming item. In fact, it returns the idispatch of the global naming item of the VBScript engine.
Then, the JScript engine calls the idsipatch of the global naming item of the VBScript engine.GetIDsOfNames
Of course, the VB engine will return the dispid of the sayhello function. When JScript calls the inovke of idispatch, it enters the VB engine and allows the VB engine to run the sayhello function of VB.
Here is our iactivescriptsiteGetItemInfo
:
STDMETHODIMP GetItemInfo(MyRealIActiveScriptSite *this, LPCOLESTR objectName, DWORD dwReturnMask, IUnknown **objPtr, ITypeInfo **typeInfo){ HRESULT hr; hr = E_FAIL; if (dwReturnMask & SCRIPTINFO_ITYPEINFO) *typeInfo = 0; if (dwReturnMask & SCRIPTINFO_IUNKNOWN) { *objPtr = 0; // If the engine is asking for our "VB" named item we created, // then we know this is the JScript engine calling. We need to // return the IDispatch for VBScript's "global named item". if (!lstrcmpW(objectName, L"VB")) { hr = VBActiveScript->lpVtbl->GetScriptDispatch(VBActiveScript, 0, objPtr); } } return(hr);}
Scripthost10Is an example of using JScript to call VBScript.
By the way, you may be a bit strange about the scriptitem_globalmembers mark. Let's look back at the previous process of a naming item. The script must reference the name of the item like an object name, for example:
VB.SayHello()
When you use the scriptitem_globalmembers flag to create an item, it indicates that the object name is optional. For example, the above Code can work or be used as follows:
SayHello()
So what we do is to make JScript call the sayhello function of VB script like calling anotherLocal)JScript function. In other words, it is more or less a shortcut to hide the troublesome details of naming items in the theoretical sense.
However, this benefit has to be paid. As with global items, these items marked with scriptitem_globalmembers may have name conflicts.