Introduction

"The difficulty is that things almost always start with some guy doing something that at the time looks totally useless" - James Burke

wrench solves the problem of needing an easy-to-understand scripting language that can fit into a very small space but retain full power, flexibility, and speed.

  • Embedable: Comfortably fits into 32k of ROM, and runs with a ~1k RAM footprint
  • Comprehensible: c-like syntax, weakly typed

  • Fast: as a comparison: as fast or faster than lua (non-JIT of course), much faster than pythgo

  • Compact: Bytecode images a small fraction of other interpreters

  • Easy To Integrate:
    • two source files: wrench.cpp and wrench.h
    • architecture/endian neutral, compile anywhere run anywhere else
    • c++98 clean and compliant, nothing fancy.
    • no third-party libs, everything you need is right here
  • Debuggable!: full remote debugger included, run/step/inspect your code over a serial port.

  • Extend: the whole idea is safe, low overhead calls to and from your main program.

  • Cooperative Multi Threaded: Scheduler included.

  • Tested: A full test-suite must pass valgrind with zero warnings/errors/leaks on multiple architectures (including cross-compatibility checks with big-endian via qemu)

Short Version: I didn't need a whole workshop with all the bells and whistles. I just needed a wrench. So I built one.


Integration

Wrench [the project] is kept current on GitHub, download it here.

