HowTo: Create a function-library with Windows Script Components (WSC)

WSCs - A Quick How-To:
This is a quick demonstration of how to use a windows script component, to create an external library with script functions and objects, that can be re-used within DO by any button or script-addin. Actually it can be used by any software development environment on windows (wsh, perl, asp, python, php, c#, c++, etc.).

Introduction:
A windows script component basically acts like a regular DLL, but it's functionality can be pure script code (jscript or vbscript by default).
This demo makes use of jscript to create a DOpus helper object, which offers the following two methods.
GetStringLength(s) - returns the length of the passed string
Sum(a,b) - sums and returns two variables
It also has a property "MyProperty" which will be fetched and set and then refetched to, well just to show-case that it works! o)

Installation:
Download the "DOpusHelper.wsc.txt" file, remove the "*.txt" extension and put it anywhere you like. I suggest "/dopusdata/scripts". Then run "regsvr32.exe x:\path\to\DOpusHelper.wsc" in a command prompt to register the script component once! You don't need to repeat this step, whenever you change or extend the script component!

Now copy/paste or download the button below and put it to work on a DOpus toolbar. Make sure to toggle the "Other Logs" utility pane visible to see it's output.

Usage:
Hit the button and see the DOpus log output. You should see something like this:

Windows Script Component - Demo:
WSC-Function 'GetStringLength()' returned: 12
WSC-Function 'Sum()' returned: 3
WSC-Property (get #1): MyValue
WSC-Property (get #2): I changed it from DO!
Resources:
This is the button that "loads" the external helper (the windows script component) and uses it's functionality.

function OnClick( data ){
	DOpus.Output("Windows Script Component - Demo:");

	//creating an instance of our scripted helper
	var helper = new ActiveXObject("DOpusHelper.wsc");

	//calling GetStringLength() from helper
	var length = helper.GetStringLength( 'hello world!');
	DOpus.Output("WSC-Function 'GetStringLength()' returned: " + length);

	//calling Sum() from helper
	var a = 1;
	var b = 2;
	DOpus.Output("WSC-Function 'Sum()' returned: " + helper.Sum( a, b));

	//getting/setting a property of the helper
	DOpus.Output("WSC-Property (get #1): " + helper.MyProperty);
	helper.MyProperty = "I changed it from DO!";
	DOpus.Output("WSC-Property (get #2): " + helper.MyProperty);
}

WSC-Demo_DOpusHelper.dcf (1.78 KB)

This is the content of "DOpusHelper.wsc", as you can see, it is basically XML with injected jscript code. Any decent windows text editor should be able to display this file with proper syntax highlighting, even though xml and jscript are mixed up (Editplus can).

<?xml version="1.0"?>
<component>
<?component error="true" debug="true"?>
<registration
	description="DOpusHelper.wsc"
	progid="DOpusHelper.wsc"
	version="1.00"
	classid="{92012337-9912-4abb-a612-8f3226dc126a}">
</registration>

<public> 
	<!-- ////////////////////////////////////////////////////////////////// -->
	<property name="MyProperty" get="get_MyProperty" put="put_MyProperty"></property>
	
	<!-- ////////////////////////////////////////////////////////////////// -->
	<method name="GetStringLength">
		<parameter name="Param1"/>
	</method>
	
	<!-- ////////////////////////////////////////////////////////////////// -->
	<method name="Sum">
		<parameter name="Param1"/>
		<parameter name="Param2"/>
	</method>

</public>

<script language="JScript">
<![CDATA[

var MyProperty = "MyValue";

///////////////////////////////////////////////////////////////////////////////
// properties setting/getting
///////////////////////////////////////////////////////////////////////////////
function get_MyProperty(){
	return MyProperty;
}
///////////////////////////////////////////////////////////////////////////////
function put_MyProperty( newValue ){
	MyProperty = newValue;
}

///////////////////////////////////////////////////////////////////////////////
// methods
///////////////////////////////////////////////////////////////////////////////
function GetStringLength( param1){
	return String(param1).length;
}
///////////////////////////////////////////////////////////////////////////////
function Sum( param1, param2){
	return (param1 + param2);
}
///////////////////////////////////////////////////////////////////////////////

]]>
</script>
</component>

DOpusHelper.wsc.txt (2.12 KB)
Hint:
Whenever you want to create a new script component, you need to create a new *.wsc file and, very important, also change the internal classid of that new script component file. A much better way to create new script components is the windows script component wizard offered by microsoft, it takes care about the ids and puts in all necessary information automatically.

1 Like

Nice explanation!

Does this work even if the languages are mixed? (e.g. can an Opus script written in vbscript use a .wsc file written in jscript?)

I've been thinking of adding a @include directive but I'm not sure how useful that would really be, plus the more code that has to be parsed every time a script runs just makes things even slower.

Thank you. Yes, as mentioned, these script components can be consumed by anything that is able to access COM objects. WSH and any installed script language can, so it does not matter if you access a WSC that contains vb-script code from jscript and vice versa.

Any @include type of thing would have to deal with errors and line-numbers and debugging related problems I assume. A WSC can give you a popup, telling what went wrong at what line, it's quite straightforward to use and debug. I guess you'd have hard times translating shifted line numbers in error situations or merging script contexts at your C++ side - if that's even possible.

I don't know why I didn't remember WSCs much earlier, I used to do them a lot in the years 2001-2003. Thanks to MS for not dropping the support for them, don't know what the situation is in window 10 though. o)

@tbone, I think I made some of those too, but I usually prefer C++ or Delphi to make com/activex objects.

No, it isn't really "more code"..it is actually less or same (1 script using the include=same, 2 or more=less).
Thinking of the include as a C++ header/include file probably makes more sense to you (it is the exact same principle).
How useful are those? especially for constants, (common functions,) and whatnot.

It should simply mimic the C++ preprocessor when it encounters an includefile, that is, replace the statement
with the contents of the referenced includefile, then pass the combined result to the scriptengine.

You tend to keep the script around for a few seconds, so an immediate rerun won't need to do the same.
You can also keep the referenced includes in memory for slightly longer than you do scripts, so you won't have to reload them.

It is quite possible to preserve the position(s) of the include-statement(s) so you can remap eventual errors to the
correct file (when the error has line, column number).

That isn't how ActiveScripting works. Each script is entirely self-contained and anything it loads has to be parsed when the script is loaded. If two scripts include the same file, the overhead is felt twice, not once.

So a system that encouraged large include files/libraries to build up would be much worse for startup and memory performance than one which encourages only the code that a script really needs to be included in the script.

An alternative might be a mechanism that allows scripts to call each other in some way, but I'm not sure how practical that would be.

Of course, we can also sometimes add capabilities to the scripting API Opus provides to cover cases where library code might be desired, which may remove the need for include files in many cases and be less work in the end.

That isn't how ActiveScripting works. Each script is entirely self-contained and anything it loads has to be parsed when the script is loaded. If two scripts include the same file, the overhead is felt twice, not once.

So a system that encouraged large include files/libraries to build up would be much worse for startup and memory performance than one which encourages only the code that a script really needs to be included in the script.

An alternative might be a mechanism that allows scripts to call each other in some way, but I'm not sure how practical that would be.

Of course, we can also sometimes add capabilities to the scripting API Opus provides to cover cases where library code might be desired, which may remove the need for include files in many cases and be less work in the end.[/quote]
I think you're misunderstanding something.
YOU load the files and replace the include statements with the contents of the referenced file(s) before passing them on.

If this is the contents of inc.js, test1.js

inc.js:

var HELLO=1;

test1.js:

@include "inc.js"
DOpus.Output(HELLO);

then after you replace the include-statements (that is, before passing it to the scripting engine) it will look
like this:

var HELLO=1;
DOpus.Output(HELLO);

This is what I mean by less code..that is, less code/duplication of code, but the exact same script to pass
on to the engine as if the one making the script had combined them to begin with.

I don't think we'll see any kind of massive includefiles or anything like that.
But, a script might instead include multiple include files.
If a script includes too much unneccesary code it becomes a hassle for the one making the script too.

tbone, thanks very much for all this detail. I'm keen to get some sort of library going because I am ending up with several copies of important functions, and editing one means that I have to copy the revised version onto all the others. Two questions, which probably indicate that yet more detail is required for us non-techies:

  1. The only WSC Wizard that I can find on the web is only updated as far as for Windows XP:
    microsoft.com/en-us/downloa ... x?id=11914
    I have Windows 7 64-bit (Windows 10 will come later, if at all).

  2. Assuming that I can sort out problem 1, when the DOpusHelper.wsc file is changed, does it have to be re-registered using regsvr32.exe? (The first registration worked only after I used an administrator DOS box.)

On the other hand, if jon's "@include" directive is implemented, perhaps the WSC libraries will not be necessary.

I don't remember if I tried the wsc wizard on Win7 or my XP laptop, maybe just try if it still works on W7. I currently see no reason why it shouldn't. If it unexpectedly does not work on W7, it's not a huge deal, because all the wizard does is generating a class-id, creating a new file, inserting some method/property names you have entered and finally registering the resulting wsc file. All of these action can be done manually or with some kind of do-it-yourself automatism, if you really feeld the need for something like that. For starters though, I thought the wizard is probably a good choice to get going quickly.

No, you don't need to repeat the registration whenever you change or extend the code within the wsc.

My feeling is, that the WSC way for creating "includes" makes for a more robust system. It's also quite easy to test if specific compononents are installed and available e.g. They also can be used outside of DO and maybe they are even faster to use, as windows probably chaches them, avoiding parsing on each use. I did not do any performance test yet though, just thinking. o)

Success!!!

  • I pasted a working function of mine into the DOpusHelper.wsc file.
  • I then added the method name and parameter names into the xml section near the top of the DOpusHelper.wsc file.
  • I was then able to call this function from your button using the code "helper.".
    Thanks very much, tbone. Having such a library will greatly simplify things in the future. This throws up two more questions:

  1. Presumably your Hint applies if one wants to have two or more such libraries (for example, a Maths library, a Dates library, and so forth). The Wizard is reported to be incompatible with Windows 7, so things have to be done manually. I'm guessing here, but are the steps then:
  • Take a copy of your DOpusHelper.wsc file and rename it?
  • Edit the name of this new file in lines 5 and 6?
  • Change the classid? (And is this a random change, and is it best done on the last block of 12 hexadecimal digits?)
  • Add the functions, and their names and parameters?
  • Run regsvr32.exe with this new file?
    Or is it more complicated than this?

  1. (a) Is it possible for a function in a library to call a function in the same library?
    I tested this, and the the answer seems to be "Yes", whether or not the called function's name and parameters have been entered into the xml section.

(b) Is it possible for a function in one library to call a function in another library? This would seem to be a fairly clumsy procedure.

The classid (GUID) is supposed to be unique, so you should use a guid generator.
This is probably not a problem as there are online tools for it, like this one (no idea how good it is).

Every attempt to make one manually (aka "invent" one) increases the chances it crashes with
an existing one more than by doing it properly (humans aren't that good at randomizing on their own).

  1. It's not more complicated than what you describe, just pay attention to use a proper class-id, as myarmor pointed out.
    Maybe play with description and progid. They actually do not need to be the same and there's also no need the *.wsc extension is part of them.

You can use this to create a new classid, just paste it in the DOpus CLI window or maybe have a new button with this:

var classid = new ActiveXObject("Scriptlet.TypeLib").Guid; DOpus.Output("ClassID: " + classid);

2a) Of course. The methods and properties you define in the xml-section only help to make specific parts visible/accessible to the outside world. Any method/property inside the wsc does not need to be registered/described in some kind to be also used internally. Inside a wsc, the script code is nothing special and works as usual.

2b) Yes, it's not even clumsy I'd say, as you do exactly the same, as if you want to use wsc-contained functionality within DO or elsewhere, just pull it in and access what you need by referencing the object which represents the "other" wsc. So in your own "MasterLibXYZ.wsc" you can also make use of:

var helper = new ActiveXObject("DOpusHelper.wsc"); and call helper.GetStringLength() e.g.

You can also run methods DO offers inside a wsc, by passing any of DOs objects or the actual "this" context to the WSC after its instance has been created.
That can be done by adding/using some some kind of init() or hug() method and allows to print to the DO script console right from inside a wsc for example.

var helper = new ActiveXObject("DOpusHelperUsingDO_Objects.wsc"); helper.init(this); Inside the wsc it's something like:

function init(doThis){ do = doThis; //save it internally for all the other methods of the wsc do.DOpus.Output("Text printed from the wsc.."); //access and use DO objects }

Some further reading and experimentation:

  1. I agree strongly about our incapacity to randomise. The TechNet blog
    http://blogs.technet.com/b/heyscriptingguy/archive/2005/02/21/how-can-i-create-a-guid-using-a-script.aspx
    recommends a built-in script command for generating GUIDs. In JScript, within DOpus, the VBScript translates to:
    var TypeLib = new ActiveXObject ("Scriptlet.TypeLib")
    DOpus.Output (TypeLib.Guid)

  2. It seems that you don't have to register the *.wsc file. Following the hints in the excellent TechNet account on WSC at
    https://technet.microsoft.com/en-us/library/Ee692823.aspx
    I took a copy of "DOpusHelper.wsc", renamed it "DOpusHelperNoRego.wsc", put it into the root of D: drive, and did not register it. Then I changed line 6 on the button to
    var helper = GetObject ("script:D:\DOpusHelperNoRego.wsc")
    and everything worked, whether I used \ or \ in the file path. DOpus' aliases would be useful here. I still needed the line with the GUID, however, contrary to the TechNet account, so I'm not sure that I'm understanding this properly.

  3. The "function OnClick (ClickData)" is unnecessary for tbone's button. Furthermore, you can run the code totally independently of DOpus by pasting it into a *.js file, if you also change DOpus.Output to WScript.Echo. Similarly, the unregistered file DOpusHelperNoRego works in a standalone *.js file with the corresponding changes described in point 2 above.

I still don't know how to make a function in one *.wsc library call a function in another *.wsc library (Question 2(b) in my previous post).

Thanks, tbone. Your post arrived an instant before I posted mine, and I didn't see it. You've answered by question 2(b), you've explained all the other details, and you've given yet another way to generate a GUID, so I have no more questions for now. The business about running DOpus methods inside a wsc is interesting.

If jon later produces his @include, we can sit back and decide which we prefer.

That should probably be the easiest one to achieve.

I mean, it is an automation server, so you should simply use it like an automation server.
Iow, "new ActiveXObject"/CreateObject. Obviously it would require the one being instantiated to be registered beforehand.
I haven't tested it, but it naturally follows, doesn't it?

Calling internal functions (those you haven't exposed) probably isn't possible though.

tbone answered this in his last post, which he posted just before I asked the question again. The thread got confused because his and my posts are in the wrong logical order.

tbone, the constructor procedure doesn't seem to work when the function that you are using is in the WSC. For example:

  1. I changed the last function "Sum" in your WSC to:

function Sum( param1, param2){ this.sum = (param1 + param2); this.diff = Math.abs (param1 - param2); }Note that I deleted the "return this" line. Then in the DOpus button, I ran the code:

var ObjSD = new helper.Sum (10, 50) DOpus.Output ("Sum = " + ObjSD.sum + " and Difference = " + ObjSD.diff) but the first line threw the error message, "Object doesn't support this property or method (0x800a01b6)". Presumably no object "ObjSD" was constructed.

  1. I then copied the function "Sum" back into the DOpus button — still with no "return this" — and changed "helper.Sum" to "Sum" in the DOpus button so that it would read the copy of "Sum" within the button. Of course it ran perfectly, presumably creating the new object "ObjSD" in the normal way that the constructor procedure works.

  2. I reverted to "helper.sum" in the button and deleted "new", and in the WSC I added the line "return this". Now the button works as intended, but of course all I am doing now is calling a function, not creating a new instance of the "Sum" object.

QUESTION: Is it possible to apply the "constructor" procedure using a function in the WSC?

I have a solution, or at least a very clumsy workaround.

  1. Add the line "return this" to the function in the WSC:

function Sum( param1, param2){ this.sum = (param1 + param2); this.diff = Math.abs (param1 - param2); return this }
2. Create a new "mirror" function within the DOpus button that does nothing but read the values — without the "return this" line:

function SumMirror (param1, param2) { var HelperSum = helper.Sum (param1, param2) this.sum = HelperSum.sum this.diff = HelperSum.diff }
3. In the "new" constructor procedure in the DOpus button, call the mirror function lower down in the DOpus button, rather than the original function within the WSC:

var ObjSD = new SumMirror (10, 50) DOpus.Output ("Sum = " + ObjSD.sum + " and Difference = " + ObjSD.diff)
It works, but I don't like it at all, because there is so much mucking about. Surely there is a better way.

I would keep things simple and put all the code into one file and/or not use objects for simple things that don't really need them.

Especially with scripting, simplicity is a goal in itself.

Nice to see you exploring the wsc area! o)

Maybe it is a cleaner approach, if you use some kind of "factory" or "create" method within the WSC, to get your object instances. A pattern used very widely in Java e.g., but maybe were're still missing some important bits here. I also couldn't get parameters on the call to "new ActiveXObject("name", param1, param2); to work, which should be possible as well. It's possible with some Microsoft COM objects at least. Maybe it's not supported for WSCs, but now back to what you tried:

WSC Code:

/////////////////////////////////////////////////////////////////////////////// function CreateSum(){ return new SumObj(); } /////////////////////////////////////////////////////////////////////////////// function SumObj( param1, param2){ this.sum = function (a,b){ return a+b; } }

Button code:

var sumObj = helper.CreateSum(); DOpus.Output("WSC-Sum: " + sumObj.sum(a,b) );If you need to pass parameters to the constructor, you can still do so by putting them into CreateSum( /* here /); and pass them on to "..new SumObj( / here again */); within the wsc.

ps: I can currently barely read what I typed, needed to increase font-size to maximum, as I was at the eye-doctor half an hour ago and he did these drops into my eyes, which open your pupils, so if things explode - don't blame me! o) I will stay away from the computer for now, makes no sense! o)

I have found a far easier and more straightforward way to create a JScript library. All you do is create a few *.js files, each with nothing in but a set of related functions. Then you call any of these library files from a DOpus button or add-in using the "eval" command, so that its text is read into the script and is processed at that point. No WSC, no registering, no problems whatsoever with constructors.

For example, the test library file "D:\test.js" contains just the one function "Sum (aa, bb)" . Note the absence of any "return this", because it is going to be uses by a constructor procedure.

DOpus.Output ("Hello from Test.js"); function Sum (aa, bb) { this.sum = (aa + bb); this.diff = Math.abs (aa - bb); }And here is a DOpus button that calls the function "Sum (aa, bb)" in the course of using a constructor procedure to create a new "Sum" object called "ObjSum" with parameters 10 and 50:

[code]@Script JScript
FSO = new ActiveXObject ("Scripting.FileSystemObject");
eval (DoInclude ("D:\Test.js"));
var ObjSD = new Sum (10, 50);
DOpus.Output ("Sum = " + ObjSD.sum + " and Difference = " + ObjSD.diff);

function DoInclude (FilePath) {
TheFile = FSO.OpenTextFile (FilePath);
TheText = TheFile.ReadAll ();
TheFile.Close ();
return TheText;
}[/code]The "DoInclude (FilePath)" function produces the text that "eval ()" reads into the script as it is being processed. The output is:


Hello from Test.js
Sum = 60 and Difference = 40


There was no need even to use the function "OnClick (ClickData)". Reference Dino Esposito's JScript tutorial:
http://www.inf.unideb.hu/~fazekasg/oktatas/VBScript/JScript_%20tutorial.doc

tbone, I couldn't get your "factory" hints to work, but I sympathise with the dilated eyes — I have to put up with that every six months. I have to go by bus instead of car, and I bump into people when I'm walking back to the bus.

leo, it's surely "simpler" to have just one copy of a function and read it in when needed, than to have multiple copies of the function sitting at the bottom of code in scattered buttons and addins. The multiple copies will inevitably end up differing in small details and lead over time to confusion.

One point, however: Does the WSC approach may allow the mixing of JScript and VBScript, perhaps even within one library?