I have recently needed to interact with some common ActiveX / COM ( Component Object Model ) objects via plain old C. A handful of examples can be found on the Internet, but the process of orchestrating the data-structures and function-calls that are necessary can initially be a little daunting.
As I pored over the documentation, I began to realize why most C++ IDE’s will generate a very nice abstraction layer to represent a COM control in software. I was going to have to code some very specific code for each type of COM object that I wanted to instantiate.
I had hoped to be able to build a general library that would allow me to access COM components at will from a C program.
The more I dug into the subject, the more I came to realize that I would benefit from some sort of embedded interpreter that could abstract the COM interfaces for me. Many scripting languages provide some sort of simple way to handle COM.
The Microsoft ScriptControl object is itself a COM object that exposes scripting engines to a COM client. I had learned enough about creating a COM client in C that I decided to try to build a library of functions that would evaluate JavaScript expressions via the ScriptControl. I could then use embedded JavaScript to create and interact with COM objects.
Please note that the ScriptControl object may not already be installed on a given Windows machine. If the control isn’t present on your machine, please look for a recent version online.
As part of the library, I needed functions that would convert between ANSI strings and Unicode OLE strings. I found the two functions I needed here:
http://support.microsoft.com/kb/138813
I have included these functions in a separate file ( unicode_conv.c ) in the archive listed at the end of this file, but I do not claim any kind of copyright on them.
My interface to the ScriptControl is in this library:
com_script.c
// Plain C interface to the ScriptControl object
//
// License: MIT / X11
// Copyright (c) 1999. 2009 by James K. Lawless
// jimbo@radiks.net http://www.radiks.net/~jimbo
// http://www.mailsend-online.com
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#include <windows.h>
#include <stdio.h>
#include "unicode_conv.h"
void javascript(LPOLESTR expression) {
HRESULT hr;
IDispatch *scrDisp;
WCHAR *tmpPtr;
DISPID dispID;
CLSID scrClsid;
VARIANT parm;
VARIANT result;
char *msg;
DISPPARAMS dispParams = { NULL, NULL, 0, 0 };
DISPID dispidNamed = DISPID_PROPERTYPUT;
{
// Get CLSID for ScriptControl Application from registry.
hr = CLSIDFromProgID(L"ScriptControl", &scrClsid);
if(FAILED(hr)) {
printf("ScriptControl not found.\n");
return;
}
// Start Scriptcontrol and get its IDispatch pointer.
hr = CoCreateInstance(&scrClsid, NULL,
CLSCTX_LOCAL_SERVER|CLSCTX_INPROC_SERVER,
&IID_IDispatch, (void **)&scrDisp);
if(FAILED(hr)) {
printf("Could not create instance of ScriptControl.\n");
return;
}
// Get the 'Language' property's DISPID.
tmpPtr = L"Language";
scrDisp->lpVtbl->GetIDsOfNames(scrDisp, &IID_NULL, &tmpPtr, 1,
LOCALE_USER_DEFAULT, &dispID);
VariantInit(&parm);
parm.vt = VT_BSTR;
parm.bstrVal = SysAllocString( OLESTR("JavaScript"));
dispParams.cArgs = 1;
dispParams.rgvarg = &parm;
dispParams.cNamedArgs = 1;
dispParams.rgdispidNamedArgs = &dispidNamed;
hr = scrDisp->lpVtbl->Invoke(scrDisp,
dispID, &IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_PROPERTYPUT | DISPATCH_METHOD,
&dispParams, NULL, NULL, NULL
);
if(FAILED(hr)) {
printf("Could not change Language property to 'JavaScript'. HRESULT=%08lx\n",hr);
}
tmpPtr = L"Eval";
scrDisp->lpVtbl->GetIDsOfNames(scrDisp, &IID_NULL, &tmpPtr, 1,
LOCALE_USER_DEFAULT, &dispID);
VariantInit(&parm);
parm.vt = VT_BSTR;
parm.bstrVal = SysAllocString( expression);
dispParams.cArgs = 1;
dispParams.rgvarg = &parm;
dispParams.cNamedArgs = 0;
VariantInit(&result);
hr = scrDisp->lpVtbl->Invoke(scrDisp,
dispID, &IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_PROPERTYPUT | DISPATCH_METHOD,
&dispParams, &result, NULL, NULL
);
if(FAILED(hr)) {
printf("Call to Eval() failed. HRESULT=%08lx", hr);
return;
}
switch(result.vt) {
case VT_EMPTY:
printf("No return value.");
break;
case VT_NULL:
printf("NULL return value.");
break;
case VT_I4: // integer
printf("Result: %d\n",result.intVal);
break;
case VT_BSTR:
UnicodeToAnsi(result.bstrVal,&msg);
printf("Result: %s\n",msg);
break;
default:
printf("Unhandled VARIANT type %d in result.\n",result.vt);
}
}
}
The first thing the function does is to look up the CLSID for the ScriptControl object. After attaining the CLSID, the function instantiates an instance of the control. ( Please note that I have not provided a way to release this object, yet. This code is a work-in-progress. ).
Once the ScriptControl object is instantiated, the function sets the Language property to “JavaScript”. It then takes the Unicode string passed in as a parameter and invokes the object’s Eval() method.
If the type of the resultant VARIANT object is in the set of handled types, a message will be displayed on the console containing the return-value.
To test the library, I first tried to use the speech API:
speech_test.c
// Invoke Speech API via JavaScript via ScriptControl
//
// License: MIT / X11
// Copyright (c) 1999. 2009 by James K. Lawless
// jimbo@radiks.net http://www.radiks.net/~jimbo
// http://www.mailsend-online.com
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#include <windows.h>
#include <stdio.h>
#include "com_script.h"
#include "unicode_conv.h"
void javascript(LPOLESTR );
void main(void) {
LPOLESTR oleString;
OleInitialize(NULL);
AnsiToUnicode(
"var voice=new ActiveXObject('SAPI.SpVoice');"
"function speak(s) {"
" voice.Speak(s,1);"
" voice.WaitUntilDone(-1);"
"}"
"speak('I like peanut butter.');",
&oleString);
javascript(oleString);
CoTaskMemFree(oleString);
OleUninitialize();
}
To compile the source enter the line:
com_comp.bat speech_test.c
The source for com_comp.bat is as follows:
cl %1 com_script.c unicode_conv.c /link ole32.lib user32.lib oleaut32.lib uuid.lib
When you run the program, you should hear your computer speak the words “I like peanut butter”, if you have the speech API installed.
A more generic testbed is as follows ( note that I never release the ScriptControl object in this example … that should be added before placing any code into a production environment that uses this technique. )
js_test.c
// Test JavaScript COM scripting
//
// License: MIT / X11
// Copyright (c) 1999. 2009 by James K. Lawless
// jimbo@radiks.net http://www.radiks.net/~jimbo
// http://www.mailsend-online.com
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#include <windows.h>
#include <stdio.h>
#include "com_script.h"
#include "unicode_conv.h"
void javascript(LPOLESTR );
void main(void) {
LPOLESTR oleString;
char buff[2048];
OleInitialize(NULL);
printf("Enter a JavaScript expression:\n");
while(fgets(buff,2047,stdin)!=NULL) {
AnsiToUnicode(buff,&oleString);
javascript(oleString);
CoTaskMemFree(oleString);
printf("\nEnter a JavaScript expression:\n");
}
OleUninitialize();
}
Compile the above using the command:
com_comp.bat js_test.c
Then, run js_test.exe. You will be prompted to enter a JavaScript expression. The program will Eval() the expression and will display the result.
If you enter the expression:
2+2
…you should see the result 4.
If you enter the expression:
parseInt(Math.random()*444)
…you’ll hopefully see an integer in the range of 0 to 443 inclusive.
Note that you cannot call methods like alert() or WScript.Echo() as they are exposed to the ScriptControl by their respective container programs.
I now have what I consider to be the precursor to an appropriate embeddable ScriptControl library.
I have yet to do the following:
- Separate the ScriptControl instantiation from the Eval() call
- Provide a function to release the instantiated ScriptControl object
- Add support for the Clear() and Reset() method calls
- Add support for the AddObject() method call to allow the JavaScript library to be able to call functions exposed in the C code
- Add a better way to more generally handle the VARIANT result
- Add support for VBScript as an alternative
- Bottle the whole thing up in a DLL
All source code, executable files, and compile batch file can be downloaded from a single archive at:
http://www.mailsend-online.com/wp/comscript.zip
Save to del.icio.us
Digg it
Save to Reddit
Share on Facebook
Share on Twitter
More bookmarks
Unless otherwise noted, all code and text entries are Copyright © 2010 by James K. Lawless
