QuadooScript

Quadoo was the best cat I ever had.  He is missed!

Download QuadooScript for Windows!

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...

Contents

Getting Started

QuadooScript can be used in five different ways:

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:

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:

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:

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

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:

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

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.

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:

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:

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:

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 print 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:

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:

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:

The binary object has two read/write properties:

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:

The following properties are available:

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:

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:

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:

Map

Maps are a native variable type that have the following methods:

Mutex

Mutex objects have the following methods:

Mutex objects also have one read/write property: Owned.

StringBuilder

StringBuilder objects have the following methods:

The following properties are available:

StringList

StringList objects have the following methods:

The following properties are available:

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:

JSON objects support the following methods:

JSON arrays support the following methods:

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:

The Host object also supports these properties:

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:

These properties are available:

Folder Object

These methods are available:

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.

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:

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 Object

The Host.Windows property has these methods:

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:

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:

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!&nbsp; 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:

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.

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:

  1. ASP's Response.Buffer property is read to determine whether buffering is enabled.
  2. The large binary data is fragmented into 2MB chunks to be sent separately to Response.BinaryWrite().
  3. The 2MB SAFEARRAY is allocated once and reused until the remaining data is smaller than 2MB.
  4. If buffering is enabled, and if there is still data remaining to be sent, then Response.Flush() is called.
  5. 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.

  1. The form data will be collected and attached to session variable TransferForm.
  2. The server variable SCRIPT_NAME will be set as session variable ReturnPage.
  3. 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:

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.

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:

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:

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.

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:

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.

Script or QBC Icon Command Line Arguments CompileQuadoo.exe QVMT.BIN Target Executable

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:

  1. strVerb
  2. strResource
  3. strReferrer
  4. vAcceptTypes - Can be null, a string, or an array of strings
  5. nFlags
  6. strHeaders - Can be null or a string
  7. 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); } }