Now the easy part, wrench packages itself into two source files: I have to credit Wren for this brilliant idea (at least that's where I saw it).

add:

/src/wrench.h
/src/wrench.cpp
to your project and compile. Of course if you want a more approachable version, the traditional layout is included in discrete_src/

example

#include "wrench.h"
#include <string.h>
#include <stdio.h>

void print( WRContext* c, const WRValue* argv, const int argn, WRValue& retVal, void* usr )
{
	char buf[128];
	for( int i=0; i<argn; ++i )
	{
		printf( "%s", argv[i].asString(buf, 128) );
	}
}

const char* wrenchCode = 
"print( \"Hello World!\\n\" );"
"for( var i=0; i<10; i++ )  "
"{                            "
"    print( i );              "
"}                            "
"print(\"\\n\");              ";


int main( int argn, char** argv )
{
	WRState* w = wr_newState(); // create the state

	wr_registerFunction( w, "print", print ); // bind a function

	unsigned char* outBytes; // compiled code is alloc'ed
	int outLen;

	int err = wr_compile( wrenchCode, strlen(wrenchCode), &outBytes, &outLen ); // compile it
	if ( err == 0 )
	{
		wr_run( w, outBytes, outLen, true ); // load and run the code! (pass ownership)
	}

	wr_destroyState( w );

	return 0;
}

command line

A command-line utility is included, to compile it under linux just make in the root dir. For windows a visual studio project is included under /win32

For PC-ish stuff this is all you have to do, the full-blown wrench compiler and interpreter are fine as they are, but for an embedded target there are a few slight changes you might want to make:

In src/wrench.h you might want to tweak

build flags

#define WRENCH_WITHOUT_COMPILER

Build wrench without it's compiler. this will minimize the code size at the cost of being able to only load/run bytecode. For embedded projects the command line tool actually compiles to c header .h code for super-easy addition to the source.

#define WRENCH_COMPACT
#define WRENCH_REALLY_COMPACT
#define WRENCH_INCLUDE_DEBUG_CODE

This causes wrench to compile into the smallest program size possible at the cost of some interpreter speed (due to the removal of unrolled loops, cache optimizations, and additional 'shared' code with some goto spaghetti)

REALLY_COMPACT switches the main message pump from computed-goto to a giant switch. This saves a few K of ram but is a constant performance hit so only use it if absolutely necessary

WRENCH_INCLUDE_DEBUG_CODE costs about 1k, without it debug-enabled code will run just fine but not trigger any functionality.

#define WRENCH_DEFAULT_STACK_SIZE 64

by default wrench allocates a static stack and does not bounds-check it, this is done for speed an simplicity. The stack is used only for function calls and local data so it need not be large, the default should be more than enough.

Stack checking can be compiled in with

#define WRENCH_PROTECT_STACK_FROM_OVERFLOW
Warning: this adds a small if() check to many opcodes, so there is a small overhead to using it.

Each stack entry consumes 8 bytes, so embedded devices that have very limited ram (like the Uno Mini) might want to reduce this. The arduino example provided has a stack of 32, which should be plenty to run even a pretty intricate script.

#define WRENCH_FLOAT_SPRINTF

wrench is kept tight and small, if you want full floating point sprintf-style formatting from str::sprintf then define this. Not a huge amount of code but unecessary for most embedded aplications

//#define WRENCH_WIN32_FILE_IO
//#define WRENCH_LINUX_FILE_IO
//#define WRENCH_SPIFFS_FILE_IO
//#define WRENCH_LITTLEFS_FILE_IO
//#define WRENCH_CUSTOM_FILE_IO

For architectural-specific file/system operations wrench provides the io:: lib (see std_io_defs.h and std_io.cpp for an overview of the provided functionality. This is not fully operational, but the calls will work if you need them.

//#define WRENCH_HANDLE_MALLOC_FAIL

for embedded systems that need to know if they have run out of memory,

WARNING: This imposes a small if() check on many opcodes, so the malloc failure is detected the moment it happens. This guarantees graceful exit if g_malloc() ever returns null

//#define READ_32_FROM_PC( P )
//#define READ_16_FROM_PC( P )
//#define READ_8_FROM_PC( P )
These three macros are defined automatically inside wrench to read bytes from the code stream, if the Endian-ness of the architectures are all little then they default to simple dereference:
#define READ_32_FROM_PC(P) *(uint32_t *)(P)
#define READ_16_FROM_PC(P) *(uint16_t *)(P)
#define READ_8_FROM_PC(P) *(uint8_t *)(P)
Some embedded devices might need special pre-compiler directives to read from their EPROM, so this is left to you to define if needed.

To add functions to extend wrench, as well as calling into it are dead simple and super low overhead. Some examples are provided, but frankly if you actually got this far and are interested, the code in wrench.h is very clear and well commented, and there are quite a few examples.

Creating data to pass to wrench

globals

globals are statically allocated in wrench, so can be accessed from c via fixed pointers using:

WRValue* wr_getGlobalRef( WRContext* context, const char* label );

A helper class is provided to help with read/write access to the global values

class WrenchValue
{
public:
    WrenchValue( WRContext* context, const char* label )
    WrenchValue( WRContext* context, WRValue* value )

    bool isValid();

    // if the value is not the correct type it will be converted to
    // that type, preserving the value as best it can
    int* Int();
    float* Float();
    WRValue& operator[] ( const int index ) { return *asArrayMember( index ); }
    WRValue* asArrayMember( const int index );
    int arraySize();
};

outside data

Data can be created outside of wrench, this includes containers, strings and values and such

Creating simple values allocates no memory and requires no cleanup:

void wr_makeInt( WRValue* val, int i );
void wr_makeFloat( WRValue* val, float f );

the WRValue itself also has helpful members for setting/getting and accessing the underlying data:

int asInt() const;
void setInt( const int val );
	
float asFloat() const;
void setFloat( const float val );

bool isFloat() const
bool isInt() const
bool isString( int* len =0 ) const;
bool isWrenchArray( int* len =0 ) const;
bool isRawArray( int* len =0 ) const;
bool isHashTable( int* members=0 ) const;

// if this value is an array, return [or create] the 'index'-th element
// if create is true and this value is NOT an array, it will be converted into one
WRValue* indexArray( WRContext* context, const uint32_t index, const bool create );
	
// if this value is a hash table, return [or create] the 'index' hash item
// if create is true and this value is NOT a hash, it will be converted into one
WRValue* indexHash( WRContext* context, const uint32_t hash, const bool create );
	
// string: must point to a buffer long enough to contain at least len bytes.
// the pointer will be passed back
char* asString( char* string, size_t maxLen =0 ) const;

// malloc a string of sufficient size and copy/format the contents
// of this value into it, the string muyst be g_free'ed
char* asMallocString( unsigned int* strLen =0 ) const;
class MallocStrScoped // helper class for asMallocString()
{
public:
    operator bool()        const { return m_str != 0; }
    operator const char*() const { return m_str; }
    unsigned int size()    const { return m_size; }
    MallocStrScoped( WRValue const& V ) : m_size(0), m_str(V.asMallocString(&m_size)) {}
    ~MallocStrScoped() { g_free((char*)m_str); }
private:
    unsigned int m_size;
    const char* m_str;
};

// same as "asString" but will print it in a more debug-symbol-y
char* technicalAsString( char* string, size_t maxLen, bool valuesInHex =false ) const;

// return a raw pointer to the raw data array if this is one, otherwise
// return null
void* array( unsigned int* len =0, char arrayType =SV_CHAR ) const;
int arraySize() const; // returns length of the array or -1 if this value is not an array

uint32_t getHash() const // returns a hash of this value

iterating arrays and hashes

WRValue also has standard iterators defined, so for the case of an array or hash, this will iterate the members:

struct WRIteratorEntry
{
    int type; // SV_VALUE, SV_CHAR or SV_HASH_TABLE

    const WRValue* key; // if this is a hash table, the key value
    const WRValue* value; // for SV_HASH_TABLE and SV_VALUE (character will be null)

    int index; // array entry for non-hash tables
    char character; // for SV_CHAR
};


// requires c++11 and above, which supports foreach() syntax:
for( WRIteratorEntry const& member : *argv )
{
    // member provided here, as defined above
}

// for c++98:
for ( WRValue::Iterator it = value->begin(); it != value->end(); ++it )
{
    WRIteratorEntry const& member = *it;

    // member is always valid here
}

strings and memory

a string has to exist in a context so it can be worked with

WRValue& wr_makeString( WRContext* context, WRValue* val, const char* data, const int len =0 );
Turning a value into a container allocates a hash table which must be released with destroy!
void wr_makeContainer( WRValue* val, const uint16_t sizeHint =0 );
void wr_destroyContainer( WRValue* val );
void wr_addValueToContainer( WRValue* container, const char* name, WRValue* value );
void wr_addIntToContainer( WRValue* container, const char* name, const int32_t value );
void wr_addFloatToContainer( WRValue* container, const char* name, const float value );
void wr_addArrayToContainer( WRValue* container, const char* name, char* array, const uint32_t size );

Example (as seen in wrench_cli.cpp test code):

WRValue container;
wr_makeContainer( &container );

WRValue integer;
wr_makeInt( &integer, 0 );
wr_addValueToContainer( &container, "integer", &integer );

char someArray[10] = "hello";
wr_addArrayToContainer( &container, "name", someArray, 10 );

char* someBigArray = new char[0x1FFFFF];
someBigArray[0] = 10;
someBigArray[10000] = 20;
someBigArray[100000] = 30;
someBigArray[0x1FFFFE] = 40;

char byte = (char)0x99;
char byte2 = (char)0x99;
wr_addArrayToContainer( &container, "b", &byte, 1 );
wr_addArrayToContainer( &container, "c", &byte2, 1 );
		
wr_addArrayToContainer( &container, "big", someBigArray, 0x1FFFFF );

// at this point 'container' can be used inside wrench as if it were a
// struct, refer to tests/008_userData.c for complete usage

.
.
.

   // create a state and get a calling context
   WRState* w = wr_newState( 128 );
   WRContext* context = wr_run( w, somScript, someScriptLen );

   unsigned char testString[12] = "test string";  // create a string
   WRValue val;
   wr_makeString( context, &val, testString, 11 ); // this allocates structures inside context!
   wr_callFunction( context, "stringCheck", &val, 1 ); // call the function 

   // NOTE: we must know that stringCheck did not store the value
   // locally or call a function that did, otherwise freeing
   // it here could segfault when wrench is called again in the future
   // (and tries to work with it).
   // Since we're not using 'w' again this is 100% safe, we're not giving wrench a chance
   // to behave badly :)

   wr_freeString( &val );
   wr_destroyState( w );

Language Reference

Everything is a Unit!

function? : unit than is called and returns a value!

struct? : unit with local variables, they are the members!

constructors? : a unit that is called and its return value discarded

classes? : yeah.. okay wrench doesn't have member functions sorry, but I have some ideas here so stay tuned

Don't let this scare you! If you completely ignore the whole "unit" thing you'll be fine, wrench is intuitive and c-like, the syntax should be very familiar.

variables

to create a variable use the var directive (NOTE: in previous version of wrench this was optional, to support that behavior a "non strict" flag can be used but is discouraged)

var a = 10;
var b = 3.4;
var string = "some string";
wrench natively handles 32 bit ints, floats and 8 bit character strings. Variable names follow c-syntax for legality, the must start with a letter or '_' and can contain letters, numbers and more '_' characters.

operators

all of these are supported, with their c-defined precedence:

//binary:
a + b;
a - b;
a / b;
a * b;
a | b; // or
a & b; // and
a ^ b; // xor
a % b; // mod
a >> b; // right-shift
a << b; // left-shift

a += b;
a -= b;
a /= b;
a *= b;
a |= b;
a &= b;
a ^= b;
a %= b;
a >>= b;
a <<= b;

// pre and post:
a++;
a--
++a;
--a;

// as well as the c logical operators:
a == b
a != b
a >= b
a <= b
a || b
a && b

comments

var A = 10; // single-line c++ comments are supported
/*
     as well as block-comment style
*/

yielding

wrench can yield and then be continued, in code the "yield()" call causes the VM to return in a yielded state. the argument passed to yield is available to the caller using bool wr_getYieldInfo( WRContext* context, int* args =0, WRValue** firstArg =0, WRValue** returnValue =0 ); which will return true if the context is yielded, and optionally the argument passed. this also provides the stack location where a return value is expected to the code (default 0)

Later WRValue* wr_continue( WRContext* context ); can be called to resume where the code left off. Additionally if any callFunction(...) is called on the context it will detect that it is yielded, and ignore parameters in favor of continuing where it left off.

An example of this functionality is provided in full under /examples/multi_context.cpp

arrays

arrays are zero-based (duh) and can be declared with [] syntax, and can contain any legal type

var arrayOne[] = { 0, 1, 2 };
print( arrayOne[1] ); // will print "1"

var arrayTwo[] = { "zero", 1, 3.55 };

var blankArrayOfTen[10];

for

follows the standard c syntax, allowing a variable to be declared in the for convenience:

var i = 0;
for( i=0; i<5; i++ )
{
    // will loop 5 times
}

for( var a=0; a<10; ++a )
{
    // will loop 10 times
}

foreach

wrench also supports "foreach" in two flavors, value only and key/value:

someArray[] = {"zero", "one", "two" };
for( var v : someArray )
{
   // this loop will run 3 times, with v taking on "zero", "one" and "two"
}

for( var k, var v : someArray )
{
   // same as above but k will take on the value 0, 1 and 2
}

while

while( condition )
{
}

switch

switch works the same as c, there is an optimized code path for a list of cases (including default) that are between 0 and 254. wrench also supports fall-through.

switch( expression )
{
    case 0:
    case 1:
        break;
    defalt:
        break;
}

do/while

do
{
} while( condition );

break/continue

inside any looping structure (do/while/for) continue and break function as they do in c

if/else

work exactly the same as c:

if( a == true )
{
}
else if ( b == true ) // or whatever
{
}
else
{
}

function

Functions can be called with any number of arguments, extra arguments are ignored, un-specified arguments are set to zero (0)

function f( arg )
{
   if ( arg > 10 )
   {
	  return true;
   }
   else
   {
	  return false;
   }
}

var first = f(20); // first will be 'true' or '1'
var second = f();  // second will be 'false' because 'arg' was not
                   // specified, so set to 0

If a variable is declared in a function, it will be local unless a global version is encountered first. Global scope can be forced with the '::' operator:

var g = 20;
var n = 30;

function foo()
{
    var n = 2;  // local 'n' is 2
    g = 30;     // the global 'g'
    ::n = 40;   // global 'n' was 30, will now be 40
    print( n ); // will print '2'
}

foo();

struct

In wrench structs are actually functions that preserve their stack frames.

Another way to put it is structs are "called" so they are their own constructors, and all the variables they declare are preserved:

struct S
{
   member1;
   member2;
};

var s = new S(); // s will be a struct with two uninitialized members (member1 and member2) 

// members are dereferenced with '.' notation:
s.member1 = 20;
s.member2 = 50 + s.member1;
// s.member2 is now 70
A more complete example:
struct S(arg1)
{
   var member1 = arg1;
   if ( arg1 > 20 )
   {
	  member2 = 0;
   }
   else
   {
	  member2 = 555;
   }
}

instance = new S(40); // s.member1 will be 40, s.member2 will be 0

Structs can also be initialized when created:

struct S
{
    var a;
    var b;
    var c;
}

var bill = new S()
{
   a = 20,
   c = "some string",

// b will be initted to zero

};

// init by order (not recommended!)
var bill2 = new S()
{
    20,  // a will be 20
    30,  // b will be 30

    // c will be zero
};

struct arg( A, B )
{
    var first = A;
    var second = B;
}
var argNew = new arg( 10, 20 ); // first will be 10, second will be 20

export

Structs can be exported so other scripts can import them, for example

script 1:

export struct Color
{
    var red;
    var green;
    var blue;
}

script 2:

var colorByteCode = io::readFile("precompiled_color_script.bin");
sys::importByteCode( colorByteCode );

var myRed = new Color() { red = 0xFF, blue = 0, green = 0 };

// ... etc

constants some constants that are compiler-defined:

true == 1
false == 0
null == 0

enums

enums are syntactic sugar, when invoked they introduce variables into the namespace with automatic initliazation

enum
{
	n0,
	n1,
	n2
}

// equivilant to:
var n0 = 0;
var n1 = 1;
var n2 = 2;

hash tables

wrench uses hash tables internally so this language feature kind of comes along "for free". Any valid value can be used as a key or value

hashTable = { 1:"one", 2:"two", 3:3, "str":6 };
print( hashTable[1] ); // "one"
print( hashTable[2] ); // "two"
print( hashTable[3] ); // 3
print( hashTable["str"] ); // 6

// and this also works (syntactic sugar for string-keys only)
print( hashTable.str ); // 6

compiler-intrinsics Some compiler directives are included for working with arrays and hash tables:

hashTable = { 1:"one", 2:"two", 3:3, "str":6 };

//  ._count
print( hashTable._count ); // prints 4

// ._exists
hashTable._exists( 2 ); // returns 'true'
hashTable._exists( 20 ); // returns 'false

// ._remove
hashTable._exists( 2 ); // returns 'true'
hashTable._remove( 2 );
hashTable._exists( 2 ); // now false

casting/coersion

it is often handy to force wrench to convert a float to an int or vice-versa, for example:

var divisor = 1000;
var result = 10 / divisor; // the result of this is '0' since divisor is an int

result = 10 / (float)divisor; // now result will be 0.01

// NOTE: divisor will also NOT be converted to a float.
//       to accomplish that:
divisor = (float)divisor;

3.0 Features


scheduler

wrench has a built in time-slicer which will force the VM to yield after a certain number of branch/jump instructions are executed. This was chosen becuase any program that runs continuously must loop, so it is far more efficient to only check them, and not waste cycles.

To enable the time-slicer, define:

#define WRENCH_TIME_SLICES
This grants access to
void wr_setInstructionsPerSlice( int instructions );
void wr_forceYield();  // for the VM to yield right NOW, (called from a different thread)
extern int g_sliceInstructionCount; // how many instructions were left when the current slice yielded
wr_setInstructionsPerSlice(...) tells VM how many branch/jumps to allow before forcing a yield and returning.

The return will have 'null' WRValue* with it's context in a yielded state such that wr_getYieldInfo(...) will return true but importantly: *returnValue will be NULL since a force-yield is not entitled to a return value.

wr_forceYield() allows an external thread to force the VM to yield immeditely on its next branch/loop instruction, for a multi-threaded system that wants to implement a pre-emptive scheduler.

A simple round-robin scheduler is included:

class WrenchScheduler
{
public:
    WrenchScheduler( const int stackSizePerThread = WRENCH_DEFAULT_STACK_SIZE );
    ~WrenchScheduler();

    WRState* state() const { return m_w; }

    void tick( const int instructionsPerSlice =1000 );

    // returns a task ID
    int addThread( const uint8_t* byteCode, const int size, const int instructionsThisSlice =1000, const bool takeOwnership =false );
    bool removeTask( const int taskId );
};
This sample implementation can be found in wrench_cli.cpp:
    const char* loop1 = "for(;;) { println(\"1\"); }";
    const char* loop2 = "for(;;) { println(\"2\"); }";
    const char* loop3 = "println(\"once\");";
    const char* loop4 = "for(;;) { println(\"4\"); }";
    const char* loop5 = "for(;;) { println(\"5\"); }";

    WrenchScheduler scheduler( 8 );

    WRstr logger;
    wr_registerFunction( scheduler.state(), "println", emitln, &logger );

    uint8_t* out;
    int outLen;

    wr_compile( loop1, strlen(loop1), &out, &outLen );
    scheduler.addThread( out, outLen, 10, true );
    scheduler.tick(10);

    wr_compile( loop2, strlen(loop2), &out, &outLen );
    scheduler.addThread( out, outLen, 10, true );
    scheduler.tick(10);

    wr_compile( loop3, strlen(loop3), &out, &outLen );
    scheduler.addThread( out, outLen, 10, true );
    scheduler.tick(10);

    wr_compile( loop4, strlen(loop4), &out, &outLen );
    scheduler.addThread( out, outLen, 10, true );
    scheduler.tick(10);

    wr_compile( loop5, strlen(loop5), &out, &outLen );
    scheduler.addThread( out, outLen, 10, true );
    scheduler.tick(10);

    printf( "%s\n", logger.c_str() );

NOTE: each task gets its own stack, defined by the size used to create the WRState (default 64)


stack protection

wrench uses the stack sparingly for storing function locals, return vectors and temp space for long calculations. Unless the script uses a lot of recursion or a lot of locals, a modest stack of even 20 or 30 entries is more than enough. The default of 64 consumes only 256 bytes of RAM.

For this reason the stack is not normally checked for overflow, since it would be a waste of cycles.

If this protection is desired, define

#define WRENCH_PROTECT_STACK_FROM_OVERFLOW
in wrench.h which will keep an eye on the stack and not allow overflow.

If detected, the vm returns null with err set to: WR_ERR_stack_overflow


Debugger

The remote debugger allowing step/inspect is a work in progress, coming soon!

Extending Wrench

There are three ways wrench interacts with "native" c/c++ code:

Callbacks

A callback appears inside wrench as an ordinary function, they take arguments and return a value:
retval = myFunction( 25 );
in order to receive the "myFunction(...)" callback the c program needs to register the callback with

void wr_registerFunction( WRState* w, const char* name, WR_C_CALLBACK function, void* usr )

w state to be installed in
name name the function will appear as inside wrench
function pointer to callback function (see below)
usr opaque pointer that will be passed when the function is called (may be null)
void myFunction( WRContext* c, const WRValue* argv, const int argn, WRValue& retVal, void* usr )
{
    // do something
}

void main()
{
    WRState* w = wr_newState( 128 );
    wr_registerFunction( w, "myFunction", myFunction, 0 );

    // and then run wrench
}
Every time myFunction() is called from wrench the external c function will be called.

void myFunction( WRContext* c, const WRValue* argv, const int argn, WRValue& retVal, void* usr )

WRContext* c context the call is from
const WRValue* argv a list of zero or more WRValue that are the arguments the function was called with
const int argn the number of arguments passed in argv
WRValue& retVal this value is returned to the caller (default integer zero)
void* usr value passed when the function was registered
The arguments passed are directly from the wrench stack for speed, because of this their values should never be accessed directly, but with the built-in accessors:

asInt();
asFloat();
asString(...);
array(...);

For safety the return value should use the value constructors

void wr_makeInt( WRValue* val, int i );
void wr_makeFloat( WRValue* val, float f );

Also since functions in wrench can be called with any number of arguments (including zero) that should be checked for safety, as in:

void openDoor( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr )
{
    if ( argn != 2 )
    {
        // log an error or something
        return;
    }

    const char* name = argv[0].c_str();
    if ( !name )
    {
        // was not passed a string!
        return;
    }
	
    int door = argv[1].asInt(); 

    OpenDoor( name, door ); // some function to do the work

    wr_makeInt( &retVal, 1 ); // return a '1' indicating success
}

Library Callbacks

The good news is these are very similair to regular callbacks. The function signature is a bit different, though, to facilitate very fast calls, minimizing the work wrench has to do.

The assumption here is that if you're writing library calls then you are likely familiar and comfortable with some of the wrench internals and don't mind looking at examples and source code

There are many examples of library calls in std_math.c, std_string.c, and std_io.c.
library functions are registered with a different function, and their names must conform to the "x::y" format for them to be recognized by wrench code:

void wr_registerLibraryFunction( WRState* w, const char* signature, WR_LIB_CALLBACK function );

WRState* w Pointer to the state that made the call
const char* signature "x::y" formatted lib call name
WR_LIB_CALLBACK function c-function to callback

The WR_LIB_CALLBACK looks like this:

void libFunc( WRValue* stackTop, const int argn, WRContext* c )

The idea is to get in and out of a library call as fast as possible, so yeah, you gotta know how to use it.

If any arguments were passed, they are below the stack pointer, examples:

argn = 1:
stackTop[-1].asInt(); // or .asFloat() or whatever

argn = 2:
stackTop[-2].asInt(); // first argument
stackTop[-1].asInt(); // second argument

It might be easier to think about it this way:

WRValue* args = stackTop - argn;

args[0].asInt(); // first
args[1].asInt(); // second
args[2].asInt(); // third
args[3].asInt(); // fourth

The return value is quite a bit easier to explain, it's stackTop itself, so for example returning 5.4:

wr_makeFloat( stackTop, 5.4f );

NOTE: The return value is defaulted to integer-0 so it is safe to install an integer value directly in the interest of speed, ie:

stackTop->i = returnValue; // we're all friends here
stackTop[0].i = returnValue; // exactly the same as the above code

The WRContext value is provided to save a dereference on the wrench side for a rarely used (but necessary!) parameter; the WRState* value contained inside it if necessary as

c->w


Calls

once wrench code is run for the first time with wr_run() the state is preserved and can be re-entered using the various forms of wr_callFunction:

WRValue* wr_callFunction( WRContext* context, const char* functionName, const WRValue* argv =0, const int argn =0 );
WRValue* wr_callFunction( WRContext* context, const int32_t hash, const WRValue* argv =0, const int argn =0 );
WRValue* wr_callFunction( WRContext* context, WRFunction* function, const WRValue* argv =0, const int argn =0 );
WRValue* wr_continue( WRContext* context ); // only for yielded contexts, see wr_getYieldInfo()
bool wr_getYieldInfo( WRContext* context, int* args =0, WRValue** firstArg =0, WRValue** returnValue =0 );
: They all return a WRValue from the called function, this pointer will be NULL if the called function was not found.
given this simple script:
g_a = 20;
function wrenchFunction()
{
   g_a += 30;
   return g_a;
}

A program that would call "wrenchFunction()" might look like this:

WRState* w = wr_newState();
WRContext* context = wr_run( w, someByteCode ); // 'a' will be 20

WRValue* retval = wr_callFunction( context, "wrenchFunction" );
if ( !retval )
{
   // error!
}
else
{
   retval->asInt(); // this will return '50'
}

An array of arguments can be passed to the function:

a = 20;
function wrenchFunction( b, c )
{
    ::a += b * c;
}
WRState* w = wr_newState();
WRContext* context = wr_run( w, someByteCode ); // 'a' will be 20

WRValue values[2];
wr_makeInt( &value[0], 2 );
wr_makeInt( &value[1], 3 );
wr_callFunction( context, "wrenchFunction", values, 2 ); // 'a' will now be 26

An example returning an array, the wrench code might be:

function arrayCheck()
{
	newState = "some string";
	newStateDuration = 0.0;
	resetCollision = false;

	res[] = { newState, newStateDuration, resetCollision };

	return  res;
}
Accessed with this user-space program, which is acutally used as part of the test suite:
WRValue* V = wr_callFunction( context, "arrayCheck" );
if ( V )
{
	assert( V->isWrenchArray() );
	char someString[256];
	assert( WRstr(V->indexArray(context, 0, false)->asString(someString)) == "some string" );
	assert( V->indexArray(context, 1, false)->asFloat() == 0.0f );
	assert( V->indexArray(context, 2, false)->asInt() == 0);
	assert( !V->indexArray(context, 3, false)  );
	assert( V->indexArray(context, 3, true)->asInt() == 0  );
}

Library

library functions are provided as well. These functions are only available if loaded, which by default they are not. There is no overhead associated with having these calls resident other than a RAM cost.
void wr_loadSysLib( WRState* w ); // system/internal functions
void wr_loadMathLib( WRState* w ); // provides most of the calls in math.h
void wr_loadStdLib( WRState* w ); // standard functions like sprintf/rand/
void wr_loadIOLib( WRState* w ); // IO funcs (time/file/io)
void wr_loadStringLib( WRState* w ); // string functions
void wr_loadMessageLib( WRState* w ); // messaging between contexts
void wr_loadSerializeLib( WRState* w ); // serialize WRValues to and from binary

note: If space is not a concern then blast them all in with:

void wr_loadAllLibs( WRState* w )
// SysLib:
sys::isFunction( "funcName" ); // return true IFF funcName is a callable
                               // function, ie loaded: wr_registerFunction(...)

sys::halt( err ); // halts execution and sets w->err to whatever was passed                 
                  // NOTE: value must be between WR_USER and WR_ERR_LAST
sys::importByteCode( byteCode ); // byteCode as compiled code
sys::importCompile( sourceCode ); // compiles and imports source code
                                  // IF the compiler was included

// MathLib:
math::sin( f );
math::cos( f );
math::tan( f );
math::sinh( f );
math::cosh( f );
math::tanh( f );
math::asin( f );
math::acos( f );
math::atan( f );
math::atan2( x, y );
math::log( f );
math::ln( f );
math::log10( f );
math::exp( f );
math::pow( a, b );
math::fmod( a, b );
math::trunc( f );
math::sqrt( f );
math::ceil( f );
math::floor( f );
math::abs( f );
math::ldexp( a, b );
math::deg2rad( f );
math::rad2deg( f );

// Std:
std::rand( a [,b] ); // returns a psuedo-random number between a and b OR
                     // 0 to a if only a is provided
std::srand( seed );  // seed the rand()
std::time();         // return unix time on systems that support it

// Str:
str::strlen( s ); // return the length of s (s._count also works)
str::sprintf( str, fmt, ... ); // as c sprintf
str::printf( fmt, ... ); // as c printf
str::format( fmt, ... ); // same as printf but return the string
str::isspace( char ); // as c 
str::isdigit( char ); // as c
str::isalpha( char ); // as c
str::mid( str, start, len ); // return the middle of a string starting
                             // at 'start' for 'len' chars
str::chr( str, char ); // as c strchr; returns -1 if not found
str::tolower( str/char ); // convert and return this character or string to lowercase
str::toupper( str/char ); // convert and return this character or string to uppercase
str::concat( str1, str2, ... ); // return str1+str2+...
                                // NOTE: str1 + str2 also works!
str::left( str, len ); // return string from left side 0 to len 
str::trunc( str, len ); // [alist for left() see above]
str::right( str, len ); // return string from right side of len chars
str::substr( str, start, len); // [alias for mid() see above]
str::trimright( str ); // remove trailing whitespace
str::trimleft( str ); // remove leading whitespace
str::trim( str ); // remove leading and trailing whitespace
str::insert( str1, str2, pos ); // insert str2 into str1 at pos

// IO:
io::readFile( name ); // returns a WRValue char array representing the file
                      // data, default is binary, and CAN include null's
io::writeFile( name, data ); // writes 'data' to a file, expecting an array of data to write to "name"
io::deleteFile( name ); // unlink/delete this file
io::open( name, flags, mode ); // returns a file descriptor
                               // flags can be any OR of:
                               // io::O_RDONLY io::O_RDWR io::O_CREAT
                               // io::O_APPEND io::O_TRUNC io::EXCL
                               // mode is octal unix file mode: default 0666
io::close( fd ); // close fd returned by open
io::read( fd, max_count ); // read the FD into data array, up to
                           // returns read data as string (0's are ok)
                           // actual number of bytes read by str::strlen() or ret._count
io::write( fd, data, count );  // same as fileWrite() above but with
                               // the fd returned by open
io::seek( fd, offset, whence ); // set the read point of a fd returned
                                // by open() above
                                // whence value can be one of: io::SEEK_SET, io::SEEK_CUR, io::SEEK_END
                                // as c (default io::SEEK_CUR)
io::sync( fd ); // make sure all data is committed (flush)


io::getline(); // return a line of text input with fgetc(stdin)

// MessageLib:
// messages are global to all contexts currently running, this allows
// inter-context communication.
msg::read( key, [clear default:false] )  // read a message, optionally clear
msg::write( key, message )
msg::clear( key )
msg::peek( key ) // return true if the key has a message

// SerializeLib:
std::serialize( value ); // serializes any value type/combination of
                         // int/float/hash/array/string into a
                         // character string suitable for writing to a file
std::deserialize( string ); // given the string output from
                            // std::serialize(), return the original value, full restored

Limits

wrench is made to run on extremely limited hardware with an absolute minimum of bytecode size, and this comes at the cost of some value compression.

int : +/-2,147,483,647 (32 bit)

float : 32 bit (compiler/lib dependant)

char : 8 bit, no wide char support

functions : 256 functions per bytecode file, note this does NOT apply to library, external c calls, or imported
            calls all of which are unlimited.

source code size: no limit

byte code size: no fixed limit but all jumps are 16-bit relative so as low as 32k but more practically 40 or 50k
                If this is a limitation (it shouldn't be), a workaround would be to import modules

imported code: no limit, each can be max byte code size

native array/string elements: ~2 million (0x1FFFFF) elements

6.0 FAQ

... why? Aren't there enough interpreters out there? Surely one of them would have worked? Probably, but I couldn't find one! I tried squirrel, wren, tiny-c, pawn, even lua and a few others I can't think of. Most of them would compile and run for my embedded system (SAMD21 CortexM0) but they all blew chow when I actually tried to run scripts.

The problem? RAM.

They all needed a pile of it, hundreds of k in some cases. My chip has 32k total and I needed most of it for shift-buffer space!

wrench was motivated by a need for lightning-fast user-programmable scripts in a tight space

So use FORTH? Wrench is also motivated by the need for scripts that are approachable by novice-to-intermediate programmers. Asking them to become familiar with FORTH (or any of the many other expressive minimalistic langauges I've encountered) would sink the project.

You say wrench is fast, but LuaJITs is faster! Yes, I know. If a JIT language solves your problem then of course use one! I am a big fan of c# personally. wrench is for when that's not an option.