Introduction
"The difficulty is that things almost always start with some guy doing something that at the time looks totally useless"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
- two source files:
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
Turning a value into a container allocates a hash table which must be released with destroy!
WRValue& wr_makeString( WRContext* context, WRValue* val, const char* data, const int len =0 );
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)
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.
var a = 10;
var b = 3.4;
var string = "some string";
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:
A more complete example:
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
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 wrench calls a registered function directly
- Library Callbacks These are like callbacks but trimmed down for absolute maximum speed.
- Calls any wrench
function
can be called directly
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 withvoid 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? 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? You say wrench is fast, but LuaJITs is faster!