QuadooScript
Download QuadooScript for Windows!
- QuadooScript Self-Installer (32-Bit)
- QuadooScript Self-Installer (64-Bit)
- QuadooScript Binaries (32-Bit)
- QuadooScript Binaries (64-Bit)
About QuadooScript
QuadooScript is a dynamically typed, high-level scripting language that tightly integrates into Windows applications and processes. It has an easy-to-use syntax that will immediately be understood by anyone familiar with VBScript or JavaScript, or even anyone with a background with C.
The QuadooScript compiler and virtual machine are implemented using native C++ directly above Win32. QuadooScript was designed and developed to be the definitive native Win32-based scripting language for Windows.
Why Choose QuadooScript?
If you're already using Windows, then you may choose to use QuadooScript if...
- You have a background in C, and you want a high-level scripting language with similar syntax.
- You want to parse and manipulate JSON data using a high-level scripting language.
- You want to write automation scripts in an environment similar to VBScript but with more modern features.
- You want to use a scripting language that is lighter weight than Python, or perhaps you simply don't like Python's syntax.
- You want to build a website (or back-end web services) for Classic ASP but want a larger feature set than what VBScript or JScript provided, including JSON requests and WebSockets support.
- You want to write batch/shell scripts with something like PowerShell but without the .NET overhead.
- You want to extend your native Windows application with an embedded scripting language that was built with native Windows development in mind.
Contents
- Getting Started
- Installing QuadooScript
- QuadooScript on Discord
- Syntax
- Values
- Operators
- Arrays
- Maps
- Functions
- Control Flow
- Variables
- Classes
- Operator Overloading
- Interfaces
- Fibers
- Intrinsics
- Error Handling
- Decorators
- Internal Objects
- JSON
- Host Methods
- Automation
- Windowed Environment
- Embedding API
- Native Objects
- ActiveScript and ASP
- WebSockets
- Delayed Timer Callbacks
- Web Services
- Scripts as Modules
- Compile to Executable
- WinHttp Plug-in
- Basic Cryptography
- Capturing From Webcam
Getting Started
QuadooScript can be used in five different ways:
- As a stand-alone console application (QVM.exe)
- As an external script engine in a Common Gateway Interface (CGI) environment (via QVM.exe)
- As a host for scripts in a windowed environment (WQVM.exe)
- In an ActiveScript environment (ActiveQuadoo.dll)
- Embedded (QuadooVM.dll and, optionally, QuadooParser.dll)
Ultimately, no matter how QuadooScript is used, both QuadooParser.dll and QuadooVM.dll will be involved, although scripts can be compiled separately so that QuadooParser.dll is only used if a script needs to compile another script at run-time. Embedded scripts can be pre-compiled so that the embedding host has no need to take a dependency on QuadooParser.dll. Scripts can also be pre-compiled into executables.
QVM.exe is the stand-alone console application, and it supports several command line parameters:
- <first argument>
The first command line argument always specifies the name of the script, unless QVM.exe is running in CGI mode. - contenttype [content type]
- The HTTP content type can be specified as a command line argument for debugging. - query [query string]
- The HTTP query string can be specified as a command line argument for debugging. - cookies [cookie string]
- The HTTP cookies can be specified as a command line argument for debugging. - form [file path]
- The HTTP form data can be specified using a path to a separate file containing the form data. The format for the form data depends on the content type. - usecgi [true/false]
- Use this option to disable CGI mode (default is disabled). - sandbox
- Enables sandbox mode. TheHost
,Request
, andResponse
objects are not added to the global namespace of the VM. Theinput
,print
andprintln
intrinsics may still be used. - m:name <module path>
- Loads the specified module and assigns it to the global namespace under the provided name. This is the only way to load external modules when using sandbox mode. - args
- All remaining command line arguments are passed to the script.
When not in CGI mode, the first command line argument specifies the script, which can also be a pre-compiled script if the the file has the .QBC extension. All other script files are sent through the script parser, regardless of file extension (the preferred extension is .QUADOO). When using CGI mode, the script file (either pre-compiled or script text) is obtained from the PATH_TRANSLATED environment variable. Also, when using CGI mode, the content type, query string, and cookies are read from environment variables while the form data is read from the standard input handle.
When using WQVM.exe, only the -args
parameter is understood. WQVM.exe attempts to run a script's WinMain()
function. A script can define this function either as having no parameters or as having two parameters. In the latter case, these are the strCmdLine
and nCmdShow
parameters, which are propagated to the script from the host's own WinMain() function.
When using ActiveQuadoo.dll in an ASP environment, the .ASP extension should still be used for server pages using QuadooScript.
Hello, Quadoo!
Before going further into the syntax, let's get the proverbial "Hello, World!" sample covered!
function main ()
{
println("Hello, Quadoo!");
}
This is all that's needed to write text to the console. Of course, QuadooScript can also be used in environments that do not write to consoles, but at least now you have seen one of the most basic programs.
Quadoo Studio
There is a basic IDE available for QuadooScript called Quadoo Studio. It provides keyword highlighting and makes it easier to edit and test scripts for QVM.exe, WQVM.exe, and as web services via ActiveQuadoo.dll.
Installing QuadooScript
QuadooScript can be used to run scripts without installing it to any specific location or registering it with the system.
However, there are benefits to registering QuadooScript with the system, such as associating the .quadoo
file extension with QVM.exe through the shell.
To perform a full installation of QuadooScript, start by creating a folder such as C:\Program Files (x86)\QuadooScript
or C:\Program Files\QuadooScript
and extracting everything from the 32-bit or 64-bit QuadooScript package to that folder. From that folder, run QuadooInstaller.exe
. If successful, QuadooScript will be fully installed on the system, and it will appear in the system's list of installed programs. QuadooScript also uninstalls itself using QuadooInstaller.exe
, which can be invoked using the system's installed program list.
QuadooScript on Discord
QuadooScript has a group on Discord. Follow this link to join the community.
There are channels specifically for:
Syntax
QuadooScript's syntax is designed to be familiar to people coming from C-like languages while being a bit simpler and more streamlined.
Scripts can be stored in either plain text files with a .QUADOO extension or pre-compiled with a .QBC extension. The syntax is easily parsed using a hand-written look-ahead Recursive Descent Parser (RDP). Expressions are parsed using Pratt parser techniques.
Comments
Comments work in QuadooScript the same as in C and C++. /*
and */
omit the enclosed text from being parsed, and //
causes all remaining text on the line to be ignored.
Reserved Words
The following are reserved keywords in QuadooScript:
array | bool | break | case | catch |
class | continue | default | delete | do |
double | else | enum | extern | false |
fiber | float | for | function | get |
goto | if | int | interface | long |
map | money | namespace | new | null |
operator | partial | property | ref | return |
set | static | string | super | switch |
syscall | this | throw | true | try |
var | virtual | while |
Identifiers
Identifiers are used to name variables, functions, classes, namespaces, enums, and labels. Identifiers are case sensitive and may not begin with numbers. Underscores are permitted.
Line Endings
Statements in QuadooScript end with a semicolon. Blocks of code do not need trailing semicolons, but class and enum definitions do require a semicolon at the end, while namespace definitions do not.
Blocks
Like C and C++, QuadooScript uses curly braces to define blocks of code. A block of code can be used anywhere a statement is allowed, such as in control flow statements. Function bodies are also blocks but can be written as a single statement without curly braces.
function FunctionWithBraces (x)
{
return x * 2 + 3;
}
function FunctionWithoutBraces (x)
return x * 2 + 3;
Namespaces
Namespaces provide a simple way to separate groups of functions, classes, and other namespaces by adding the namespace's name as part of the path required to access anything within that namespace by anything outside of it. A dot is used to navigate between the components of a namespace path.
namespace MyGroup
{
function MyFunction ()
{
return 123;
}
}
function MyOtherFunction ()
{
return 456 + MyGroup.MyFunction();
}
Enumerations
Enumeration values work similarly in QuadooScript as they do in C and C++, except that the name of the enumeration must be used to reference the values.
enum Stuff
{
a,
b,
c
};
function GetValueOfB ()
{
return Stuff.b;
}
Values
Values are the built-in atomic object types that all other objects are composed of. They can be created through literal values in script and expressions that evaluate to a value. All simple values are immutable: once created, they do not change. The number 3 is always the number 3. The string "frozen" can never have its character array modified in place. Objects, arrays, maps, JSON objects, and JSON arrays can all have their contents changed.
Booleans
A Boolean value in QuadooScript is represented by one of two values, true
or false
. The literal keywords true
and false
are used to create a Boolean value.
Numbers
QuadooScript can manage numeric values using five different types:
- 32-bit integers
- 64-bit integers
- 32-bit floating point
- 64-bit floating point
- Four decimal place currency
At compile-time, QuadooScript automatically determines whether an integral literal is 32-bit or 64-bit.
var x = 1234567890;
var y = 12345678900;
In the example above, x is assigned a 32-bit value, while y is assigned a 64-bit value. 32-bit floating point values can be created with literals that are followed by an 'f' character.
var x = 3.14;
var y = 3.14f;
In the example above, x is assigned a 64-bit double value, while y is assigned a 32-bit floating point value.
Currency values are 64-bit integers with four lossless decimal places.
var x = $123.4567;
Integers can be defined using three formats:
- Base 10
- Base 16 or Hexadecimal - numbers are prefixed with
0x
- Base 2 or Binary - numbers are prefixed with
0b
Strings
A string in QuadooScript is internally represented by a wide-string RSTRING value. String literals are represented in script as zero or more characters enclosed in quotations.
"This is my string!"
QuadooScript supports escape sequences just like in C and C++.
"This string ends with a line break!\r\n"
Strings in QuadooScript cannot be modified, but array subscript syntax can be used to read from them just like in C.
var strName = "Quadoo";
var nFirstChar = strName[0];
In this example, nFirstChar would be set equal to the ASCII value of 'Q'.
QuadooScript accepts escaped character values in strings using the \uhhhh
and \Uhhhhhhhh
sequences where h
denotes a hexadecimal digit.
if(Host.CodePage != 65001)
Host.CodePage = 65001;
println("Smile Emoji: \U0001F600");
When running QVM.exe under the Windows Terminal application, this is the output:
Smile Emoji: 😀
When working with large Unicode characters, there are differences between indexing a string directly as compared to using the asc
and chr
intrinsics. For example:
var strText = "\U0001F600";
println("First element: " + strText[0]);
println("First character: " + asc(strText));
Reading the first element directly returns exactly that element. In this case, the value printed would be 55357. The asc
intrinsic understands the large Unicode values and will look for the rest of the value in the next element. In this example, asc
would return 128512. That same value can be turned back into a string of two elements with chr(128512)
.
Literal Strings
QuadooScript has an additional way to define strings enclosed within #[
and ]#
tags. No escape sequences or other translations are considered when literal strings are parsed. Line breaks are also preserved. Literal strings can be used to include large blocks of text that contain quotation marks, such as JSON text, for example.
var strJSON = #[{
"text": "This is my text!",
"array": [ "stuff", "things", "values" ]
}]#;
Null
QuadooScript has a special null
value that serves as the default value of any variable that is otherwise uninitialized. If you call a method that doesn't return anything and get its returned value, you get null
back. The null
value evaluates to false
when tested by an equality operator.
Void
QuadooScript has a special void
variable type whose purpose is to hold pointers to opaque objects that scripts may need to pass between native modules and custom script hosts.
For example, a window handle in WQVM.exe
is held in scripts as a void
value. Other hosts and native modules may manage some of their resources with void
values too.
Scripts generally won't interact directly with these values, but they can be converted to strings and integers using casts.
void v;
println(v); // Prints 0x00000000
println((int)v); // Prints 0
println(v == null); // Prints 1
void
values can be used in equality tests just like other value types. They can also be compared against each other.
void v1 = (void)123;
void v2 = (void)234;
if(v1 < v2)
println("v1 is less than v2");
Operators
QuadooScript supports the following operations on or between variables:
Operator | Description |
---|---|
... | Ellipsis |
+ | Addition |
- | Subtraction |
* | Multiplication |
** | Exponentiation |
/ | Division |
% | Modulus |
= | Assignment |
+= | Compound Addition and Assignment |
-= | Compound Subtraction and Assignment |
*= | Compound Multiplication and Assignment |
/= | Compound Division and Assignment |
%= | Compound Modulus and Assignment |
|= | Compound Bitwise OR and Assignment |
&= | Compound Bitwise AND and Assignment |
++ | Pre or Post Increment |
-- | Pre or Post Decrement |
== | Equality |
!= | Not Equal |
< | Less Than |
> | Greater Than |
<= | Less Than or Equal |
>= | Greater Than or Equal |
<< | Left Bit Shift |
>> | Right Bit Shift |
&& | Logical AND |
|| | Logical OR |
! | Logical NOT |
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise XOR |
~ | Bitwise Complement |
. | Member Access |
? : | Inline Conditional |
<<| | Bitwise Rotate Left |
>>| | Bitwise Rotate Right |
?. | Nullsafe |
Arrays
Arrays in QuadooScript work similarly to arrays in other scripting languages, although all arrays in QuadooScript are dynamic array objects on the heap. There are five ways to create an array in script syntax:
var a0[];
var a1[10];
var a2 = new array;
var a3 = new array[23];
var a4[] = { 1, 2, 3, 47, 90, 234 };
a0 is defined as a dynamic array without any slots initially allocated. a1 has 10 slots initially allocated. a2 is assigned a new array without any slots, while a3 is assigned a new array with 23 slots available. a4 is assigned a new array sized to the data in the C-style data set.
Array Length
An array's length is retrieved using the len function. Using the variables from above, len(a4)
would return 6.
Getting and Setting Values
Array values are indexed using square brackets.
a3[0] = 123;
var n = a3[0];
Inline Arrays
Array initialization lists can be defined inline and used as expressions.
var n = 3;
var v = { 1, 2, 3, 4, 5, 6 }[n];
In this example, 4 is assigned to v.
Array Methods
- Remove(n) - Removes slot n from the array.
- InsertAt(item, n) - Inserts item at index n.
- Append(item) - Appends item to the end of the array.
- Splice(nInsertAt, cRemove, aData) - Inserts the data held by aData beginning at position nInsertAt. If cRemove is greater than zero, then that number of slots are removed from the array being modified (or are overwritten with data from aData). aData can be either a regular array or a JSON array.
- Slice(nBegin [, nEnd]) - Returns a new array beginning with nBegin that runs through (but excluding) nEnd. If only one argument is passed to Slice(), then the array's length is used.
- GetValue(n) - Returns the item at index n.
- SetValue(n, item) - Sets item into index n.
- Clear() - Removes all items from the array.
Maps
A map in QuadooScript is an associative array. It holds a set of entries, each of which maps a key to a value.
QuadooScript supports maps with these key types:
- Strings
- 32-bit integers
- 64-bit integers
A map is dynamically created using this syntax:
var m0 = new map<string>;
var m1 = new map<int>;
var m2 = new map<long>;
All three maps can associate keys with any type of value, but m0 only stores string keys, m1 only stores 32-bit integers for keys, and m2 only stores 64-bit integers for keys.
Map Length
A map's length is retrieved using the len()
function.
var cItems = len(m0);
Getting and Setting Values
Map keys and values are retrieved using this syntax:
m0["stuff"] = 123;
var n = m0["stuff"];
Maps using string keys also support syntax like this:
var n = m0.stuff;
Map Methods
- Has(key) - Returns true if the map contains key. Returns false is the map does not contain key.
- Remove(key) - Removes the pair matching key or throws an exception.
- Find(key) - Returns the items specified by key or throws an exception.
- GetKey(n) - Returns the key at slot n. An exception is thrown if the index is invalid.
- Add(key, item) - Adds or updates the value at key.
- MultiAdd(key, item) - Adds the item if they key hasn't already been used. If it has, then an array is used to contain all items under the key.
- GetValue(n) - Returns the item at index n.
- SetValue(n, item) - Sets item into index n.
- Clear() - Removes all items from the map.
Functions
Functions in QuadooScript begin with the "function" keyword, followed by the function's name and finally a list of function parameters. A function can either explicitly return a value, or the compiler will insert a "return null" instruction at the end of the function.
Execution in a script always begins with the main function.
function main ()
{
}
QuadooScript allows functions to be called directly by name or by using indirect references (like a function pointer). For example:
function TestFunction (n)
{
return 890 + n;
}
function main ()
{
var oFunction = TestFunction;
var n = oFunction(123);
}
Function references can only be created on static functions.
Most functions in QuadooScript will only return one value using the return keyword, but function arguments that are local variables can be passed by reference to functions, allowing the called function to pass data back to the caller through the function parameters.
function Test (x)
{
x = 1;
}
function main ()
{
var n
Test(ref n);
}
Partial Functions
QuadooScript allows functions to be constructed incrementally throughout the source files that are compiled together.
partial MyFunction (a, b, c)
{
// Do something
}
<Other code...>
partial MyFunction (a, b, c)
{
// Do something else
}
Internally, at the parsing level, the partial functions are stitched together into a single function. The first partial function defines the parameters, if any. Later extensions to the partial function must either redefine the same parameter list or omit the list. For example:
partial MyFunction
{
// Parameters a, b, and c were defined previously and didn't need to be redefined.
}
Lambda Functions
QuadooScript supports lambda functions using the new keyword.
function main ()
{
var oTest = new function (x)
{
Host.WriteLn("x: " + x);
var oInner = new function (y)
{
Host.WriteLn("y: " + y);
return x * y;
};
return oInner(x * 2);
};
Host.WriteLn("Test: " + oTest(3));
}
NOTE: The Host
object is part of QVM.exe and ActiveQuadoo.dll, rather than part of the language itself.
In this example, a lambda function is created and assigned to oTest
. When it runs, it also creates another lambda function and assigns it to oInner
. Also, when the second lambda is created, it inherits the value of x from the first lambda.
Internally, a lambda function is an object, and inherited values are stored as member variables on that object.
QuadooScript also allows lambda functions to name the function.
var oLambda = new function MyNamedLambda ()
{
};
oLambda.MyNamedLambda();
While this syntax does create a lambda function that can be called anonymously, since the function is named, it can also be called by name.
Arrow Functions
QuadooScript supports an alternative syntax for lambda functions known as arrow functions.
An arrow function is defined by the presence of the =>
operator. The function parameter list is on the left, and the function body, which may be a single expression, is on the right side. If an arrow function receives only one argument, then its parameter's name does not need to be enclosed in parentheses. If the arrow function has no parameters, or more than one parameter, then a pair of parentheses must be used.
var fn1 = () => 42;
println(fn1());
var fn2 = a => 5 * a;
println(fn2(20));
var fn3 = (v1, v2) => v1 * v2 + 1;
println("Arrow: " + fn3(1, 2));
var x = 100;
var fn4 = () => x + 1;
println(fn4());
Arrow function definitions are expressions and can be used anywhere an expression would be accepted, including as an argument to another function.
PassLambda(1234, (n) => n / 2);
...
function PassLambda (n, oFn)
{
println("PassLambda(" + n + "): " + oFn(n));
}
Tail Call Optimization
QuadooScript can optimize tail calls for functions and non-virtual class methods. The optimization replaces the calling function's stack frame with the called function's stack frame. This means the stack will not grow recursively. This optimization is only considered when a return statement's expression is an eligible function call (global function, static method, or non-virtual class method from the same class).
Control Flow
Control flow is used to determine which chunks of code are executed and how many times. Branching statements and expressions decide whether or not to execute some code and looping ones execute something more than once.
Truth
All control flow is based on deciding whether or not to do something. This decision depends on some expression's value.
- The value false evaluates to false.
- The value null evaluates to false.
- 0, 0.0f, 0.0, and $0.0 evaluate to false.
- All other values evaluate to true.
To test whether strings, arrays, and maps are empty, you can use the isempty()
intrinsic function. isempty()
returns true
for null
and empty strings, arrays, and maps. It returns false for strings, arrays, and maps that have at least one element. For all other values, an exception is thrown.
If Statements
The simplest branching statement, if lets you conditionally skip a chunk of code. It looks like this:
if(ready)
Host.WriteLn("go!");
That evaluates the parenthesized expression after if. If it's true, then the statement after the condition is evaluated. Otherwise it is skipped. Instead of a statement, a block may be used:
if(ready)
{
Host.WriteLn("getSet");
Host.WriteLn("go!");
}
An else branch can also be included. It will be executed if the condition is false:
if(ready)
Host.WriteLn("go!");
else
Host.WriteLn("not ready!");
And, of course, it can take a block too:
if(ready)
{
Host.WriteLn("go!");
}
else
{
Host.WriteLn("not ready!") ;
}
Logical Operators
The &&
operator evaluates the left-side expression first. If it is true, only then is the right-side expression evaluated. Otherwise, the right-side expression is never evaluated.
The ||
operator evaluates the left-side expression first. If it is true, the right-side expression is not evaluated. The right-side expression is only evaluated when the left-side expression is false.
var a = true;
var b = false;
if(a && b)
Host.WriteLn("true!");
if(a || b)
Host.WriteLn("true!");
The Conditional Operator ?
:
The conditional operator works just like in C and C++.
Host.WriteLn(a ? "a was true!" : "a was false!");
Do/While Statements
Loops can be constructed in QuadooScript following either the do {} while(expression) format or using the while(expression) {} format.do
{
Host.WriteLn("Still looping!");
} while(keep_looping);
while(keep_looping)
{
Host.WriteLn("Still looping!");
}
Like C and C++, the distinction is when the expression is evaluated. A do loop always executes the loop code at least once, but a while loop may not execute its loop code at all.
For Statements
The for loop works similarly in QuadooScript as it does in C. Three code fragments, separated by semicolons, are expected. The first fragment is an initializer, which can either be an assignment or a variable declaration. The second fragment is the expression for continuing the loop. The third segment is an expression to be executed at the end of each loop iteration.
for(var n = 0; n < 10; n++)
{
Host.WriteLn("n: " + n);
}
Any or all of the three code segments may be empty. For example, infinite loops may be expressed with this syntax:
for(;;)
{
}
Break Statements
The break statement works the same as in C and C++. Put a break statement in a loop to immediately jump from that point to just outside the loop.
goto
The goto
keyword can be used to jump to a label (a word followed by a colon) that is in the same scope or in a parent scope. The script cannot jump into child scopes using the goto
keyword.
goto next;
println("Skipped!");
next:
println("Printed!");
Variables
Variables are named slots for storing values. New variables are declared using this syntax:
var x;
var y = 1;
Like JavaScript, but unlike VBScript, variables can be declared and initialized on the same line.
In the example above, variable x's value is null.
QuadooScript also allows variables to be declared and initialized using the value types. For example:
bool f;
int a, b, c;
long l;
float r;
double d;
money m;
string s;
void v;
Since QuadooScript is a dynamically typed language, these "types" are meaningless if other values are later assigned to these variables. However, without explicitly initializing them in script, the default values of each type are automatically assigned at compile-time since the types are specified. Therefore, values a, b, and c are automatically initialized to 0, instead of null. Variable d is initialized to a 64-bit value of 0, and so on.
Type Casting
The value types can also be used to cast a variable of one type into another type. For example:
var n = 0;
var q = (double)n;
var s = (string)q;
var v = (void)n;
Scope
A local variable in QuadooScript always exists until the end of the block in which it was defined. Static variables can be referenced from other scopes of code. For either, QuadooScript searches up the scope stack to find a variable.
namespace Stuff
{
var MyValue;
namespace OtherStuff
{
var MyOtherValue;
}
}
function main ()
{
Host.WriteLn("Value: " + Stuff.MyValue);
Host.WriteLn("Value: " + Stuff.OtherStuff.MyOtherValue);
}
Assignment
After a variable has been declared, you can assign to it using =
:
var a = 123;
a = 234;
An assignment walks up the scope stack to find where the named variable is declared.
When used in a larger expression, an assignment expression evaluates to the assigned value.
var a = "before";
Host.WriteLn(a = "after");
External Variables
The extern keyword can be used to create a variable (at the scope where it's used) that loads an external object having the same name. For example:
extern Host;
After this line of code, a variable called Host can be used that is loaded with an external object called Host. It's always more performant to load from a variable than to look up a named object.
Variable Types
A variable's type can be retrieved using the typeof intrinsic.
var abc = 123;
var type = typeof(abc);
The type can be checked against a predefined set of enumeration values:
- QVType.Null
- QVType.Bool
- QVType.Int
- QVType.Long
- QVType.Float
- QVType.Double
- QVType.Currency
- QVType.String
- QVType.Array
- QVType.Map
- QVType.Object
- QVType.JSONArray
- QVType.JSONObject
- QVType.Ref
- QVType.Void
Classes
Classes work in QuadooScript about the same as they do in VBScript, except they use syntax that is more like C++.
Classes are defined using the class
keyword.
class Example
{
};
Class Lifetime Management
Class instances are created by using the new
keyword.
var o = new Example;
Class instances are reference counted, although this detail is invisible to a QuadooScript programmer. When a variable holding a class instance goes out of scope, the class instance's reference count is decremented. When a class's reference count reaches zero, the class's destructor (if any) runs, and then the instance is removed from memory.
Constructors and Destructors
Classes in QuadooScript can optionally have a constructor and/or a destructor. The constructor is a special class method that runs as part of the object's creation. Likewise, the destructor is a special class method that runs just before the object is deleted from memory. Both constructors and destructors have the same name of the class, but the destructor's name is prepended with a tilde character.
class Example
{
Example ()
{
// This is a constructor
}
~Example ()
{
// This is a destructor
}
};
QuadooScript allows classes to have multiple constructors as long as each constructor has a unique number of parameters.
class Example
{
Example ()
{
// No parameters
}
Example (vParam)
{
// One parameter
}
Example (vParam1, vParam2)
{
// Two parameters
}
};
The example class above can be instantiated using any of its three constructors.
var oExample0 = new Example; // Pass nothing
var oExample1 = new Example(123); // Pass one argument
var oExample2 = new Example(123, 456); // Pass two arguments
Member variables
Any variable declared in a class but outside any class method becomes a member variable. Class member variables can be made static using the static keyword.
class Example
{
var m_vData; // This is a class member variable, only accessible to class methods
static m_vStaticData; // This is a static member variable, accessible both inside and outside of the class methods
};
Static class member variables can be accessed from outside the class, but non-static class member variables are only accessible to non-static class methods unless they're marked with the property
keyword (see below).
Class Methods
All class methods are publicly available.
Class methods can also be static. Static class methods can be accessed by treating the name of the class as a namespace.
class Example
{
Example ()
{
}
~Example ()
{
}
static function MyStaticMethod ()
{
Host.WriteLn("Hello!");
}
};
function main ()
{
Example.MyStaticMethod();
}
Properties
QuadooScript supports special syntax for exposing properties on objects.
class Example
{
var m_value;
Example ()
{
m_value = 0;
}
~Example ()
{
}
property Example
{
get
{
return m_value;
}
set (v)
{
m_value = v;
}
}
};
function main ()
{
var o = new Example;
Host.WriteLn(o.Example);
o.Example = 37;
Host.WriteLn(o.Example);
}
QuadooScript also supports indexed properties for classes.
property Example
{
get (n)
{
return m_value[n];
}
set (n, v)
{
m_value[n] = v;
}
}
...
var o = new Example;
Host.WriteLn(o.Example[0]);
o.Example[0] = 37;
Host.WriteLn(o.Example[0]);
A property can simultaneously support both regular and indexed getters and setters.
Properties can also be accessed dynamically using the get and set keywords in expressions. Using the example class from above, then the properties can also be accessed as follows:
var x = get(o, "Example");
var y = get(o, "Example", 0);
set(o, "Example", 123);
set(o, "Example", 0, 123);
Properties can be created automatically from member variables by marking them with the "property" keyword.
property var MyProperty;
When this is done, the proprty can be accessed externally by the same name as the member variable.
this
A class method can reference its own class member variables using the this
keyword. A class instance can also pass a reference to itself to another function by passing this
as an expression.
Inheritance
QuadooScript classes support single class inheritance. This means that a class can inherit members and methods from another class. However, a class cannot simultaneously inherit from two base classes.
class CBaseClass
{
var m_nValue;
function GetValue ()
{
return m_nValue;
}
};
class CSuperClass : CBaseClass
{
CSuperClass ()
{
m_nValue = 10;
}
};
...
var oClass = new CSuperClass;
return oClass.GetValue();
If a base class defines a constructor that has parameters, then a super class must also provide a constructor that calls the base class constructor. However, the super class constructor's signature does not have to match the base class constructor.
CBaseClass (nValue) :
m_nValue(nValue)
{
}
CSuperClass () :
CBaseClass(123)
{
}
Virtual Methods
Methods on classes can be marked virtual or pure virtual. Methods marked with the "virtual" keyword are called by name, instead of by ordinal, so that super classes can override base class behaviors. Pure virtual methods are like virtual methods but are unimplemented in base classes and must be implemented in super classes.
class CAnimal
{
virtual function Feed ()
{
Host.WriteLn("The " + GetType() + " has been fed.");
}
virtual function GetType () = 0;
};
class CGiraffe : CAnimal
{
virtual function GetType ()
{
return "giraffe";
}
};
function main ()
{
var oAnimal = new CGiraffe;
oAnimal.Feed();
}
In the example above, the CAnimal base class defines a GetType() method but leaves the implementation to the super class CGiraffe.
Method Delegates
Just as global functions can be wrapped into objects, non-static class methods can also be assigned as objects. In this case, they are called method delegates.
class MyBaseEvents
{
function OnClick (x, y)
{
Host.WriteLn("x: " + x + ", y: " + y);
}
};
class MyEvents : MyBaseEvents
{
MyEvents ()
{
Host.WriteLn("Test");
}
function GetOnClick ()
{
return MyBaseEvents.OnClick;
}
function GetOnStuff ()
{
return OnStuff;
}
function OnStuff ()
{
Host.WriteLn("Stuff!");
}
};
function main ()
{
var oMyEvents = new MyEvents;
var dlgOnStuff = oMyEvents.GetOnStuff();
var dlgOnClick = oMyEvents.GetOnClick();
dlgOnStuff();
dlgOnClick(100, 200);
}
In the example above, delegates are created for two non-static class methods. Once created, they're callable just like global function references. Internally, they maintain an object reference and the code pointer for the class method. When invoked, the code pointer is updated directly without doing a name lookup.
Virtual class methods may be assigned as delegates, but pure virtual methods may not. This is because a specific function is picked at compile-time. In the above example, a path to a method in a base class picks the base class method, even if it had an override in the subclass.
Class method delegates may only be assigned from non-static class methods of the class for which the delegates are being created. This allows both the class's this pointer to be available as well as type information for ensuring valid delegates are being created.
delete
QuadooScript supports a delete
keyword that works on classes (objects), arrays, maps, JSON objects, and JSON arrays.
class CDeleteTest
{
delete (strName)
{
println("delete: " + strName);
return strName;
}
function DeleteMore ()
{
delete "more";
}
};
function DeleteTest ()
{
var oTest = new CDeleteTest;
delete oTest.abc;
delete oTest["def"];
oTest.DeleteMore();
}
When using delete
on a QuadooScript class, the class's delete
handler function is called. In the example above, when the DeleteMore()
method is compiled, QuadooScript knows it's being compiled as a class method, so when it sees delete "more"
without a left-hand expression, it pushes this
onto the stack so that the calling object's delete
handler is invoked.
The delete
keyword can be used as an expression. It returns true
if successful or false
if unsuccessful.
When using delete
on an array, the index is specified, and that index is removed from the array. When used on a map or JSON object, the named member is removed.
Default Properties and Methods
QuadooScript classes can specify a member variable for receiving unhandled methods and properties.
class CDefaults
{
var m_oProps = propbag();
default property m_oProps;
var m_oMethods;
default function m_oMethods;
CDefaults (oMethods) : m_oMethods(oMethods)
{
}
};
In this example, an unhandled property access will be forwarded to the m_oProps
member, and an unhandled method call will be forwarded to the m_oMethods
member. In each case, the member is expected to be an object. Any other data type will result in a runtime exception.
Operator Overloading
QuadooScript supports basic operator overloading for classes. Just like C++, the overloaded operators are defined using the operator
keyword.
class CMyOperators
{
var m_nValue;
CMyOperators (v) :
m_nValue(v)
{
}
operator * (v)
{
return new CMyOperators(m_nValue * v);
}
operator <<| (v)
{
return new CMyOperators(m_nValue <<| v);
}
operator neg
{
return new CMyOperators(-m_nValue);
}
operator dup
{
return new CMyOperators(m_nValue);
}
operator ++
{
m_nValue++;
return this;
}
operator --
{
m_nValue--;
return this;
}
function ToString ()
{
return (string)m_nValue;
}
};
In the example above, the neg
operator is invoked when applying a unary negation to the object, and the dup
operator is invoked when applying the post-increment operator (the original object must be duplicated before it can be "incremented").
Most operators will perform some kind of operation and return a new object, leaving the original object unmodified. The unary increment and decrement operators modify the original object and also return a new object.
var o = new CMyOperators(42);
println("Multiplied: " + o * 10);
println("Rotate Left: " + (o <<| 3));
println("Negated: " + -o);
println("Pre-Increment: " + ++o);
println("Post-Increment: " + o++);
println("Final Value: " + o);
Interfaces
QuadooScript supports interfaces, similar to those in C++, for defining abstract coding interfaces. Classes inherit from interfaces and must implement the interface methods before they can be instantiated. Interfaces can also inherit from other interfaces, and classes and interfaces can inherit from multiple interfaces.
To define an interface, the interface keyword is used.
interface IMyInterface
{
virtual function DoSomething () = 0;
};
class CMyClass : IMyInterface
{
virtual function DoSomething ()
{
println("DoSomething!");
}
};
The methods of an interface must be defined as pure virtual, like they would be in C++.
The full benefit to using QuadooScript interfaces is derived when embedding the VM into a larger application because the interfaces provide a fast mechanism for the host to call script methods. The IQuadooObject
interface has a GetInterface()
method that queries the object for the specified interface. The name of the interface is the same name that is used in the script. If a requested interface is found, it is returned to the caller as an IQuadooInterface
object.
The native IQuadooInterface
interface has two methods:
- Invoke - Calls the script method by its ordinal.
- ResolveMethod - Resolves a method's ordinal from its name.
Method ordinals begin from zero. Interfaces that have base interfaces always include the base interface methods first, in the order in which they're defined in script. A code generator could simultaneously define interface definitions for QuadooScript and constants for native code. At run-time, a larger application embedding the QuadooScript VM could retrieve interfaces from objects and call methods using code-generated constants. In this way, no name lookups would ever be necessary.
Interfaces may also be called by scripts, giving scripts a fast way to call into native objects. Using the sample interface above, the following shows how a script could call interface methods (on either a native or script-based interface):
var oInterface = interface(oNativeObject, "IMyInterface");
oInterface.DoSomething();
While the above example works, it is important to remember that a name lookup must occur for every interface method call. For situations where it would make sense to resolve the name once and use the ordinal for every call (such as in a loop), there is also a way to obtain an object wrapping a specific interface method:
var oMethod = oInterface.DoSomething;
oMethod();
When the method is referenced like a property (no parentheses), a new object wrapping that interface method is returned. When the returned object is invoked like a nameless method, the interface method's cached ordinal is used to make the method call.
While this syntax works for both native and script-based interfaces, it will usually be less expensive for scripts to call methods on script-based objects directly rather than by using their implemented interfaces. However, this syntax is the only way for scripts to call interface methods on native objects (assuming the interface methods aren't also exposed through the IQuadooObject
implementation).
Fibers
Fibers are like coroutines, which can be thought of as cooperatively scheduled threads. Fibers wrap either a global function or a lambda, and they have separate call stacks from the caller. Code that is running within a fiber can yield control back to the caller by suspending itself. A fiber remembers its state (call stack and instruction pointer) until the fiber is called again. At that point, the fiber resumes running immediately after the original yield call. When the last function running within a fiber exits, the fiber's call stack is deallocated.
function main ()
{
var oFiber = new fiber(new function ()
{
for(var i = 0; i < 5; i++)
{
var o = JSONCreateObject();
o.i = i;
yield(o);
}
return null;
});
var oValue;
do
{
oValue = oFiber();
if(null == oValue)
break;
Host.WriteLn((string)oValue);
} while(oFiber.Active);
}
In this example, a fiber is created using a lambda. In the do loop, the fiber is started on the first iteration of the loop. Subsequent iterations resume the fiber. With each call to the fiber, the fiber runs until it calls the yield() intrinsic. The fiber returns a JSON object through the yield() intrinsic, and the caller retrieves the value as the return value from the fiber.
Fibers support these properties:
- Active - Returns true if the fiber is allocated, false if it is not allocated.
- Running - Returns true if the fiber is running, false if it is not.
When a fiber's last running function exits, the fiber's call stack is deallocated. If the fiber is called again, it is restarted, and a new call stack is created.
Yielding and Resuming
The yield() intrinsic can be used with or without a single argument. If an argument is passed, then the caller receives the value. When calling the fiber to resume it, either no arguments can be passed, or a single argument can be passed. If one argument is passed, then the yield() call in the fiber returns the passed value.
Intrinsics
QuadooScript has a number of built-in instructions that appear as functions in script but are actually their own double byte-code instructions. One byte-code instruction specifies the INTRINSIC (value 0xFE) instruction, while the second byte-code instruction specifies which intrinsic to execute.
len | sqrt | log | log10 | exp |
asc | chr | trim | substring | strchr |
strrchr | hex | abs | lcase | ucase |
instr | instri | instrrev | instrrevi | now |
nowutc | strcmpi | replace | timer | rand |
srand | yield | split | space | sin |
cos | tan | hyp | left | right |
stringbuilder | gc | mutex | extract | extracti |
eventsource | modf | sigmoid | sinh | cosh |
tanh | rad | deg | event | wait |
waitall | asin | acos | atan | strcmp |
scan | sleep | println | utoa | |
replacei | isempty | nsn | inf | ramp |
newasync | async | await | propbag | strcmpn |
strcmpni | base64 | base64url | strins | strtok |
reduce | dice | stringlist | join | round |
ceil | floor | sum | splitlines | min |
max | atou | input | linereader | fetch |
mapfind | strrcmp | strrcmpi | atan2 | doevents |
typeof |
Error Handling
QuadooScript exposes runtime errors to the script as exceptions. QuadooScript supports exceptions using syntax that is similar to C++.
try
{
Host.ThisMethodDoesNotExist();
}
catch
{
Host.WriteLn("We didn't have that method!");
}
Scripts can also catch the error code from the exception.
try
{
Host.ThisMethodDoesNotExist();
}
catch(e)
{
Host.WriteLn("Error code: " + hex(e.Value));
}
The script can also use the throw
keyword to generate an exception or to re-throw a caught exception.
try
{
throw 123;
}
catch(e)
{
Host.WriteLn("Caught exception: " + e.Value);
throw e;
}
The exception itself is an object that exposes information about the exception. The Value property returns the value that was thrown. The following properties are available:
- IP - This is the instruction pointer for the instruction that threw the exception.
- File - This is the name of the script file (if available) that threw the exception.
- Line - This is the line number from the original sources that threw the exception.
- Value - This is the value that was thrown by the script.
Exception objects also support a ToString()
method that returns a human-readable string containing all the information about the exception.
QuadooScript supports an additional way to catch and handle exceptions using the catch
keyword that returns a caught exception to the caller.
function ThisWillThrow ()
{
throw 123;
}
function main ()
{
var oException = catch(ThisWillThrow());
}
In the example above, catch
is used as an inline expression, and its return value is the exception object (or null
if there is no exception). Program execution continues without any jumping to separate exception handlers. If the exception is not needed at all, then the return value may be discarded immediately:
catch(ThisWillThrow());
Decorators
QuadooScript supports function decorators for modifying the behaviors of functions using the @
operator. When a decorator is specified, the original function is compiled under a modified name, and a new function is generated with the original function name. The generated function passes a delegate of the original code to the decorator function.
namespace Decorators
{
function MyTimer (fn, a)
{
var msTime = timer();
fn(a);
println("Time: " + (timer() - msTime));
}
}
@Decorators.MyTimer
function Test (a)
{
println("This is a test: " + a);
sleep(500);
}
function main ()
{
Test(1234);
}
When the Test()
function is called, the decorator function receives a delegate to the original code. Decorators always receive a delegate parameter, but the rest of the parameter list must match the parameter list of the function being decorated. This could be zero or more parameters (in addition to the delegate parameter, which is always at the beginning).
Decorators can also be used with class methods.
Internal Objects
The QuadooScript VM has several internal objects.
Array
Arrays are a native variable type that have the following methods:
- Remove
- InsertAt
- Append
- Splice
- Slice
- GetValue
- SetValue
- Clear
Binary
The binary object is used to manage a blob of binary data. Internally, the binary object implements the ILockableStream interface, so the data isn't meant to be extended. The data could come from a file or from an external object, for example. Several methods are available to support data extraction:
- ToBase64
- ToByteArray
- ReadString
- ReadUTF8
- ReadANSI
- ReadByte
- ReadInt16
- ReadInt32
- Clone
The binary object has two read/write properties:
- ContentType
- FileName
The binary object has a read-only property called Size that returns the length, in bytes, of the managed binary data. The binary object also has one indexed property called Data. This property also allows the binary data to be modified in-place.
Date
The date object is used to manage a date/time value. It has the following methods:
- AddYears
- AddMonths
- AddDays
- AddHours
- AddMinutes
- AddSeconds
- AddMilliseconds
- ToLocalTime
- ToSystemTime
- ToString
- To12Hour
- To12HourTime
- ToISO8601
- ToDouble
- GetTimeUntil
- GetTimeUntilMS
The following properties are available:
- Value
- Year
- Month
- Day
- Hour
- Minute
- Second
- Millisecond
- DayOfWeek (Read-Only)
The date object supports the default value mechanism. Invoking the default value is the same as calling ToString()
.
Event
The event object is a wrapper around the OS event. It is created using the event()
intrinsic function, which requires two arguments: name (null is allowed) and a manual reset boolean. The following methos are supported:
- Set() - Sets the event.
- Reset() - Resets the event.
- Wait(msTimeout) - Waits on the event. Returns true when the event becomes set. Returns false if the timeout occurs. The timeout argument is optional.
- Pulse() - Pulses the event.
An event instance can be passed to the wait()
and waitall()
intrinsic functions.
Event Source
The event source object manages event subscriptions. It has the following methods:
- Fire (variable argument list)
- Subscribe
- Unsubscribe
- Clear
- SetMethod
- ClearMethod
The arguments passed to Fire()
are passed to the subscribed sinks. Zero or more arguments may be passed. If any other method name is called, that method name is used as the method to call into the subscribed objects. Normal objects, global functions, and lambdas may be used as sinks.
Fiber
Fibers are objects that maintain their own call stacks and can be suspended and resumed. They have two properties:
- Active
- Running
Map
Maps are a native variable type that have the following methods:
- Has
- Remove
- Find
- GetKey
- Add
- GetValue
- SetValue
- Clear
Mutex
Mutex objects have the following methods:
- Acquire
- Release
Mutex objects also have one read/write property: Owned
.
StringBuilder
StringBuilder objects have the following methods:
- Append(vData)
- AppendChar(ch)
- AppendChar(ch, cch)
- AppendOffset(strSource, idxSourceStart)
- AppendOffset(strSource, idxSourceStart, cchCopy)
- AppendJoin(oData, strSeparator)
- Insert(idxInsert, strInsert)
- Clear()
- CopyTo(strSource, idxCopyTo)
- ToString()
- TruncateLeft(cchLeft)
- TruncateRight(cchRight)
- Substring(idxStart, cchRun)
- Set(ch, idxStart, cchSet)
- Reverse()
- FormatString(strFormatString, ...) - Formats into the builder using the format string and variable argument list and returns a new string from the builder.
- Format(strFormatString, ...) - Formats into the builder using the format string and variable argument list and returns the builder object for method chaining.
- Find(strText) - Returns the index of the first occurrence of strText or -1 if not found.
- FindR(strText) - Returns the index of the last occurrence of strText or -1 if not found.
The following properties are available:
- Length (Read-Only)
- Capacity
StringList
StringList objects have the following methods:
- IndexOf
- Add
- Insert
- Remove
- Join
- Copy
- Compare
- Clone
- Clear
- Compact
The following properties are available:
- Count (Read-Only)
- Strings - Returns the array view of the string list
JSON
QuadooScript supports JSON natively. In fact, JSON functionality is one of QuadooScript's biggest strengths, and one of the primary motivations for creating QuadooScript was to integrate the JSON library into a scripting environment.
JSON objects and JSON arrays are primitives in QuadooScript. Fields of JSON objects can be referenced just like fields of regular QuadooScript objects, and JSON array elements can be referenced just like regular QuadooScript arrays. Strings, Booleans, and integers are passed directly in and out of JSON objects and JSON arrays without any conversion necessary. The string objects used by QuadooScript are the same string objects used by the JSON library.
var oJSON = JSONCreateObject();
oJSON.x = 123;
println("x: " + oJSON.x);
println(oJSON); // This call will automatically convert the JSON object to a string.
println("Values: " + len(oJSON));
oJSON = JSONCreateArray();
oJSON.Append(123);
println("Element 0: " + oJSON[0]);
println(oJSON); // This call will automatically convert the JSON array to a string.
println("Values: " + len(oJSON));
The following JSON functions are supported:
- JSONParse(json_text) - Parses the provided JSON text and returns a native value, which can also be a JSON object or JSON array.
- JSONGetObject(root, path, ensure_exists) - Retrieves the object from the root object, given the provided path, and optionally creates the object if it doesn't already exist.
- JSONGetValue(root, path) - Retrieves any value from the root object, given the path to the value.
- JSONSetValue(root, path, value) - Sets the specified value into the root object, given the path to the new value.
- JSONRemoveValue(root, path) - Removes and returns the value from the root object, given the path to the value to be removed.
- JSONCreateObject() - Creates and returns a new JSON object.
- JSONMergeObject(target, source) - Merges the source object into the target object.
- JSONAddFromObject(target_object, target_field, source_object, source_field [, options]) - Copies a value from the source JSON object to the target JSON object, without translation between JSON and QuadooScript types. The optional "options" parameter defaults to required (optional is 1), allows null (not null is 2), and overwrite existing (no overwrite is 4).
- JSONClone(value, deep_clone) - Clones the JSON value as a shallow or deep copy.
- JSONFormat(json_text) - Reformats the provided JSON text using line breaks and indentation.
JSON objects support the following methods:
- Has(name) - Returns true if the named item exists, false if not.
- Remove(name) - Removes the named item from the object.
- Find(name) - Returns the named item from the object. If the named item is not in the object, then null is returned.
- FindThrow(name) - Like Find() but throws an exception if the named item is not in the object.
- GetKey(index) - Returns the name of the item at the provided index.
- GetValue(index) - Returns the indexed item from the object.
- SetValue(index, v) - Sets item v into the provided index.
- Add(name, v) - Adds item v to the object with the provided name.
JSON arrays support the following methods:
- Remove(index) - Removes the index item from the array.
- Find(field, value) - Finds the item in the array where the named field has the provided value. The items in the array must be objects. If found, the index is returned. Otherwise, -1 is returned.
- Find(value) - Finds the value in the array. If found, the index is returned. Otherwise, -1 is returned.
- FindThrow() - Like Find() (either parameter list) but throws an exception if the item isn't found.
- FindObject(field, value) - Finds the item in the array where the named field has the provided value. The items in the array must be objects. If found, the object is returned. Otherwise, null is returned.
- FindObjectThrow(field, value) - Like FindObject() but throws an exception is the item isn't found.
- GetValue(index) - Returns the indexed item from the array.
- SetValue(index, v) - Sets item v into the provided index.
- InsertAt(v, index) - Inserts item v to the new index. Existing items at and beyond that index are pushed one index further into the array.
- Append(v) - Appends item v to the end of the array.
- Clear() - Removes all items from the array.
Finding a JSON Object in a JSON Array
The following example demonstrates two ways to find a JSON object from within a JSON array.
var oColors = JSONParse(#[ [{color:"red"},{color:"green"},{color:"blue"}] ]#);
var oColor = oColors.FindObject("color", "blue");
println(oColor.color);
var idxGreen = oColors.Find("color", "green");
println("Green Index: " + idxGreen);
Finding an object using a JSON path
JSON paths can be used to find specific objects buried within deep JSON data trees.
var oJSON = JSONParse(#[
{
type: "test",
objects:
[
{ type: "ABC", data: [10, 20, 30] },
{ type: "XYZ", data: [15, 30, 45] }
]
}
]#);
var nValue = JSONGetValue(oJSON, "objects:[type:XYZ]:data[2]");
println("Value: " + nValue);
In this example, the value 45 will be printed. The JSON path parser uses the same token set as the main JSON parser, so it still uses square brackets to denote arrays, and it uses colons to separate fields. Arrays can be referenced using either an absolute index or a name and value pair.
More information on using the path parsing APIs is on the JSON page.
The cryptography section includes an example of using JSON to build and validate JSON Web Tokens.
Host Methods
While not technically part of the QuadooScript language itself, QVM.exe adds a Host
object into the global namespace to let scripts interact with the external environment and file system. These are the methods available on the Host
object:
- Args() - Returns the command line argument array.
- Args(n) - Returns argument n from the command line argument array.
- ReadArg(n, vDefault) - Returns argument n or vDefault if n would have been an invalid index.
- FindParam(strParam) - Returns the argument index of strParam from the argument list or 0 if not found.
- FindParamValue(strParam) - Returns the next argument following strParam, if found, from the argument list. Throws an exception on error.
- OpenStdInPipe() - Returns a pipe opened on STD_INPUT_HANDLE.
- OpenStdOutPipe() - Returns a pipe opened on STD_OUTPUT_HANDLE.
- OpenStdErrorPipe() - Returns a pipe opened on STD_ERROR_HANDLE.
- Write(strText) - Writes strText (converts to CP_ACP first) to STD_OUTPUT_HANDLE.
- WriteLn(strText) - Does the same as Write() but adds "\r\n" to the output stream.
- WriteTextFile(strText, strFile) - Writes strText (converts to CP_UTF8 first) to strFile. The UTF-8 BOM is written first.
- WriteBinaryFile(oData, strFile) - Writes the file data (needs to be a file object) contained by oData to strFile.
- WriteAsciiFile(strText, strFile) - Writes strText (converts to CP_ACP first) to strFile. No BOM is written.
- ReadTextFile(strFile) - Reads strFile as a text file and returns the text. Conversions from ASCII and UTF-8 are supported.
- ReadBinaryFile(strFile) - Reads strFile as a binary file and returns a binary file object.
- GetEnv(strName) - Reads the environment variable specified as strName or returns an empty string.
- SetEnv(strName, strValue) - Writes the environment variable and provided string data.
- MapPath(strPath) - Creates an absolute path using strPath as a relative path from the script's path (non-CGI mode) or using the "PATH_INFO" environment variable (CGI mode).
- OpenFolder() - Returns a folder object that is equivalent to calling OpenFolder(MapPath("/")).
- OpenFolder(strPath) - Returns a folder object for strPath.
- OpenResources(vModule) - Returns a resources object for vModule, which can be either a string path or null for the current process module.
- OpenLineReader(strPath) - Returns a line reader object for reading lines from the text file specified by strPath.
- Find(strQuery) - Returns a query object for the file path query specified. Throws an exception if there are no files or folders to return.
- FindChanges(strPath, fWatchSubtree, nNotifyFilter) - Returns a file/folder change notification object. The wait() function can be used on the returned object.
- CreateProcess(strFile, strCmdLine) - Returns a process object if a process can be created using strFile and strCmdLine.
- CreateProcess(strFile, strCmdLine, strCurrentFolder) - Returns a process object using the specified current folder.
- CreateProcess(strFile, strCmdLine, oInput, oOutput, oError) - Returns a process object using custom redirection pipes.
- CreateProcess(strFile, strCmdLine, strCurrentFolder, oInput, oOutput, oError) - Returns a process object using a specified current folder and custom redirection pipes.
- CreateObject(strName) - Returns a native
IQuadooObject
(if available) or a wrapped ActiveX control (supportingIDispatch
) using COM. The object has to be registered under strName in the registry. - CreatePipe() - Returns an anonymous, bidirectional pipe object.
- CreateWriteFileAsPipe(strFile) - Opens strFile for writing and wraps it in a pipe object.
- CreateReadFileAsPipe(strFile) - Opens strFile for reading and wraps it in a pipe object.
- CreateBinary([strFileName,] vBytes) - If
vBytes
is an integer, then creates an empty binary object with a size of(int)vBytes
bytes or, ifvBytes
is a string, then converts the string to UTF-8 and returns the data as a binary object. Optionally, the object can also be initialized with a file name. - ParseDate(strDate) - Attempts to read a date and/or time in "m/d/y h:m:s" format and return a date object. If null is passed, then an empty date object is returned.
- ParseDate(nDate) - Returns a date object from a 64-bit integer value.
- ParseDate(dblDate) - Returns a date object from a 64-bit floating-point value.
- FileFromBase64(strFile, strBase64) - Decodes strBase64 and returns a binary file (with the name set to strFile) object from the decoded text.
- FileFromBase64Url(strFile, strBase64Url) - Decodes strBase64Url and returns a binary file (with the name set to strFile) object from the decoded text.
- FileFromHex(strFile, strHex) - Decodes strHex and returns a binary file (with the name set to strFile) object from the decoded text.
- FileFromByteArray(strFile, aData) - Treats aData as an array of bytes and returns a binary file (with the name set to strFile) object from the byte data.
- ResolvePath(strBase, strRelative) - Returns an absolute path built by combining strRelative with strBase.
- LoadQuadoo(strModule [, strClassGuid]) - Loads either an external object (a DLL that implements CreateObject()) or a pre-compiled QuadooScript (.QBC) file.
- GetActiveObject(strName) - Returns a wrapped ActiveX control (supporting IDispatch) using COM. The object has to be registered under strName in the ROT.
- GetComponent(strComponent) - Returns a wrapped ActiveX control (supporting IDispatch) using COM. The object created is provided by an ActiveX component described by the
strComponent
parameter. This method is equivalent to VBScript's GetObject method. - EnableCOM() - Enables COM inside of QVM.exe so that CreateObject() and GetActiveObject() can be used. COM is unloaded automatically when the script ends.
- Compile(strFileRef, strScriptText, fPermissive, oBinaryRef) - Compiles the provided script text and returns the compiled QBC through the last parameter, which must be a
ref
variable. - RegisterServer(strModule) - Registers the specified module with COM by calling its DllRegisterServer() method.
- UnregisterServer(strModule) - Unregisters the specified module with COM by calling its DllUnregisterServer() method.
The Host
object also supports these properties:
- CurrentDirectory - Gets or sets the current directory of the script engine's process space.
- ConsoleTitle - Gets or sets the console window's title.
- NativeModule - Returns the path to the VM running the script.
- Args - Returns the array of arguments that were passed to the script.
- CodePage - Gets or sets the console's code page.
- WindowsDir - Returns the primary directory for the Windows installation.
- SystemDir - Returns the system directory for the Windows installation.
Script Directory vs. Current Directory
When working with QVM.exe
or WQVM.exe
, a script has access to the Host.CurrentDirectory
property. This property directly uses GetCurrentDirectory() and SetCurrentDirectory for managing the current working directory. To open a folder object with the current working directory, a script would do this:
var oCWD = Host.OpenFolder(Host.CurrentDirectory);
The current directory depends on the environment when the script is started, but the script's directory where the script itself resides is always the same.
var oFolder = Host.OpenFolder();
The code above opens the same folder regardless of the current directory.
Find File Object
These methods are available:
- Next() - Returns true/false based on whether there was another file/folder.
- OpenFolder() - Returns a folder object for the current item if it's a folder. If it's not a folder, then an exception is thrown.
These properties are available:
- Name
- Size
- IsFile
- IsFolder
- IsReadOnly
- Creation
- LastWrite
- Path
- Ext
- FullPath
Folder Object
These methods are available:
- ToString()
- Remove()
- AvailableSpace()
- TotalSize()
- TotalAvailable()
- Empty()
- CreateFolder(strRelativePath)
- MoveTo(oTargetFolder)
- MoveTo(strTargetFolder)
- DeleteFolder(strRelativePath)
- DeleteFile(strRelativeFile)
- OpenFolder(strRelativePath)
- Find(strRelativePath)
- IsFolder(strRelativePath)
- IsFile(strRelativePath)
- GetPathOf(strRelativePath)
- GetAttributes(strRelativePath)
- GetFileSize(strRelativePath)
- CopyFile(strRelativePath, strTargetPath)
- CopyHardLink(strRelativePath, strTargetPath)
- MoveFile(strRelativePath, strTargetPath)
- ReplaceFile(strRelativeExistingPath, strTargetPath)
- SetAttributes(strRelativePath, nAttributes)
- GetFileTime(strRelativePath, ref oCreation, ref oLastAccess, ref oLastWrite)
- SetFileTime(strRelativePath, oCreation, oLastAccess, oLastWrite)
The folder object also supports a read/write Path
property. When setting the property, the path must be an existing folder.
Resources
The Host.OpenResources(vModule)
method returns a resources object for the specified module. This object allows the script to query for and to load embedded resources from the executable module file.
- Load(vName, vType) - Returns a binary object containing the resource data.
- EnumTypes() - Returns an array of the enumerated resource types.
- EnumNames(vType) - Returns a JSON array of objects, each containing the name and type of the enumerated resources.
The vName
and vType
variables can specify either a string or an integer. Enumerated names and types can also be either strings or integers.
Running System Commands
While there is no equivalent to the system()
function in QVM.exe, its functionality can be recreated in a variety of ways depending on the behaviors needed.
The following can be used to run a system command with a specified starting directory and to wait for the command to complete:
function ShellCmd (strCmd, strStartDirectory)
{
Host.CreateProcess(Host.GetEnv("ComSpec"), "/C " + strCmd, strStartDirectory).Wait();
}
If the system command's return value is desired, the following may be sufficient:
function ShellCmdResult (strCmd, strStartDirectory)
{
var oCmd = Host.CreateProcess(Host.GetEnv("ComSpec"), "/C " + strCmd, strStartDirectory);
oCmd.Wait();
return oCmd.GetExitCode();
}
In both examples, the caller may set the strStartDirectory
parameter to null
if the starting directory is not important.
Automation
Given everything that exists on the Host object, you shouldn't be surprised to learn that QuadooScript is a great language choice for building automation scripts such as build scripts and packaging scripts. Instead of writing batch files, for example, QuadooScript can be used as a scripting alternative. A QuadooScript installation is substantially smaller than Python too. Finally, unless you have requirements that specifically depend on the .NET framework, then QuadooScript could also be a better choice than PowerShell.
There are a variety of ways that "DOS" (system) commands can be invoked using QuadooScript (from QVM.exe or WQVM.exe). In addition to the ShellCmd()
and ShellCmdResult()
examples from the system commands section, other reusable functions can be constructed to launch (and wait for) external utilities.
function RunTool (oCurrent, strToolExe, strCmdLine)
{
var oTool = Host.CreateProcess(oCurrent.GetPathOf(strToolExe), strCmdLine, (string)oCurrent);
oTool.Wait();
return oTool.GetExitCode();
}
function RunToolWithStartDir (oCurrent, strToolExe, strCmdLine, strStartDir)
{
var oTool = Host.CreateProcess(oCurrent.GetPathOf(strToolExe), strCmdLine, strStartDir);
oTool.Wait();
return oTool.GetExitCode();
}
The previous examples create processes that attach to the same pipes used by the calling process. Of course, it's also possible to attach custom pipes to a child process.
var oPipe = Host.CreatePipe();
var oChild = Host.CreateProcess(strProgramPath, strArgs, oPipe, Host.OpenStdOutPipe(), Host.OpenStdErrorPipe());
oPipe.Write(strCommand + "\r\n");
There is also nothing stopping QuadooScript from providing a custom pipe to a child process as its output pipe and then reading the output from a child process. In that case, a script would read from the pipe using the Read(cbBytes)
method.
The QuadooScript package contains a simple module for handling ZIP files. Scripts that need to build ZIP files can use the QSZIP.dll
module to write files into ZIP files.
var oZIP = Host.LoadQuadoo("QSZIP.dll");
var oPackage = oZIP.OpenWriter("Package.zip", "new");
var cbPackaged = oPackage.AddFile("C:\\files\\file.txt", "files/file.txt");
cbPackaged += oPackage.AddFile("C:\\stuff\\stuff.txt", "stuff/stuff.txt");
The 32-bit and 64-bit QuadooScript packages are built by an automation script.
Windowed Environment
Whereas QVM.exe operates in console mode, WQVM.exe runs in a windowed environment. When WQVM.exe runs, Windows internally calls its WinMain() function. In turn, a script's WinMain
function is also called. However, if WinMain()
does not exist, then a script's main()
function is called instead.
A script can define WinMain()
in one of two ways:
function WinMain ()
{
// No parameters were defined
}
function WinMain (strCmdLine, nCmdShow)
{
// The command line and command show values have been provided
}
Host Object
Like in the console version, WQVM.exe also provides a Host
object. However, not all methods are available, and some methods are unique to the windowed environment.
Under WQVM.exe, the following Host
methods are unavailable: OpenStdInPipe()
, OpenStdOutPipe()
, and OpenStdErrorPipe()
. The ConsoleTitle
and CodePage
properties are also unavailable.
Five methods are unique to the WQVM.exe Host
object:
- MessageBox(hwnd, strText, strCaption, nType)
- MessageBeep(nType)
- Quit(nValue)
- Execute(hwnd, strOperation, strFile, strParameters, strDirectory, nShowCmd)
- CreateMessagePump()
- ShellLink(strPath, strStartDir, strArgs, strLinkFile, strIcon, idxIcon, nCmdShow, strDesc)
The Quit()
method posts WM_QUIT
with the provided value to the message queue.
The Execute()
method is a thin wrapper for the ShellExecute()
function in Windows.
Two properties are unique to the WQVM.exe Host
object:
- Windows - Returns an interface for the windowed environment of Windows
- Pump - Returns the active message pump object or
null
if there isn't one
Windows Object
The Host.Windows
property has these methods:
- EnumWindows()
- EnumChildWindows(hwnd)
- EnumThreadWindows(idThread)
- SendMessage(hwnd, nMsg, wParam, lParam)
- SendMouseWheel(x, y, oData)
- SendMouseDown(x, y, oData)
- SendMouseUp(x, y, oData)
- SendMouseMove(x, y, oData)
- SendKeyDown(nKey)
- SendKeyUp(nKey)
- SendCopyData(hwnd, nData, vData) - vData must either be a string or be a binary data object
- PostMessage(hwnd, nMsg, wParam, lParam)
- FindWindow(strClass, strName)
- GetWindowText(hwnd)
- GetWindowClass(hwnd)
- GetFocus()
- GetForegroundWindow()
- GetCapture()
- GetWindowRect()
- GetDesktopWindow()
- GetCursorPos()
- GetScreenSize()
- SetFocus(hwnd)
- SetForegroundWindow(hwnd)
- SetCursorPos(x, y)
- IsWindow(hwnd)
- IsWindowVisible(hwnd)
- IsZoomed(hwnd)
- IsIconic(hwnd)
- FromPoint(x, y)
Message Pump
WQVM.exe provides a standard message pump object, via Host.CreateMessagePump()
, for QuadooScript scripts running in a windowed environment. It has the following methods:
- Run()
- End([nValue])
- AddHandler(oHandler)
- RemoveHandler(oHandler)
The message pump object also has a Task
property that can set (or cleared by passing null
) a task object that runs a callback method when there are no messages being pumped. Reading the property simply returns true
/false
based on whether there is a task set.
The Run()
method runs the message pump either until WM_QUIT
is received or until End()
is called. The value passed to End()
or received with WM_QUIT
is returned to the original caller. When Run()
is called, that message pump becomes the active message pump, and it can also be accessed using the Host.Pump
property. There is only one active message pump. If another message pump becomes active, then it remembers the previously active message pump and reactivates it when the current message pump exits.
Tasks and message handlers are both designed to be implemented by native code but can be passed to the message pump by scripts as QuadooScript objects. The message handlers decide whether the messages should still be passed onto the remaining handlers and ultimately to the translation and dispatch part of the pump. Native code could implement message handlers using IsDialogMessage()
, using accelerators, or with any other message handling logic. A message handler returns TRUE
to prevent additional processing of the message.
WQVM.exe does not provide window creation or GUI elements beyond the message box. QuadooScript plug-ins should be used to provide additional graphical and windowing functionality. However, plug-ins are encouraged to leverage the standard message pump using the published interfaces from WQVMInterfaces.h
.
If a native plug-in wants finer control of the message pump, then it should implement its own loop, but it can still leverage the standard message pump object by implementing the IQVMMessagePumpController
interface. When the native plug-in's code calls IQVMMessagePump::UseController()
to set itself as the pump's controller, then the standard message pump still becomes the active pump, and its End()
method may still be used to set the result and notify the message pump's controller that it should exit. Additionally, a controller's custom message pump can also leverage the standard message pump's handlers by calling IQVMMessagePump::ProcessMessages()
. Messages are processed either until the queue is empty or until WM_QUIT
is received, at which point the method also returns E_ABORT
. Finally, a plug-in can also invoke the standard message pump object's task, if there is one, by calling IQVMMessagePump::RunTask()
.
Embedding QuadooScript in Other Applications
It is very easy to include QuadooScript in another application. The first step is to decide whether the application should include pre-compiled byte-code or compile the script on startup. QuadooParser.dll includes a single API for parsing and compiling script text (or script files) into a binary byte-code stream.
HRESULT WINAPI QuadooParseToStream (PCWSTR pcwzFile, __out ISequentialStream* pstmBinaryScript, IQuadooCompilerStatus* pStatus)
HRESULT WINAPI QuadooParseTextToStream (PCWSTR pcwzText, INT cchText, __out ISequentialStream* pstmBinaryScript, IQuadooCompilerStatus* pStatus)
If you do not have an implementation of ISequentialStream
available, then you can use QuadooScript's default implementation:
HRESULT WINAPI QuadooAllocStream (__deref_out ISequentialStream** ppStream);
DWORD WINAPI QuadooStreamDataSize (ISequentialStream* pStream);
Once byte-code is ready to be executed, an application calls the QVMCreateLoader() method from QuadooVM.dll.
HRESULT WINAPI QVMCreateLoader (__deref_out IQuadooInstanceLoader** ppLoader)
Now the application calls methods from the IQuadooInstanceLoader interface.
interface __declspec(uuid("8C32C545-0802-4e32-A830-83EA42BA2870")) IQuadooInstanceLoader : IUnknown
{
virtual HRESULT STDMETHODCALLTYPE FindInstance (RSTRING rstrProgramName, __deref_out IUnknown** ppunkCustomData) = 0;
virtual HRESULT STDMETHODCALLTYPE AddInstance (RSTRING rstrProgramName, IUnknown* punkCustomData, ISequentialStream* pstmProgram,
DWORD cbProgram, __in_opt ISequentialStream* pstmDebug) = 0;
virtual HRESULT STDMETHODCALLTYPE LoadVM (RSTRING rstrProgramName, __out HRESULT* phrRegistered, __deref_out IQuadooVM** ppVM) = 0;
virtual HRESULT STDMETHODCALLTYPE RemoveInstance (RSTRING rstrProgramName) = 0;
virtual bool STDMETHODCALLTYPE IsUsingDebugger (VOID) = 0;
};
The application now calls AddInstance() with the script's file path and the byte-code in a stream.
Once the AddInstance() method returns, the byte-code has been registered and can be loaded into a new VM. To create a new VM instance, the application calls LoadVM() and passes the name of the script and the stack size. LoadVM() returns a new VM instance that is ready to execute byte-code.
interface __declspec(uuid("35FE6D03-4D05-4b49-A7A3-CD9DC2C944C1")) IQuadooVM : IUnknown
{
virtual HRESULT STDMETHODCALLTYPE AddGlobal (RSTRING rstrName, IQuadooObject* pObject) = 0;
virtual HRESULT STDMETHODCALLTYPE FindGlobal (RSTRING rstrName, __deref_out IQuadooObject** ppObject) = 0;
virtual HRESULT STDMETHODCALLTYPE RemoveGlobal (RSTRING rstrName, __deref_opt_out IQuadooObject** ppObject) = 0;
virtual HRESULT STDMETHODCALLTYPE RunConstructor (__deref_out IQuadooObject** ppException) = 0;
virtual HRESULT STDMETHODCALLTYPE RegisterDestructor (IQuadooObject* pObject, DWORD idxDestructor) = 0;
virtual HRESULT STDMETHODCALLTYPE PushValue (QuadooVM::QVARIANT* pqvValue) = 0;
virtual HRESULT STDMETHODCALLTYPE FindFunction (PCWSTR pcwzFunction, __out ULONG* pidxFunction, __out DWORD* pcParams) = 0;
virtual HRESULT STDMETHODCALLTYPE RunFunction (ULONG idxFunction, __out QuadooVM::QVARIANT* pqvResult) = 0;
virtual HRESULT STDMETHODCALLTYPE Throw (QuadooVM::QVARIANT* pqvValue, __in_opt QuadooVM::QVARIANT* pqvCode = NULL) = 0;
virtual HRESULT STDMETHODCALLTYPE Resume (__in_opt QuadooVM::QVARIANT* pqvValue, __out_opt QuadooVM::QVARIANT* pqvResult) = 0;
virtual HRESULT STDMETHODCALLTYPE Unload (VOID) = 0;
virtual QuadooVM::State STDMETHODCALLTYPE GetState (VOID) = 0;
virtual VOID STDMETHODCALLTYPE SetExternalScriptSite (__in_opt IExternalScriptSite* pSite) = 0;
virtual HRESULT STDMETHODCALLTYPE AddGlobalFunction (RSTRING rstrMethod, IQuadooFunction* pFunction) = 0;
virtual HRESULT STDMETHODCALLTYPE RemoveGlobalFunction (RSTRING rstrMethod) = 0;
virtual HRESULT STDMETHODCALLTYPE SetSysCallTarget (__in_opt IQuadooSysCallTarget* pTarget) = 0;
virtual HRESULT STDMETHODCALLTYPE SetPrintTarget (__in_opt IQuadooPrintTarget* pTarget) = 0;
virtual HRESULT STDMETHODCALLTYPE End (VOID) = 0;
virtual HRESULT STDMETHODCALLTYPE ThrowAndResume (QuadooVM::QVARIANT* pqvValue, __in_opt QuadooVM::QVARIANT* pqvCode,
__out_opt QuadooVM::QVARIANT* pqvResult) = 0;
virtual HRESULT STDMETHODCALLTYPE SetInputSource (__in_opt IQuadooInputSource* pSource) = 0;
virtual HRESULT STDMETHODCALLTYPE AddExternalClassLoader (IExternalClassLoader* pLoader) = 0;
};
interface __declspec(uuid("0ED2B4A2-61E3-4054-B3E5-9D6149AF18E8")) IQuadooDebugProvider : IUnknown
{
virtual VOID STDMETHODCALLTYPE EnableCodeStepping (bool fStepping) = 0;
virtual HRESULT STDMETHODCALLTYPE GetFileAndLine (__inout ULONG* pIP, __out RSTRING* prstrFile, __out ULONG* pnLine) = 0;
virtual HRESULT STDMETHODCALLTYPE GetBreakpoints (ULONG nFile, ULONG nLine, __out ISequentialStream* pstmIP) = 0;
virtual HRESULT STDMETHODCALLTYPE SetBreakpoint (ULONG idxIP, QuadooVM::Instruction eBreakpoint) = 0;
};
(See QuadooVM.h for the full set of interface definitions.)
Before running byte-code functions, the application can expose functionality in any of three ways:
- Register global objects with the VM.
- Register global functions with the VM.
- Attach a system call target to the VM.
The first option will be familiar to anyone who's worked with the IDispatch
interface. The application creates its global objects (derived from the IQuadooObject
interface) and adds them to the VM's global namespace using the AddGlobal()
function.
The second option involves implementing the IQuadooFunction
interface and adding objects of this type using the AddGlobalFunction()
method. These objects have only a single method, Invoke()
, which also receives the name of the function being called, so it is possible to use one instance to support multiple exposed functions. These functions are called at the global scope since they have no associated object, from the script's perspective.
The third option involves implementing the IQuadooSysCallTarget
interface and attaching an instance using the SetSysCallTarget()
method. This is the lightest weight and most performant way to expose external functionality to scripts. Like IQuadooFunction
, IQuadooSysCallTarget
also just has a single Invoke()
method. Instead of a method name, Invoke()
receives the system call number, which was either compiled directly into the bytecode through the SYSCALL_STATIC
instruction or acquired at runtime from a variable passed to the SYSCALL_DYNAMIC
instruction. The instruction emitted depends on whether a literal is used with the "syscall" function keyword. If the external system call returns E_PENDING
, then the VM immediately exits and sets the script's running state to suspended.
Another way to expose external data to scripts is through the IExternalScriptSite
interface. Names exposed through that interface appear as properties of the global scope. For example, from the ASP environment, the Response and Session objects are exposed using the IExternalScriptSite
interface.
Once the globals are loaded, the application should call RunConstructor()
to run the script's global construction code. This initializes the script's global variables.
After the global constructor completes, the application is now free to call any other function it wishes to call from the byte-code. An application uses FindFunction()
to look up the function index for a function and RunFunction()
to call that function. When RunFunction()
returns, the byte-code function has finished running.
Before unloading the VM, the application should call the Unload()
method on the VM. This ensures that everything is properly freed from the VM's byte-code stack.
Input and Output
QuadooScript provides three intrinsic functions for reading input and writing output. For QVM.exe
, the input
intrinsic reads from stdin
, and both print
and println
write to stdout
. However, QuadooScript's VM (QuadooVM.dll
) doesn't know anything about input and output streams. Instead, even input
, print
, and println
are controlled using the exposed embedding API.
Input is implemented using the IQuadooInputSource
interface, and output is implemented using the IQuadooPrintTarget
interface. Both interfaces are implemented by QVM.exe
.
interface __declspec(uuid("487A453D-60C7-4e8f-A762-D0C1F5B1466F")) IQuadooPrintTarget : IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Print (RSTRING rstrText) = 0;
virtual HRESULT STDMETHODCALLTYPE PrintLn (RSTRING rstrText) = 0;
};
interface __declspec(uuid("A3C9B280-1AA0-4ced-A0B0-28E322409A25")) IQuadooInputSource : IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Read (__inout QuadooVM::QVARIANT* pqvRead) = 0;
};
The ActiveQuadoo.dll
host also implements both interfaces, but it sends output to the Active Script environment's Response
object, and its input source can be either request variables in the ASP environment or the standard input stream for other Active Script environments.
Custom embedding hosts are also free to implement their own behaviors for these intrinsic functions so that their scripts can manipulate input and output that is appropriate for those environments.
Sample Code
The following is a quick sample for running a script from a C++ program:
RSTRING rstrName;
HRESULT hr = RStrCreateW(LSP(L"Test Script"), &rstrName);
if(SUCCEEDED(hr))
{
ISequentialStream* pScript;
hr = QuadooAllocStream(&pScript);
if(SUCCEEDED(hr))
{
CStatus status;
hr = QuadooParseToStream(L"TestScript.quadoo", QUADOO_COMPILE_LINE_NUMBER_MAP, pScript, NULL, &status);
if(SUCCEEDED(hr))
{
IQuadooInstanceLoader* pLoader;
hr = QVMCreateLoader(NULL, &pLoader);
if(SUCCEEDED(hr))
{
hr = pLoader->AddInstance(rstrName, NULL, pScript, QuadooStreamDataSize(pScript), NULL);
if(SUCCEEDED(hr))
{
HRESULT hrRegistered;
IQuadooVM* pVM;
hr = pLoader->LoadVM(rstrName, &hrRegistered, &pVM);
if(SUCCEEDED(hr))
{
IQuadooObject* pException = NULL;
CPrintTarget printer;
SideAssertHr(pVM->SetPrintTarget(&printer));
hr = pVM->RunConstructor(&pException);
if(SUCCEEDED(hr))
{
if(pException)
{
PrintException(pVM, pException);
pException->Release();
}
else
{
DWORD idxMain, cParams;
hr = pVM->FindFunction(L"main", &idxMain, &cParams);
if(SUCCEEDED(hr) && 0 == cParams)
{
QuadooVM::QVARIANT qvResult; qvResult.eType = QuadooVM::Null;
hr = pVM->RunFunction(idxMain, &qvResult);
if(SUCCEEDED(hr))
{
if(QuadooVM::Object == qvResult.eType)
PrintException(pVM, qvResult.pObject);
QVMClearVariant(&qvResult);
}
}
}
}
pVM->Unload();
pVM->Release();
}
}
pLoader->Release();
}
}
pScript->Release();
}
RStrRelease(rstrName);
}
There is a CStatus
class used for handling callbacks during the compilation phase. It can be implemented with empty methods:
class CStatus : public IQuadooCompilerStatus
{
public:
virtual VOID STDMETHODCALLTYPE OnCompilerAddFile (PCWSTR pcwzFile, INT cchFile) {}
virtual VOID STDMETHODCALLTYPE OnCompilerStatus (PCWSTR pcwzStatus) {}
virtual VOID STDMETHODCALLTYPE OnCompilerError (HRESULT hrCode, INT nLine, PCWSTR pcwzFile, PCWSTR pcwzError) {}
};
In the example above, a printing target is attached with a fake IUnknown
implementation, mainly because its lifetime doesn't need to extend beyond that stack frame. Production code should use reference counting.
class CPrintTarget : public IQuadooPrintTarget
{
public:
// IUnknown
HRESULT WINAPI QueryInterface (REFIID iid, LPVOID* lplpvObject) { return E_NOTIMPL; }
ULONG WINAPI AddRef (VOID) { return 2; }
ULONG WINAPI Release (VOID) { return 1; }
virtual HRESULT STDMETHODCALLTYPE Print (RSTRING rstrText)
{
wprintf(L"%ls", RStrToWide(rstrText));
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE PrintLn (RSTRING rstrText)
{
wprintf(L"%ls\r\n", RStrToWide(rstrText));
return S_OK;
}
};
Finally, the example above makes calls to a PrintException()
function when exceptions have occurred.
HRESULT PrintException (IQuadooVM* pVM, IQuadooObject* pException)
{
QuadooVM::QVPARAMS qvParams; qvParams.cArgs = 0;
QuadooVM::QVARIANT qvString; qvString.eType = QuadooVM::String;
HRESULT hr = pException->Invoke(pVM, RSTRING_CAST(L"ToString"), &qvParams, &qvString);
if(SUCCEEDED(hr) && QuadooVM::String == qvString.eType)
wprintf(L"%ls\r\n", RStrToWide(qvString.rstrVal));
QVMClearVariant(&qvString);
return hr;
}
Note that this example is using wprintf()
to output information to the console. This might not even be correct for a console application, depending on the selected character set. Regardless, host applications that run with a GUI or as a back-end server will need to use reporting mechanisms that are appropriate for their environments.
Native Objects
On its own, QuadooScript is just a programming language. It doesn't know how to access the network or read from databases. Even with the Host object, QuadooScript has limited file system support. Also, as a byte-code language running on a virtual machine, code written in QuadooScript will never be as fast as native code. Therefore, there will always be reasons why it will make sense to provide external functionality to QuadooScript from C++ (or any language that can implement QuadooScript's interfaces).
The QuadooScript package already contains multiple external native modules, including the WinHttp and Cryptographic modules. As a developer using QuadooScript, you may find that you want to write your own native modules that your scripts can call.
To be clear, QuadooScript itself doesn't know anything about ActiveX or the mechanisms involved with loading external modules. Creating a module that can be loaded by Host.CreateObject()
or by Host.LoadQuadoo()
only applies to environments that include the Host
object, such as QVM.exe, WQVM.exe, and ActiveQuadoo.dll. QuadooScript itself (i.e. QuadooVM.dll) only knows about IQuadooObject
without regard for how the object was loaded.
Creating a Native QuadooScript Module
Creating a native QuadooScript module is easy, but it requires some overhead to be implemented. A native module for QuadooScript is basically the same as any other ActiveX control that exposes IDispatch
objects. To be usable by QuadooScript, the module must expose objects implementing IQuadooObject
. A module could expose IDispatch
too if it wanted to be compatible with ActiveScript environments.
The first step to creating a module is to create a C++ project for a dynamic link library. At the very least, the project will need to expose DllGetClassObject() using a module definition file (or equivalent mechanism).
A QuadooScript module's class factory should support at least two class IDs, even if they both create the same object. For example:
BEGIN_GET_CLASS_OBJECT
EXPORT_FACTORY(CLSID_QSWinHttp, CQSWinHttp)
// QuadooScript plug-ins always support the CLSID_QuadooObject class.
EXPORT_FACTORY(CLSID_QuadooObject, CQSWinHttp)
END_GET_CLASS_OBJECT
The standard CLSID_QuadooObject
class ID (defined in QuadooObject.inc) is used to load a module without it being registered with the operating system. Host.LoadQuadoo()
loads unregistered modules using only its file system path. For other environments, including ActiveScript (using ActiveQuadoo.dll), it may be necessary to register the module with the system and load it by its registered name using COM
(i.e. via CLSIDFromString()
and CoCreateInstance()
). In that case, the module must also expose its module-specific class ID associated with its component reference.
By convention, a native module typically has its DllGetClassObject()
function and other module-specific overhead defined in a file called DLLMain.cpp
. The class factory code is included by DLLMain.cpp
. A developer using ATL or another framework might have a similar or different structure.
The class factory will support the creation of an object that implements IQuadooObject
. This will be the main object for the module. In the example above, CQSWinHttp
is the class instantiated whenever either CLSID_QSWinHttp
or CLSID_QuadooObject
is requested. To be loaded natively by QuadooScript, the object must implement IQuadooObject
. If VBScript loads the same object, it will query for IDispatch
. An object could implement both interfaces for maximum compatibility. In QuadooScript's case, if the object only supports IDispatch
, then QuadooScript will provide an adapter for the object.
If a native module loaded through COM
wants to participate in dynamic unloading, it must expose the DllCanUnloadNow() function. Every object created by the module, except for the class factory object, must manage an object counter. This is typically done from the constructor and destructor of every object that implements IQuadooObject
in the module. For example:
CQSWinHttp::CQSWinHttp () :
m_pWinHttp(NULL)
{
DLLAddRef();
}
CQSWinHttp::~CQSWinHttp ()
{
SafeRelease(m_pWinHttp);
DLLRelease();
}
These DLLAddRef()
and DLLRelease()
functions simply increment and decrement a counter, respectively. If the module was loaded by COM
(using Host.CreateObject()
from script), then the module's DllCanUnloadNow()
function will be called periodically (by COM
) to determine whether the module can safely be unloaded. If this function is not already defined by your framework, then it might be defined like this:
STDAPI DllCanUnloadNow (VOID)
{
return (0 == InterlockedCompareExchange(&CDLLServer::m_pThis->m_cReferences, 0, 0)) ? S_OK : S_FALSE;
}
DLL registration and unregistration rely on implementing and exposing the standard DllRegisterServer()
and DllUnregisterServer()
functions from your module, respectively. If Host.CreateObject()
fails to load an object, it is often because either the name isn't correctly associated to a class ID or the class ID is not correctly registered with the module's file path.
Optional Class Factory and Registration Support
Three optional functions are available from SimbeyCore.dll
to simplify the implementation of class factories and DLL registration:
typedef HRESULT (WINAPI* QUERYCREATEIID)(REFIID, PVOID*);
struct CLASS_FACTORY_OBJECT
{
const CLSID* pclsid;
QUERYCREATEIID pfnQueryCreateIID;
};
HRESULT WINAPI ScCreateClassFactory (__in_ecount(cDefs) const CLASS_FACTORY_OBJECT* pcfo, sysint cDefs,
REFCLSID rclsid, REFIID riid, __deref_out PVOID* ppvObject);
HRESULT WINAPI ScRegisterServer (HMODULE hModule, const IID& iidClass, PCWSTR pcwzProgID, PCWSTR pcwzModuleDescription);
HRESULT WINAPI ScUnregisterServer (const IID& iidClass, PCWSTR pcwzProgID);
Three macros (used in the CQSWinHttp
example above) for implementing the class factory are defined like this:
#define BEGIN_GET_CLASS_OBJECT \
HRESULT WINAPI DllGetClassObject (REFCLSID rclsid, REFIID riid, __deref_out PVOID* ppvObject) \
{ \
static const CLASS_FACTORY_OBJECT cfo[] = \
{ \
#define EXPORT_FACTORY(clsid, class) \
{ &clsid, class::QueryCreateIID },
#define END_GET_CLASS_OBJECT \
}; \
return ScCreateClassFactory(cfo, ARRAYSIZE(cfo), rclsid, riid, ppvObject); \
}
As you would guess, CQSWinHttp
defines (through inheritance) a static method called QueryCreateIID()
:
template <typename TFinalClass>
class TBaseUnknown : public CBaseUnknown
{
public:
static HRESULT CreateInstance (__deref_out TFinalClass** ppObj)
{
HRESULT hr;
Assert(ppObj);
*ppObj = __new TFinalClass;
if(*ppObj)
{
hr = (*ppObj)->FinalConstruct();
if(FAILED(hr))
(*ppObj)->Release();
}
else
hr = E_OUTOFMEMORY;
return hr;
}
static HRESULT WINAPI QueryCreateIID (REFIID riid, __deref_out PVOID* ppvObject)
{
TFinalClass* pObject;
HRESULT hr = CreateInstance(&pObject);
if(SUCCEEDED(hr))
{
hr = pObject->QueryInterface(riid, ppvObject);
pObject->Release();
}
return hr;
}
};
If your implementation has a compatible structure with a static method (or global function) having the same signature as the QueryCreateIID()
method, then you may want to consider using the ScCreateClassFactory()
function to simplify your code.
Registration and unregistration rely upon each module defining a module-specific class that inherits from a CDLLServer
class. DLLMain.cpp always instantiates a global instance of a subclass of that class.
HRESULT WINAPI DllRegisterServer (VOID)
{
return CDLLServer::m_pThis->RegisterServer();
}
HRESULT WINAPI DllUnregisterServer (VOID)
{
return CDLLServer::m_pThis->UnregisterServer();
}
HRESULT CDLLServer::RegisterServer (VOID)
{
return ScRegisterServer(m_hModule, GetStaticClassID(), GetStaticProgID(), GetStaticModuleDescription());
}
HRESULT CDLLServer::UnregisterServer (VOID)
{
return ScUnregisterServer(GetStaticClassID(), GetStaticProgID());
}
The last line in DLLMain.cpp instantiates the module-specific subclass of CDLLServer
:
CQSWinHttpModule g_module;
All of this is optional if you are using ATL or another framework that provides equivalent class factory and module registration functionality.
Sample Module Project
There is a sample module project available on GitHub that can be used as a template for creating a new external native module for QuadooScript.
The sample module can be used from script as follows:
var oDemo = Host.LoadQuadoo("QSDemoModule.dll");
oDemo.Navigate();
ActiveScript and ASP
Included in the QuadooScript package is a file called ActiveQuadoo.dll. This is a version of QVM.exe that can be loaded by an ActiveScript host, such as CScript.exe, WScript.exe, or by ASP.dll (using IIS).
When loaded by an ActiveScript host, the output routines (Host.Write()
, Host.WriteLn()
, Host.BinaryWrite()
, print()
, and println()
) are redirected through the host. In the case of CScript.exe, output will appear on the console, but with WScript.exe output will appear in popup message boxes. When used from the ASP environment of IIS, output is sent to the web browser!
For a web page to use QuadooScript, it must tell the ASP environment that it is written in QuadooScript by placing the following declaration at the top of the web page:
<%@ language="QuadooScript" %>
Alternatively, the ASP environment can be configured to use QuadooScript by default.
When using QuadooScript outside the ASP environment, the #include ""
directive can be used to include other QuadooScript files. In the ASP environment, files are included using special ASP syntax:
<!-- #include file="..\inc\core.asp" -->
That still inserts the specified file's text into the page at that location, but ASP will automatically remap line numbers for error logs when exceptions occur.
Unlike other scripting languages, QuadooScript does not allow executable statements (other than variable declarations) at the global scope. Therefore, all text blocks must be typed within functions. After declaring QuadooScript as the page's language, the remainder of the page will be wrapped within <%
and %>
markers. Free text would then be placed within %>
and <%
markers. Just like with a script running under QVM.exe, execution begins with the main()
function.
QuadooScript never sees free text blocks or the text that they contain. The ASP environment converts those blocks into Response.WriteBlock()
calls. The Response
object is provided by the ASP environment at runtime.
The ASP environment is somewhat language agnostic and doesn't know what kind of language syntax will ultimately be used, but it assumes that Response.WriteBlock()
will be valid syntax. Fortunately, that is valid QuadooScript syntax, with one issue. The ASP environment does not emit a semicolon at the end of the statement. Normally, this would be a syntax error for QuadooScript. However, when used within the ActiveScript framework, parsing is done slightly differently, and semicolons at the end of statements become optional.
Since so much of an ASP page will involve calling methods on the host's Response
object, it would be a good idea to declare the Response
object upfront using extern
.
extern Response;
extern Session;
If you intend to use ASP's Session
object, then it could be declared as well.
While you could also declare the Request
object, you might want to use a special version of the Request
object that is built into ActiveQuadoo.dll.
var Request = Host.ParseFiles(Session);
var Browser = Request.Browser;
This works only if form variables have not been accessed yet. The returned Request
object will now parse the form variables instead of ASP's object. The benefit is that ActiveQuadoo's object can handle both uploaded file data (multipart/form-data
) and JSON data (application/json
) in addition to normal form data (application/x-www-form-urlencoded
). There are also a few convenience methods available that make it easy to route the page request to handlers based on form variables.
The following is an example of a web page that uses QuadooScript:
<%@ language="QuadooScript" %>
<%
extern Response;
extern Session;
var Request = Host.ParseFiles(Session);
var Browser = Request.Browser;
function main ()
{
%>
This is my page text! Your browser is: <% =Browser.Browser %>!
<%
}
%>
When using ActiveQuadoo.dll in the ASP environment, the Host
object exposes an indexed ServerVariables
property that can be used for reading the content type and other values before deciding how to process the request. The indexed Host.ServerVariables
property is available even without using Host.ParseFiles()
.
var strContentType = Host.ServerVariables["CONTENT_TYPE"]);
Request Properties
There are three collections (map objects) on the Request
object that can be retrieved directly as exposed properties:
- Form
- Cookies
- Files
Since those properties return maps, all methods available to maps can be used with those objects.
When files are uploaded, each file's name is accessible using Request.Form["file_upload_field"]
. The file data is exposed as a binary data object from Request.Files["file_upload_field"]
. The binary data object also exposes the file's name.
Uploaded files can be enumerated using the following code:
var mapFiles = Request.Files;
for(int i = 0; i < len(mapFiles); i++)
{
var oData = mapFiles.GetValue(i);
var strName = oData.FileName;
}
Normal form variables can also be retrieved using this syntax: Request["form_variable_name"]
.
Server variables are retrieved using the indexed ServerVariables
property.
var Request = Host.ParseFiles(Session);
...
println(Request.ServerVariables["REMOTE_ADDR"];
If there is a query string that's different from the form, its variables can be retrieved using the indexed QueryString
property. The query string, if available, can also be returned as a string using the QueryString
property. The cookies can also be returned as text using the CookieText
property.
The Request
object also exposes ContentType
, UserAgent
, and Browser
properties.
Request Methods
The Request
object supports several methods for simplifying common operations like checking for form variables and making decisions based on form variables.
- Has(strName) - Returns true if the form contains the specified variable name.
- HasInvoke(strName, fnCallback) - Calls fnCallback with the value of strName if it exists, then returns true. Otherwise, false is returned.
- GetFormVariables() - Returns the form variables as a string.
- ReplaceFormVariables(strForm) - Replaces the form variables with the provided form string. This method fails if there are uploaded files.
- OptFind(strName, vDefault) - Returns the form value specified by strName or vDefault is the name doesn't exist in the form.
- Select(oPage, aVariables, strPrefix) - Calls methods on oPage based on whether the form contains a name from the array, then returns true. Returns false if none of the names were found in the form.
The Request.Select()
method allows easy routing based on the form. Form buttons with names can be used to select a different code path into an object managing the web page, for example. If nothing matches the contents of the array, then a default code path can be taken.
The following example would select a different code path based on the form:
class CPage
{
function Main ()
{
}
function DoEdit (strValue)
{
}
function DoNew (strValue)
{
}
function DoDelete (strValue)
{
return false; // Returning false causes the Select() call to return false.
}
}
function main ()
{
var oPage = new CPage;
if(!oPage.Select(oPage, { "Edit", "New", "Delete" }, "Do"))
oPage.Main();
}
The third parameter is an optional prefix. If specified, the method name expected to be called has the prefix value added. The form variables do not contain the prefix in their names.
Writing Binary Data
When sending binary data, such as a file, back to the client, an ASP script can send data through the standard Response.BinaryWrite()
method. QuadooScript's ActiveQuadoo.dll module also supports a convenience method for sending large data buffers as smaller chunks with optional Flush()
calls when ASP page buffering is enabled.
The Host.BinaryWrite()
method performs the following actions:
- ASP's
Response.Buffer
property is read to determine whether buffering is enabled. - The large binary data is fragmented into 2MB chunks to be sent separately to
Response.BinaryWrite()
. - The 2MB SAFEARRAY is allocated once and reused until the remaining data is smaller than 2MB.
- If buffering is enabled, and if there is still data remaining to be sent, then
Response.Flush()
is called. - If buffering is not enabled, then 2MB chunks are sent without any
Response.Flush()
calls.
The default size for the "Response Buffering Limit" setting in ASP is 4MB. When using Host.BinaryWrite()
with buffering enabled, there is no need to increase the buffering limit.
Page Transfers
When using ActiveQuadoo's Request
object, the Host.Transfer(Session, Server, strPage [, strForm])
method can be used to transfer control to a different page. The Session
and Server
objects must be passed to the method because, internally, Server.Transfer()
is ultimately used to transfer, and all the rules and conditions of that method apply.
The benefit to using Host.Transfer()
is that this method will also collect data and pass it through the Session
object to be retrieved by the target page.
- The form data will be collected and attached to session variable
TransferForm
. - The server variable
SCRIPT_NAME
will be set as session variableReturnPage
. - Session variable
Transferred
will be set to true.
If the target page wants to continue using the transferred form, then it should use Host.ParseFiles(Session)
to collect the form data. If the target page wants to preserve the form data, then it should exclude the Session
data from the call.
// Do not read the page transfer
var Request = Host.ParseFiles();
This model can be used to transfer control to a login page whenever credentials expire. If credentials are successfully entered, then control can be passed back to the original page (using Server.Transfer()
) without any loss of data or extra coding. The original page might not even realize that a page transfer occurred.
In the login model that has been described, it is important to note that the login page would use ASP's Server.Transfer()
method to return control to the original page, instead of using Host.Transfer()
. This is because once the user has entered credentials on the login page, control needs to be returned to the original page without overriding the transferred page data that's already in the Session
object. ASP's Server.Transfer()
method merely transfers control to the new page without changing any other session data.
JSON Requests
ActiveQuadoo.dll handles incoming JSON requests natively. If the content type of the request is application/json
, then ActiveQuadoo.dll parses the request data as UTF-8 JSON text. The JSON data is accessible by reading the Request.JSON
property.
In addition to the JSON property, if the parsed JSON data is a JSON object, then the top-level fields of the JSON object are copied to the field map so that they can be accessed as if they had been set by a regular query string. Some scripts may handle either requests using query strings or requests using JSON data, without knowing or caring about the type of request.
As an example, if the script receives this JSON data:
{
"type": "json_sample_data",
"samples: [1, 2, 3, 4]
}
The script could then use Request["type"]
and Request["samples"]
to access the data fields, in addition to using Request.JSON
to access the original JSON object.
Custom Requests
You may have noticed that application/xml
does not have native support by QuadooScript. To be fair, classic ASP didn't handle XML requests either (and also didn't handle JSON requests). There may be other content types that a script might want to handle, and QuadooScript couldn't possibly know about all of them.
If a script is intended to receive custom request types (including XML), then it must call Request.LoadData(oHandler)
before anything attempts to read field data from the Request
object. Care must be taken to design the code flow so that the Request
object is created, and then LoadData()
is called before any field data is accessed.
class CParser
{
function ParseData (oData, mapFields, mapFiles)
{
if(oData.ContentType == "application/xml")
{
// Parse the data, set fields, attach files (if applicable)
return true;
}
return false;
}
}
function main ()
{
var oRequest = Host.ParseFiles(Session);
oRequest.LoadData(new CParser);
// Read from oRequest
}
In the sample above, a script class called CParser
is instantiated and passed to LoadData()
immediately after creating the Request
object. The script class's ParseData()
method is called with three parameters: the binary data object holding the request data, the field map, and the files map.
The script's ParseData()
method should check the data object's ContentType
for types it understands. If it understands the content type and successfully parses the data, it should return true
. If the data is not handled, it should return false
.
If the script understands and handles the binary data, it has the opportunity to write some field data into the mapFields
map object. If the custom binary data contains files, they can also be added to the mapFiles
map object.
Character Sets and Multipart Form Data
ActiveQuadoo.dll recognizes the special _charset_
form field when parsing multipart/form-data
forms. For example:
<form method="post" action="page.asp" enctype="multipart/form-data">
<input type="hidden" name="_charset_">
When the hidden _charset_
form field is included as shown above, the web browser automatically fills the field's value with the name of the character set selected by the browser. ActiveQuadoo.dll begins parsing the form with the ISO 8859-1 character set but switches to a different character set when the parser encounters the _charset_
form field. The field itself is not included in the form variable set that is visible to the script.
Global.asa
QuadooScript can also be used to implement a website's Global.asa
file. For example:
<script language="QuadooScript" runat="server">
extern Application catch;
extern Session catch;
function Application_OnStart ()
{
Application["Visitors"] = 0;
}
function Application_OnEnd ()
{
}
function Session_OnStart ()
{
Application["Visitors"] = Application["Visitors"] + 1;
Session["Started"] = nowutc();
}
function Session_OnEnd ()
{
Application["Visitors"] = Application["Visitors"] - 1;
}
function main ()
{
}
</script>
Each time ASP runs the Global.asa
script, first the global constructor is called, followed by the main()
function, which must be present but can be empty. Finally, ASP calls one of the other global functions, whose names must be:
- Application_OnStart
- Application_OnEnd
- Session_OnStart
- Session_OnEnd
When the Application_OnStart()
and Application_OnEnd()
functions are called, the Session
object is unavailable, but, in the example above, it is still defined globally using the extern Session catch
syntax. This syntax ensures that the global constructor does not throw an exception if the object cannot be retrieved.
WebSockets
In addition to serving ASP requests from IIS, ActiveQuadoo.dll can also handle WebSocket requests using QuadooScript.
Setup
The first step to using the WebSocket handler is registering ActiveQuadoo.dll as an HTTP request handler. The standard APPCMD.EXE
IIS utility should be used to register ActiveQuadoo.dll. Next, verify that the following three XML fragments are configured in the applicationHost.config
file:
<globalModules>
...
<add name="WebSocketModule" image="%windir%\System32\inetsrv\iiswsock.dll" />
<add name="WebSocketModule32" image="%windir%\SysWOW64\inetsrv\iiswsock.dll" />
<add name="QuadooWebSocket" image="C:\path\ActiveQuadoo.dll" />
</globalModules>
<handlers accessPolicy="Read, Script">
...
<add name="QuadooWebSocket" path="*.qws" verb="*" modules="WebSocketModule32" scriptProcessor="C:\path\ActiveQuadoo.dll"
resourceType="File" preCondition="bitness32" />
...
</handlers>
<location path="Default Web Site">
<system.webServer>
<handlers>
<remove name="QuadooWebSocket" />
<add name="QuadooWebSocket" path="*.qws" verb="*" modules="QuadooWebSocket" scriptProcessor="C:\path\ActiveQuadoo.dll"
resourceType="File" requireAccess="Script" preCondition="bitness32" />
</handlers>
</system.webServer>
</location>
Creating a WebSockets Script
After registering ActiveQuadoo.dll for handling WebSockets, then a script file with the .qws
extension can be invoked as a WebSocket handler. The following script could be used to test the system:
extern Host;
interface IWebSocket
{
virtual function OnOpen () = 0;
virtual function OnPacket (v) = 0;
virtual function OnClose (nStatus, strReason) = 0;
};
class CWebSocket : IWebSocket
{
virtual function OnOpen ()
{
Host.Send("Hello, WebSocket!");
}
virtual function OnPacket (vPacket)
{
// vPacket will either be a string or a binary object
}
virtual function OnClose (nStatus, strReason)
{
Host.Output("Status: " + nStatus + ", Reason: " + strReason);
}
};
function websocket (strProtocol, strExtensions)
{
Host.SetHeader("Sec-WebSocket-Protocol", "test");
return new CWebSocket;
}
In the WebSocket environment, the Host
object has all the base methods and properties available plus five additional methods specific to WebSockets.
- Send(vData) - Sends either a string or binary object to the client.
- Close(nStatus, strReason) - Sends the provided status and reason text and closes the connection.
- SetHeader(strName, strValue) - Sends a header name and value to the client but should only be used during the
websocket()
call. - Output(strDebug) - Sends text to the debug output.
- Post(nSocket, oJSONObject) - Sends a JSON object to the WebSocket handler identified by
nSocket
.
As you can see from the example script, a new WebSocket handler is instantiated when the websocket()
function is called. The script instantiates a new class that inherits from the IWebSocket
interface, which has three methods:
- OnOpen() - Called once after the headers have been flushed to the client, used for sending initiation messages to the client.
- OnPacket(vPacket) - Called whenever a packet has been received. The
vPacket
will either be a string or a binary object. - OnClose(nStatus, strReason) - Called after receiving a close event from the client.
Every WebSockets script must define and implement the IWebSocket
interface, and it can be copied from here.
interface IWebSocket
{
virtual function OnOpen () = 0;
virtual function OnPacket (v) = 0;
virtual function OnClose (nStatus, strReason) = 0;
};
For the earlier example, JavaScript can connect using this code:
var oSocket = new WebSocket("ws://localhost/test.qws", ["test"]);
oSocket.onopen = function ()
{
oSocket.send("Hello!");
};
oSocket.onmessage = function (evt)
{
alert("Received: " + evt.data);
};
oSocket.onclose = function ()
{
};
Socket Groups
By default, WebSocket handlers are islands within an IIS worker process. WebSocket handlers are not normally aware of each other. However, if a handler also inherits from the ISocketGroup
interface, then handlers may communicate asynchronously with each other.
interface ISocketGroup
{
virtual function OnRegister (nSocket) = 0;
virtual function OnJoin (nSocket) = 0;
virtual function OnRemove (nSocket) = 0;
virtual function OnMessage (nSender, oMessage) = 0;
};
To implement a WebSocket handler that joins the socket group, the handler must inherit from both interfaces:
class CWebSocket : IWebSocket, ISocketGroup
{
...
When the WebSocket handler is registered with the socket group, its OnRegister()
method is called with its socket number. The OnRegister()
method will be called after the handler's OnOpen
method is called.
When other WebSocket handlers are added to the group, their socket numbers are passed to each handler's OnJoin()
method. When handlers are disconnected, each other handler's OnRemove()
method is called.
A WebSocket handler can send a JSON object to another handler by calling the Host.Post()
method:
var oMsg = JSONCreateObject();
oMsg.hello = "Hello, WebSocket!";
Host.Post(nOtherSocket, oMsg);
There is no return value from the Post()
call because the call is made asynchronously, but the call will throw an exception if there are problems posting the message. The target WebSocket will receive the message to its OnMessage()
method. Only JSON objects are allowed to be sent between WebSocket handlers. The JSON object should not be modified further after sending it because the receiving handler may begin reading from it immediately on another thread.
WebSocket handlers registered with the socket group may query the Host
object for the following properties:
- GroupSocket - Returns the socket number of the current WebSocket handler.
- Sockets - Returns a JSON array of all the registered socket numbers.
Controlling WebSocket Handlers
WebSocket handlers can effectively "register" for notification callbacks from external systems by passing their Host
object to that external system. When that external system receives such a call containing the Host
object, then that system should query the object for the IWebSocketsInterface
interface.
interface __declspec(uuid("7D9081A8-811A-42f0-A0A9-8BD2D5C041E1")) IWebSocketsInterface : IUnknown
{
virtual HRESULT STDMETHODCALLTYPE GetInterface (RSTRING rstrInterface, __deref_out IQuadooInterface** ppInterface) = 0;
virtual HRESULT STDMETHODCALLTYPE ProtectedCall (IQuadooInterface* pInterface, ULONG idxCall, QuadooVM::QVPARAMS* pqvParams,
__out QuadooVM::QVARIANT* pqvResult) = 0;
virtual HRESULT STDMETHODCALLTYPE SendToClient (QuadooVM::QVARIANT* pqv) = 0;
};
Once another system has the IWebSocketsInterface
interface pointer, then it can query the script for custom interfaces, make protected interface calls into the script, or send JSON objects or binary data directly to the client.
The following example demonstrates creating an object in the script handler's OnOpen()
method and then registering itself for callbacks.
var m_oExternalSystem;
virtual function OnOpen ()
{
m_oExternalSystem = Host.CreateObject("MyOrganization.MyExternalSystem");
m_oExternalSystem.Register(Host);
}
virtual function OnClose (nStatus, strReason)
{
m_oExternalSystem.Unregister(Host);
m_oExternalSystem = null;
}
When the other object's Register()
method is called, it will query for the IWebSocketsInterface
object and then retrieve a callback interface using the GetInterface()
method. Both the hypothetical Register()
method and callback interface represent a contract defined by the developers of those parts. ActiveQuadoo.dll
has no interest in the designs of the callback system once it provides the Host
object to the other system.
For a simple notification system, the callback interface could have just one method.
interface IMyNotification
{
virtual function OnExternalNotification (oJSON) = 0;
};
class CWebSocket : IWebSocket, IMyNotification
{
virtual function OnExternalNotification (oJSON)
{
Host.Send(oJSON);
}
...
For this example, the external system would use GetInterface()
to query for the IMyNotification
interface. In most cases, it would make sense to cache the interface and its callback method index. When the external system needs to make the callback, it must always call the interface using the ProtectedCall()
method, which protects the script call using an internal critical section. Also for this example, it's important to note that the external system could pass its data directly to the client using the SendToClient()
method.
Since the external system must deal with C++ interfaces, either C++ or a language compatible with C++ interfaces must be used to implement the IQuadooObject
object that receives the script's Host
object and queries it for IWebSocketsInterface
.
Delayed Timer Callbacks
The ASP and WebSockets hosts contain two methods for managing delayed timer callbacks.
- StartTimer(strTimerName, msTimeout, vData) - Starts a timer using the provided name, timeout (milliseconds), and JSON data.
- StopTimer(strTimerName) - Attempts to stop a timer using the provided name, throws an exception if the timer can't be stopped.
Timer callbacks are registered under arbitrary names, along with a timeout in milliseconds, and any kind of JSON data (i.e. null
, a string, a JSON array, or a JSON object). If a timer with the same name is already registered, then the JSON data is added to an array that will eventually be passed to the timer callback when it fires.
function main ()
{
var oData = JSONCreateObject();
// Write some data into oData
oData.stuff = 123;
Host.StartTimer("timer_123", 5000, oData);
}
The timer callback function that will be called is TimerMain(), which is defined in the same script that registers the timer.
function TimerMain (aData)
{
for(int i = 0; i < len(aData); i++)
{
var oData = aData[i];
// Do something with oData
}
}
Notice that the required parameter is actually a JSON array.
Each call to Host.StartTimer()
adds JSON data to the array that is passed to TimerMain()
when the timer fires.
The TimerMain()
function can also be defined with two parameters to receive the timer's name.
function TimerMain (strName, aData)
{
}
The Host.StopTimer()
can be used to stop a registered timer. If successful, it returns the array of JSON data that would have been passed to the timer callback.
Timer Host/VM Environment
When registering a timer callback, QuadooScript keeps a reference to the compiled byte-code of the calling script and reuses it for the timer callback later. The timer itself is registered with the Windows thread pool, and when it fires the callback runs from an arbitrary thread pool worker thread.
When the TimerMain()
function is called, the script is no longer running under the original host. In fact, the script is running in a new VM instance too. Timer callbacks may run seconds, minutes, or hours after the original script completed, and there is no longer any connection to either the ASP or WebSockets connections.
The Host
object for a timer callback still supports the common Host
methods, but it does not support the ASP or WebSockets specific methods. However, the timer callback's Host
object still supports the StartTimer()
and StopTimer()
methods.
When writing a script that includes timer callbacks, some considerations must be made to avoid invoking objects that may not exist from the global scope. For example, while Request
and Session
can still be defined using extern
, remember that they will be null
when running in a timer callback.
Calling Host.ParseFiles()
from the global scope does still work (it returns null
) when running as a timer callback.
var Request = Host.ParseFiles();
Another consideration for timer callbacks is limited functionality for resolving virtual paths. When running under ASP and WebSockets, virtual paths can be resolved by the underlying IIS system. When a timer callback runs, it no longer has the ASP or WebSockets connections, so it is limited in its ability to resolve relative file system paths. Only the root "/"
path is mapped and cached for the timer callback. If a timer callback knows that it will need to resolve other paths, then it should resolve relative paths before registering a timer and include that information in the JSON data.
Web Services
QuadooScript is a great choice for creating web services in the classic ASP environment.
The following script could be used as a template for creating web services that handle JSON requests and return JSON responses.
<%@ language="QuadooScript" %>
<%
var Request = Host.ParseFiles();
function WritePageJSON (vJSON)
{
Response.ContentType = "application/json";
Response.CacheControl = "no-store";
print((string)vJSON);
}
function WriteCustomJSONError (nStatus, strError)
{
var oError = JSONCreateObject();
oError.error_message = strError;
Response.Status = nStatus + " Error";
WritePageJSON(oError);
}
function WriteJSONError (strError)
{
WriteCustomJSONError(400, strError);
}
class CMyWebService
{
CMyWebService ()
{
// Initialize things here
}
function _hello ()
{
var oHello = JSONCreateObject();
oHello.message = "Hello, Web Service!";
WritePageJSON(oHello);
}
function _error ()
{
WriteJSONError("This is an Error!");
}
};
function main ()
{
var oWebService = new CMyWebService;
var strAction = Request.OptFind("action", "");
try
invoke("_" + strAction, oWebService);
catch(e)
{
var oError = JSONCreateObject();
var vValue = e.Value;
oError.error_message = e.ToString();
if(QVType.String == typeof(vValue))
oError.exception = vValue;
else
oError.exception = "Code: " + (string)vValue;
oError.action = strAction;
Response.Status = "500 Exception";
WritePageJSON(oError);
}
}
%>
This model makes it easy to add additional "actions" by simply adding new methods that begin with an underscore. These methods are called automatically via the use of the invoke()
call, but only methods beginning with an underscore are directly callable externally.
In this example, a JSON response can easily be returned using the WritePageJSON()
function, and errors can be returned using the WriteJSONError()
function. Any unhandled errors are always returned using the catch
handler in the main()
function.
Scripts as Modules
Just as Host.LoadQuadoo()
can be used to load native modules, it can also load external script files as modules to be used by the calling script. These scripts can be either plain text script files or pre-compiled QBC files.
To load an external script as a module, it must implement a CreateObject()
function. This function is called internally by the Host.LoadQuadoo()
call and should return some kind of object that can be used by the caller.
class MyObject
{
function MyMethod ()
{
}
};
function CreateObject (vParam)
{
return new MyObject;
}
The calling script would then load the above script as a module and call the MyMethod()
method.
var oModule = Host.LoadQuadoo("MyModule.quadoo");
oModule.MyMethod();
Scripts can also be pre-compiled into QBC files, which can then be loaded with a Host.LoadQuadoo()
call.
CompileQuadoo.exe <Input Script File> <Output File.QBC>
An optional value can be passed through the Host.LoadQuadoo()
call to be used in the object creation process.
var oModule = Host.LoadQuadoo("MyModule.qbc", 12345);
oModule.MyMethod();
The optional value, when provided, is passed to the script module's CreateObject()
function as its vParam
parameter. If no value is provided, then vParam
receives a null
value.
Compile to Executable
Scripts and pre-compiled QBC files can also be embedded into a single executable file.
CompileQuadoo.exe -e <Script File> <Target Executable> [-i <Icon File>] [-t <Template>] [-a <Arguments>]
Optional command line arguments for CompileQuadoo.exe:
-i
Specifies an ICO file for the generated executable.-t
Specifies the template binary for the executable (QVMT.BIN is used by default).-a
If present, all text after this option is copied into the new executable to be used as the base command line text passed to the embedded script.
The generated executable contains the compiled script and optionally the icon and any provided command line arguments. Additional command line arguments provided when launching the executable are simply appended to the embedded command line arguments.
If the embedded command line arguments do not already contain the -args
parameter, then it is added before adding additional command line arguments passed to the executable when launched. This means that while embedded command line arguments can alter the behavior of the host, command line arguments passed to the executable are only script-level arguments, and -args
does not need to be explicitly passed to the generated executable by the user.
The above diagram was generated using a QuadooScript wrapper for the Pikchr library.
WinHttp Plug-in
Since so much of what developers need to do involves making web calls, it makes sense to include a WinHttp plug-in in the QuadooScript package. The QSWinHttp.dll plug-in makes it easy to send and receive data from web endpoints.
#define WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY 4
function main ()
{
var oHttp = Host.LoadQuadoo("QSWinHttp.dll");
var oSession = oHttp.Open("WinHttp Web Agent", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, null, null);
var oServer = oSession.Connect("www.quadooscript.com", 0);
var oRequest = oServer.OpenRequest("GET", "/", null, null, 0, null, null);
wait(oRequest, -1);
println(oRequest.Status);
println(oRequest.ToText());
}
Alternatively, if QSWinHttp.dll
is registered and needs to be opened in a COM
environment (e.g. ASP
), then it would be loaded like this:
var oHttp = Host.CreateObject("Simbey.QSWinHttp");
Server objects may also be opened with a user name and password.
var oServer = oSession.Connect("www.quadooscript.com", 0, "user", "password");
The server object's OpenRequest()
method expects seven arguments:
- strVerb
- strResource
- strReferrer
- vAcceptTypes - Can be null, a string, or an array of strings
- nFlags
- strHeaders - Can be null or a string
- vBody - Can be null, a string, a binary object, or a JSON object
If the response is binary data, then the ToBinary()
method should be used.
Response headers can be obtained by accessing the Headers
property, but a specific header can be retrieved using the GetHeader()
method.
POST Requests
To make a POST
request, set the verb to POST
in the OpenRequest()
call. The Content-Type
header should be specified, and form variables will be sent for the request body.
var oRequest = oServer.OpenRequest("POST", "/webapi.asp", null, null, 0,
"Content-Type: application/x-www-form-urlencoded", "name=value&field=data");
If the request is uploading files, then the multipart/form-data
content type should be used.
Secure Sessions
To use a secure session, the WINHTTP_FLAG_SECURE
flag should be passed to the OpenRequest()
method, and 443 should be specified for the port in the Connect()
call.
#define WINHTTP_FLAG_SECURE 0x00800000
var oServer = oSession.Connect("www.duckduckgo.com", 443);
var oRequest = oServer.OpenRequest("GET", "/", null, null, WINHTTP_FLAG_SECURE, null, null);
Security Options
Before opening a request, security options can be set for ignoring certificate issues, including enabling the use of self-signed certificates on servers.
#define SECURITY_FLAG_IGNORE_UNKNOWN_CA 0x00000100
var oServer = oSession.Connect("secure.mytestserver.com", 443);
oServer.Security = SECURITY_FLAG_IGNORE_UNKNOWN_CA;
Basic Cryptography
The QuadooScript package contains a module called QSCrypto.dll. While not a comprehensive cryptographic library, it does provide some useful functions for hashing and verifying signed messages.
CryptProtect and CryptUnprotect
One of the simpler pair of operations supported by the QuadooScript cryptographic module is "protecting" and "unprotecting" data. A binary data object can be "protected" (encrypted) and then later "unprotected" (decrypted) using Windows cryptography.
var oPassword = Host.CreateBinary("Entropy Password");
var oCrypto = Host.LoadQuadoo("QSCrypto.dll"), nFlags = 0;
var oEncrypted = oCrypto.CryptProtect(Host.CreateBinary("Hello, Crypto!"), "This is the data description!", oPassword, nFlags);
println("Protected: " + base64(oEncrypted));
var strDataDescription;
var oDecrypted = oCrypto.CryptUnprotect(oEncrypted, oPassword, nFlags, ref strDataDescription);
println("Message: " + oDecrypted.ReadUTF8());
println("Description: " + strDataDescription);
These protection routines are useful for storing sensitive data, such as passwords, in configuration settings. If the Win32 flag CRYPTPROTECT_LOCAL_MACHINE
(an integer of value 4) is passed through the nFlags
parameter, then the protected data can be retrieved by any user on the same computer that protected the data.
To further simplify the calls, the data description text could have instead been set to null
for CryptProtect()
and entirely omitted for the CryptUnprotect
call. The oPassword
parameter could also have been set to null
.
Signing and Verifying Signatures
The QuadooScript cryptographic module wraps several CryptoAPI functions, and the following sample (split into segments) demonstrates using them to sign and verify signatures. Private and public keys are also generated, exported, and imported.
The first block defines constants for the APIs and loads the module.
#define AT_KEYEXCHANGE 1
#define AT_SIGNATURE 2
#define RSA2048BIT_KEY 0x08000000
#define CRYPT_STRING_BASE64HEADER 0x00000000
#define CRYPT_EXPORTABLE 0x00000001
#define CRYPT_VERIFYCONTEXT 0xF0000000
#define X509_ASN_ENCODING 0x00000001
#define PKCS_7_ASN_ENCODING 0x00010000
#define PKCS_RSA_PRIVATE_KEY 43
#define SIMPLEBLOB 0x1
#define PUBLICKEYBLOB 0x6
#define PRIVATEKEYBLOB 0x7
#define PLAINTEXTKEYBLOB 0x8
#define OPAQUEKEYBLOB 0x9
#define PUBLICKEYBLOBEX 0xA
#define SYMMETRICWRAPKEYBLOB 0xB
#define PROV_RSA_FULL 1
#define PROV_RSA_AES 24
#define ALG_CLASS_ANY (0)
#define ALG_CLASS_SIGNATURE (1 << 13)
#define ALG_CLASS_MSG_ENCRYPT (2 << 13)
#define ALG_CLASS_DATA_ENCRYPT (3 << 13)
#define ALG_CLASS_HASH (4 << 13)
#define ALG_CLASS_KEY_EXCHANGE (5 << 13)
#define ALG_CLASS_ALL (7 << 13)
#define ALG_TYPE_ANY (0)
#define ALG_TYPE_DSS (1 << 9)
#define ALG_TYPE_RSA (2 << 9)
#define ALG_TYPE_BLOCK (3 << 9)
#define ALG_TYPE_STREAM (4 << 9)
#define ALG_TYPE_DH (5 << 9)
#define ALG_TYPE_SECURECHANNEL (6 << 9)
#define ALG_SID_MD2 1
#define ALG_SID_MD4 2
#define ALG_SID_MD5 3
#define ALG_SID_SHA 4
#define ALG_SID_SHA1 4
#define ALG_SID_MAC 5
#define ALG_SID_RIPEMD 6
#define ALG_SID_RIPEMD160 7
#define ALG_SID_SSL3SHAMD5 8
#define ALG_SID_HMAC 9
#define ALG_SID_TLS1PRF 10
#define ALG_SID_AES_256 16
// RC2 sub-ids
#define ALG_SID_RC2 2
// Stream cipher sub-ids
#define ALG_SID_RC4 1
#define ALG_SID_SEAL 2
#define CALG_MD2 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD2)
#define CALG_MD4 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD4)
#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5)
#define CALG_SHA (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA)
#define CALG_SHA1 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA1)
#define CALG_MAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MAC)
#define CALG_AES_256 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_256)
function main ()
{
var oCrypto = Host.LoadQuadoo("QSCrypto.dll"), oHash, oSignature, oExport, oContext;
The next segment creates a cryptographic context, generates a private key, hashes a file, signs the hash, and exports the public and private keys.
oContext = oCrypto.CreateContext(null, null, PROV_RSA_FULL, 0);
var oPrivate = oContext.GenKey(AT_KEYEXCHANGE, CRYPT_EXPORTABLE | RSA2048BIT_KEY);
oHash = oContext.CreateHash(CALG_SHA);
oHash.Add(Host.ReadBinaryFile("file.dat"));
oSignature = oHash.Sign();
println("Signature: " + base64(oSignature));
oExport = oContext.ExportPublicKey();
var strPublic = oCrypto.BinaryToString(oExport, CRYPT_STRING_BASE64HEADER);
println("Public Key: " + strPublic);
oExport = oPrivate.Export(PRIVATEKEYBLOB, 0);
var oEncoded = oCrypto.EncodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, PKCS_RSA_PRIVATE_KEY, oExport);
var strPrivate = oCrypto.BinaryToString(oEncoded, CRYPT_STRING_BASE64HEADER);
println("Private Key: " + strPrivate);
A common use case for cryptography is verifying a file's authenticity by checking its signed hash using the signer's public key.
oContext = oCrypto.CreateContext(null, null, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
var oPublic = oContext.ImportPublicKey(oCrypto.StringToBinary(strPublic, CRYPT_STRING_BASE64HEADER));
oHash = oContext.CreateHash(CALG_SHA);
oHash.Add(Host.ReadBinaryFile("file.dat"));
println("Verified: " + oHash.Verify(oSignature, oPublic));
The final segment of this sample also demonstrates loading the exported private key back into a new cryptographic context and using it to verify the file's signature.
oContext = oCrypto.CreateContext(null, null, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
var oDecoded = oCrypto.DecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, PKCS_RSA_PRIVATE_KEY, oEncoded);
oPrivate = oContext.ImportKey(oDecoded, CRYPT_EXPORTABLE);
oHash = oContext.CreateHash(CALG_SHA);
oHash.Add(Host.ReadBinaryFile("file.dat"));
println("Verified: " + oHash.Verify(oSignature, oPrivate));
}
Hashing Passwords and Deriving Keys
The previous sample generated a random key. In the following sample, a password hash is used to derive a cryptographic key, which is then used to encrypt and decrypt data.
var oCrypto = Host.LoadQuadoo("QSCrypto.dll");
var oContext = oCrypto.CreateContext(null, null, PROV_RSA_AES, 0);
var oHash = oContext.CreateHash(CALG_MD5);
oHash.Add("MyPassword");
println("Password Hash: " + oHash.GetHexKey());
var oKey = oContext.DeriveKey(CALG_AES_256, oHash, CRYPT_EXPORTABLE);
var oEncrypted = oKey.Encrypt(true, 0, Host.CreateBinary("This string will be encrypted!"));
println("Encrypted Data: " + oEncrypted.ToBase64());
var oDecrypted = oKey.Decrypt(true, 0, oEncrypted);
println("Decrypted Data: " + oDecrypted.ReadUTF8());
JSON Web Tokens
The following example demonstrates creating and validating JSON Web Tokens (JWTs).
function CreateJWT (oCrypto, strKey, strSubject, strUser)
{
var oHmac = oCrypto.CreateHmacSHA256();
var oHeader = JSONCreateObject(), oPayload = JSONCreateObject();
oHeader.alg = "HS256";
oHeader.typ = "JWT";
oPayload.sub = strSubject;
oPayload.name = strUser;
oPayload.iat = (nowutc().Value - 116444736000000000) / 10000000;
var strHeaderAndPayload = base64url((string)oHeader) + "." + base64url((string)oPayload);
oHmac.InitRfc2104(strKey);
oHmac.Add(strHeaderAndPayload);
return strHeaderAndPayload + "." + base64url(oHmac.GetDigest());
}
function ValidateJWT (oCrypto, strKey, strJWT, /* out */ refPayload)
{
var aParts = split(strJWT, ".");
if(len(aParts) == 3)
{
var oHmac = oCrypto.CreateHmacSHA256();
try
{
var strHeaderAndPayload = aParts[0] + "." + aParts[1];
var oHeader = JSONParse(Host.FileFromBase64Url("header.json", aParts[0]).ReadUTF8());
var oPayload = JSONParse(Host.FileFromBase64Url("payload.json", aParts[1]).ReadUTF8());
oHmac.InitRfc2104(strKey);
oHmac.Add(strHeaderAndPayload);
// Confirm that we're using HS256 and then verify the signature.
if(oHeader.alg == "HS256" && base64url(oHmac.GetDigest()) == aParts[2])
{
// Validated! Set the payload into the out parameter and return true.
refPayload = oPayload;
return true;
}
}
catch;
}
return false;
}
function TestJWT ()
{
var oCrypto = Host.LoadQuadoo("QSCrypto.dll");
var strKey = "my_secret_key";
var strJWT = CreateJWT(oCrypto, strKey, "website_token", "quadoo");
println("Token: " + strJWT);
var oPayload;
if(ValidateJWT(oCrypto, strKey, strJWT, ref oPayload))
{
println("Token validated!");
println("User: " + oPayload.name);
println("Issued: " + oPayload.iat);
}
else
println("Invalid token!");
}
This code could be adapted to create and validate JWTs in web-based environments.
The output from running TestJWT()
should look like this:
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ3ZWJzaXRlX3Rva2VuIiwibmFtZSI6InF1YWRvbyIsImlhdCI6IjIwMjEtMDMtMjhUMjI6NDk6MjUuODQzIn0.
-6N-UL-h9R-nggu1QohwX3l3yjsNZF9t8rvPfIsb3wI
Token validated!
User: quadoo
Issued: 2021-03-28T22:49:25.843
In the example, the Hmac-SHA256 (HS256) algorithm is used to sign and verify the JWT. More information about JWTs can be found here.
When making a request to an endpoint that expects a JWT, the token will probably be sent through the Authorization
header. For example:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ3ZWJzaXRlX3Rva2VuIiwibmFtZSI6InF1YWRvbyIsImlhdCI6IjIwMjEtMDMtMjhUMjI6NDk6MjUuODQzIn0.
-6N-UL-h9R-nggu1QohwX3l3yjsNZF9t8rvPfIsb3wI
Elliptic Curves
The QSCrypto.dll module can be used to perform basic operations with elliptic curves using the SECP256K1 algorithm.
var oEC = oCrypto.CreateElliptic();
var oKey = oEC.CreateKey("224877F96B66F4A114DDCE97085F5F1570EDF5EB1F1D7E6795673729A2E80B20");
println("Private Key Valid: " + oKey.Valid);
println("Private: " + oKey.Private.ToHex());
println("Public: " + oKey.Public.ToHex());
The elliptic curve can be used to sign 32 bytes of data, such as a 32-byte (256 bit) Hmac SHA256 hash.
var oHash = oCrypto.CreateHmacSHA256();
oHash.InitRfc2104("MYSECRETKEY");
oHash.Add("This is my document data.");
var oSig = oKey.Sign(oHash.GetDigest());
println("Signature: " + oSig.ToDER().ToHex());
To verify a signature, call the key's Verify()
method with the hash and the signature. If false is returned, then either the data or the signature has been modified.
println("Verify: " + oKey.Verify(oHash.GetDigest(), oSig.ToDER().ToHex()));
Only the public key is needed to verify a signature. The public key can be extracted from the private key. Signature verification using the public key works the same as it does using a full private/public key pair.
var oPublic = oEC.KeyFromPublic(oKey.Public.ToHex());
println("Verify: " + oPublic.Verify(oHash.GetDigest(), oSig.ToDER().ToHex()));
It is also valid to pass the signature in binary DER form to the Verify()
method.
println("Verify: " + oPublic.Verify(oHash.GetDigest(), oSig.ToDER()));
Generating Keys for Elliptic Curves
Keys for Elliptic Curves can be generated from 32-byte (256 bit) blocks of data. A script can generate these entirely by itself, or a script can generate 32 bytes of random data using the GenRandom()
method.
#define PROV_RSA_FULL 1
var oContext = oCrypto.CreateContext(null, null, PROV_RSA_FULL, 0);
var oRandom = oContext.GenRandom(32);
Not all random data can be used as a key for Elliptic Curve. There is a 1/2128 chance that the private key is invalid. Before using the key, it must be checked for validity.
if(oEC.VerifyKey(oRandom))
{
var oRandKey = oEC.CreateKey(oRandom);
println("Private Key: " + oRandKey.Private.ToHex());
var oRandPublic = oEC.KeyFromPublic(oRandKey.Public);
println("Public Key: " + oRandPublic.Public.ToHex());
}
ECDH Shared Keys
Given two private keys, a shared key can be generated using Elliptic Curve Diffie-Hellman (ECDH) by combining the other key's public key with your private key.
var oSharedA = oKeyA.ComputeShared(oKeyB.Public);
var oSharedB = oKeyB.ComputeShared(oKeyA.Public);
println("Shared A: " + oSharedA.ToHex());
println("Shared B: " + oSharedB.ToHex());
If oKeyA
and oKeyB
each hold a private key, then the shared ECDH key can be computed by combining its private key with the other key's public key. The resulting shared key can then be used for symmetric-key encryption.
One possibility for using an ECDH shared key would be to load it into an AES key for encryption and decryption.
#define MS_ENH_RSA_AES_PROV "Microsoft Enhanced RSA and AES Cryptographic Provider"
#define PROV_RSA_AES 24
var oContext = oCrypto.CreateContext(null, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, 0);
var oAES = oContext.ImportAesKey(oSharedA, 0);
var oEncrypted = oAES.Encrypt(true, 0, Host.CreateBinary("Secret message!"));
println("Encrypted: " + oEncrypted.ToHex());
var oDecrypted = oAES.Decrypt(true, 0, oEncrypted);
println("Decrypted: " + oDecrypted.ReadUTF8());
Encrypted: a98961fd6e383664460491f70c30ac5e
Decrypted: Secret message!
AES Encryptors/Decryptors
If AES is needed in an environment where the Windows cryptographic library has limited functionality, the reference AES can be used instead.
var oAES = oCrypto.CreateAESEncryptor(256, oSharedA.GetDigest());
var oEncrypted = oAES("This is my encrypted string!", true);
println(base64(oEncrypted));
oAES = oCrypto.CreateAESDecryptor(256, oSharedA.GetDigest());
var oDecrypted = oAES(oEncrypted, true);
println(oDecrypted.ReadUTF8());
The example above works because ECDH keys are 32 bytes, and 256-bit AES requires a 32 byte key. 192-bit AES requires a 24 byte key, and 128-bit AES requires a 16-bit key. An MD5 hash would be compatible with 128-bit AES.
Cryptographic Entropy
In some environments, such as the ASP environment, it may not be possible to use GenRandom()
to generate an Elliptic Curve key. In those situations, it may be possible to use the GetEntropy()
method instead, as it does not rely upon a cryptographic context object.
var oCrypto = Host.CreateObject("Simbey.QSCrypto");
var oSHA256 = oCrypto.CreateHashSHA256();
var oEC = oCrypto.CreateElliptic();
var oRandom;
do
{
oRandom = oSHA256.Add(oCrypto.GetEntropy()).GetDigest();
} while(!oEC.VerifyKey(oRandom));
var strKey = base64(oRandom);
This method of key generation may not be suitable for a production environment, but it may be sufficient for generating keys in a testing environment.
Blockchain Example
There is enough cryptography in the library to support several blockchain concepts. Click here to see the blockchain example script.
Bitcoin and Ethereum Addresses
Using QuadooScript's cryptographic library, Bitcoin and Ethereum addresses can be generated and formatted in script.
var oCrypto = Host.LoadQuadoo("QSCrypto.dll");
Using QuadooScript and its cryptographic library to format Bitcoin addresses:
function BuildBitcoinAddress (oCrypto, oBinKey, nVersionByte)
{
var oEC = oCrypto.CreateElliptic();
if(oEC.VerifyKey(oBinKey))
{
var oSHA256 = oCrypto.CreateHashSHA256();
var oRipeMd160 = oCrypto.CreateHashRIPEMD160();
var oKey = oEC.CreateKey(oBinKey);
var oHash = oSHA256.Add(oKey.Public).GetDigest();
oHash = oRipeMd160.Add(oHash).GetDigest();
var oVersioned = Host.CreateBinary(oHash.Size + 1);
oVersioned.WriteBinary(1, oHash);
oVersioned.Data[0] = nVersionByte;
oSHA256.Reset();
var oChecksum = oSHA256.Add(oVersioned).GetDigest();
oSHA256.Reset();
oChecksum = oSHA256.Add(oChecksum).GetDigest();
var oAddress = Host.CreateBinary(oVersioned.Size + 4);
oAddress.WriteBinary(0, oVersioned);
oAddress.WriteDWord(oAddress.Size - 4, oChecksum.ReadDWord(0));
return oCrypto.EncodeBase58(oAddress);
}
return null;
}
...
var oBinKey = Host.FileFromHex(null, "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a");
println(BuildBitcoinAddress(oCrypto, oBinKey, 0));
1JwSSubhmg6iPtRjtyqhUYYH7bZg3Lfy1T
Using QuadooScript and its cryptographic library to format Ethereum addresses:
function BuildEthereumAddress (oCrypto, oBinKey)
{
var oEC = oCrypto.CreateElliptic();
if(oEC.VerifyKey(oBinKey))
{
var oKeccak = oCrypto.CreateHashSHA3(256, true);
var oKey = oEC.CreateKey(oBinKey);
var oPublic = oKey.Public;
var oKey128 = Host.CreateBinary(oPublic.Size - 1);
oPublic.ReadBinary(1, oKey128.Size, oKey128);
var strHash = oKeccak.Add(oKey128).GetHexKey();
return "0x" + right(strHash, 40);
}
return null;
}
...
var oEtherKey = Host.FileFromHex(null, "7231bfb75a41481965e391fb6d4406b6c356d20194c5a88935151f05136d2f2e");
println(BuildEthereumAddress(oCrypto, oEtherKey));
0x8a2250aafb31638b19a83caa49d1ee61089dcb4b
Capturing From Webcam
The QuadooScript package contains a QSDS.DLL
module that allows scripts to read video frames from webcams. Internally, this is accomplished using DirectShow.
The first steps are to enable COM, load QSDS.DLL
, and enumerate and select the webcam (or other compatible video input device) for video capture.
Host.EnableCOM();
var oDS = Host.LoadQuadoo("QSDS.dll");
var oEnum = oDS.EnumVideoInput();
while(oEnum.Next())
{
println("Device: " + (string)oEnum);
println("Name: " + oEnum.FriendlyName);
}
Once the desired device has been located, then it can be opened for video capture. In this example, the MSE24()
method is used to compare two video frames using the Mean Squared Error algorithm.
println("Opening: " + oEnum.FriendlyName);
var oCaptureFilter = oEnum.Open();
var oFilterGraph = oDS.CreateFilterGraph();
var oCaptureGraph = oFilterGraph.AddFilter(oCaptureFilter, "Video Capture").CreateCaptureGraph();
var oStreamConfig;
try
oStreamConfig = oCaptureGraph.FindInterface("Preview", "Video", oCaptureFilter);
catch
oStreamConfig = oCaptureGraph.FindInterface("Capture", "Video", oCaptureFilter);
var oNullRenderer = oDS.CreateNullRenderer();
oFilterGraph.AddFilter(oNullRenderer, "Null Renderer");
var oSampleGrabber = oDS.CreateSampleGrabber();
oFilterGraph.AddFilter(oSampleGrabber.Filter, "Sample Grabber");
oSampleGrabber.SetOneShot(false);
oSampleGrabber.SetBufferSamples(false);
oSampleGrabber.SetMediaType("Video", "RGB24");
var oFrames = oDS.CreateVideoFrameCallback(oStreamConfig.Width * oStreamConfig.Height * 3);
oSampleGrabber.SetCallback(oFrames, 0);
// The video frames are copied into binary data objects.
var oFrameA = Host.CreateBinary(oFrames.FrameSize);
var oFrameB = Host.CreateBinary(oFrames.FrameSize);
println("Connecting filters...");
oCaptureGraph.RenderStream("Preview", "Video", oCaptureFilter, oSampleGrabber.Filter, oNullRenderer);
println("Beginning capture...");
var oMedia = oFilterGraph.GetMediaControl();
oMedia.Run();
var oKbdHit = Host.OpenStdInPipe().GetKBHit();
var cFrames = 0, msStart = timer();
while(!oKbdHit())
{
wait(oFrames, -1);
var dblTime = oFrames.CopyTo(oFrameA);
println("MSE: " + oDS.MSE24(oFrameA, oFrameB));
var oTemp = oFrameB;
oFrameB = oFrameA;
oFrameA = oTemp;
cFrames++;
}
oMedia.Stop();
var msDiff = timer() - msStart;
println("Time: " + msDiff + ", Frames: " + cFrames);
println("Per Frame: " + ((double)msDiff / (double)cFrames));
DisconnectPins(oFilterGraph, oCaptureFilter);
DestroyGraph(oFilterGraph);
The calls to DisconnectPins()
and DestroyGraph()
are needed to release circular references between the internal objects.
#define PINDIR_INPUT 0
function DisconnectPins (oGraph, oFilter)
{
var aPins = oFilter.GetPins();
for(int i = 0; i < len(aPins); i++)
{
var oPin = aPins[i];
try
{
var oConnectedTo = oPin.ConnectedTo();
if(oConnectedTo.Direction == PINDIR_INPUT)
{
var oConnectedFilter = oConnectedTo.Filter;
DisconnectPins(oGraph, oConnectedFilter);
oGraph.Disconnect(oConnectedTo);
oGraph.Disconnect(oPin);
oGraph.RemoveFilter(oConnectedFilter);
}
}
catch
{
println("Pin \"" + oPin.Name + "\" is not connected!");
}
}
}
function DestroyGraph (oGraph)
{
var aFilters = oGraph.GetFilters();
for(int i = 0; i < len(aFilters); i++)
{
var oFilter = aFilters[i];
println("Removing Filter: " + oFilter.Name);
oGraph.RemoveFilter(oFilter);
}
}