QuadooScript

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

Download QuadooScript for Windows!

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.

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.

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 do double
else enum extern false fiber
float for function get goto
if int long map money
namespace new null 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, like in control flow statements.  Function bodies are also blocks but can also be 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;

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

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.

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.

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 that is in the same scope or in a parent scope.  The script cannot jump into child scopes using the goto keyword.

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:

int a, b, c; long l; float r; double d; money m; string s;

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;

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.

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

try { throw 123; } catch(e) { Host.WriteLn("Caught exception: " + e.Value); }

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.

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 converted directly in and out of JSON objects and JSON arrays.

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:

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.

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 clear 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);

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 FindInstance (RSTRING rstrProgramName, __deref_out IUnknown** ppunkCustomData) = 0; virtual HRESULT AddInstance (RSTRING rstrProgramName, IUnknown* punkCustomData, ISequentialStream* pstmProgram, DWORD cbProgram) = 0; virtual HRESULT LoadVM (RSTRING rstrProgramName, ULONG cbAllocStack, __deref_out IQuadooVM** ppVM) = 0; virtual HRESULT RemoveInstance (RSTRING rstrProgramName) = 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 AddGlobal (RSTRING rstrName, IQuadooObject* pObject) = 0; virtual HRESULT FindGlobal (RSTRING rstrName, __deref_out IQuadooObject** ppObject) = 0; virtual HRESULT RemoveGlobal (RSTRING rstrName, __deref_opt_out IQuadooObject** ppObject) = 0; virtual HRESULT RunConstructor (__deref_out IQuadooObject** ppException) = 0; virtual HRESULT RegisterDestructor (IQuadooObject* pObject, DWORD idxDestructor) = 0; virtual HRESULT PushValue (QuadooVM::QVARIANT* pqvValue) = 0; virtual HRESULT FindFunction (PCWSTR pcwzFunction, __out ULONG* pidxFunction, __out DWORD* pcParams) = 0; virtual HRESULT RunFunction (ULONG idxFunction, __out QuadooVM::QVARIANT* pqvResult) = 0; virtual HRESULT Throw (QuadooVM::QVARIANT* pqvValue) = 0; virtual HRESULT Resume (__in_opt QuadooVM::QVARIANT* pqvValue, __out_opt QuadooVM::QVARIANT* pqvResult) = 0; virtual HRESULT Unload (VOID) = 0; virtual QuadooVM::State GetState (VOID) = 0; virtual VOID SetExternalScriptSite (__in_opt IExternalScriptSite* pSite) = 0; virtual HRESULT AddGlobalFunction (RSTRING rstrMethod, IQuadooFunction* pFunction) = 0; virtual HRESULT RemoveGlobalFunction (RSTRING rstrMethod) = 0; virtual HRESULT SetSysCallTarget (__in_opt IQuadooSysCallTarget* pTarget) = 0; virtual HRESULT SetPrintTarget (__in_opt IQuadooPrintTarget* pTarget) = 0; virtual HRESULT End (VOID) = 0; virtual HRESULT ThrowAndResume (QuadooVM::QVARIANT* pqvValue, __out_opt QuadooVM::QVARIANT* pqvResult) = 0; virtual VOID EnableCodeStepping (bool fStepping) = 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.

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.

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.

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.

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.

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.

WebSockets

In addition to serving ASP requests from IIS, ActiveQuadoo.dll can also handle WebSocket requests using QuadooScript.

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>

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:

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:

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, or a binary object

If the response is binary data, then the ToBinary() method should be used.

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.

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().ToISO8601(); 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.

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!

Blockchain Example

There is enough cryptography in the library to support several blockchain concepts.  Click here to see the blockchain example script.