A full decoder SDK example with comments
The following script decoder relates to the hypothetical protocol named "chapo". The reason we use a made up protocol in this example rather than a pre-existing one is:
So the protocol is very simple to document.
To require and thus show off many features of the script decoder.
Description of chapo protocol:
In the fictional chapo format each message begins with a header composed of:
Bell (ascii character 7)
Message Type (only byte ASCII '8' supported)
32-bit unsigned integer - length of message body, not including header.
64-bit unsigned integer - sequence id.
The unsigned integers are encoded in network-byte order. Here is an example header:
<7: 1 byte><ASCII '8': 1 byte><103: 4 bytes><1: 8 bytes>
This indicates a message with a body of size 103 and a sequence of 1.
The message body is composed of key value pairs. Pairs are seperated by the '~' character, and keys are seperated from values by the '=' character.
For example:
M=hello~T=1234
This represents two key value pairs ("M", "hello") and ("T", 1234)
Numbers are encoded as ASCII strings in the message body.
The chapo decoder script
// Load console module for writing messages to the terminal display.var console = vu8.load('console'), idx = 0, CHAPO_MSG_START = 7, CHAPO_MSG_TYPE = 8, CHAPO_HEADER_SIZE = 14, // uint64 sequence, start, type, uint32 size PAIR_SEP_BYTE = '~'.charCodeAt(0), KEY_VAL_SEP_BYTE = '='.charCodeAt(0)// Create 4 datafield objects used for this decodervar dfMarketId = new decoder.DatafieldUInt("script_v8.market_id"), dfSequence = new decoder.DatafieldUInt("script_v8.sequence"), dfSize = new decoder.DatafieldUInt("script_v8.size"), dfSymbolTicker = new decoder.DatafieldString("script_v8.symbol_ticker")try { // Register datafields with decoder's protocol layer. decoder.protocol.addDatafield(dfMarketId) decoder.protocol.addDatafield(dfSequence) decoder.protocol.addDatafield(dfSize) decoder.protocol.addDatafield(dfSymbolTicker)}catch (e) { // C++ exceptions propogate to JavaScript exceptions... console.log('chapo decoder: error adding data fields')}console.log('chapo decoder: loaded script')////////////////////////////////////////////////////////////////////////////// The next two functions are events called by the underlying stack API// "on_new_decoder" is called each time a new decoder is created and must be// defined in every script.function on_new_decoder(dec) { dec.idx = ++idx // market ID is a 64-bit integer and JavaScript numbers cannot represent // this type so decoder.UInt64 is provided for handling this type. dec.marketId = new decoder.UInt64() dec.sequence = new decoder.UInt64() // dfSize is 32-bit so can use a plain JavaScript number. dec.dfSize = 0 // Use this to signal if a message begin marker has been found. dec.foundMsgBegin = false // Can associate method with decoder object for use in on_data callback. dec.log = function(msg) { console.log('chapo decoder', this.idx + ':', msg) } console.log('created new chapo decoder')}// "on_data" is called when a decoder receives new data and must be defined// in every script.// The dec.buffer attribute is a Buffer object which represents a series of// underlying buffers as a single conceptual buffer.// The fact that the user is dealing with multiple underlying buffers// is hidden by the Buffer object API.// The last buffer in the chain is not copied from the underlying// stream until after on_data has been called (in the event that after// such a call there is still data remaining in the buffer) allowing// for zero-copy memory handling when possible.function on_data(dec) { // Use method associated with decoder object by "on_new_decoder". // size is the number of bytes left in the buffer. dec.log('new data available, reimaning buffer length ' + dec.buffer.size()) if (! dec.dfSize) { parse_header(dec) if (! dec.dfSize) return } while (parse_message(dec)) { dec.foundMsgBegin = false parse_header(dec) if (! dec.dfSize) return }}////////////////////////////////////////////////////////////////////////////// helper methods from here on// Parse chapo header.function parse_header(dec) { if (! dec.foundMsgBegin) { // Skips up to and over the character with the ascii byte given.. // returns true if the byte is found otherwise empties the buffer // and returns false. if (dec.buffer.skipToAndConsumeByte(CHAPO_MSG_START)) dec.foundMsgBegin = true else return } // Doing this is faster than dec.buffer.size() < 5, there is also: // sizeLessThanOrEqualTo, sizeGreaterThan, sizeGreaterThanOrEqualTo if (dec.buffer.sizeLessThan(CHAPO_HEADER_SIZE - 1)) return // Javascript has no facility for dealing with characters, so passing a // number through to isByte() is better than having to deal with expensive // string types. while (! dec.buffer.isByte(CHAPO_MSG_TYPE)) { dec.log('unsupported message type') if (! dec.buffer.skipToAndConsumeByte(CHAPO_MSG_START)) { dec.foundMsgBegin = false return } if (dec.buffer.sizeLessThan(CHAPO_HEADER_SIZE - 1)) return } // This advances the current character pointer in the buffer but does not // reclaim memory until an entire underlying buffer within the chain has // been advanced over so it is efficient to use. dec.buffer.advance(1) // Consume a byte-encoded uint32 from the buffer using network-byte // order. Use consumeUInt32LE to decode the integer using little-endian // byte ordering. There is also consumeInt32 and consumeInt32LE for // signed integers. This advances the buffer past the decoded data. dec.dfSize = dec.buffer.consumeUInt32() // An alternative way to do the above two operations would be: // dec.dfSize = dec.buffer.decodeUInt32(1) // dec.buffer.advance(5) // Binary decode uint64 from head of buffer and advance buffer pointer // past it. // Can use consumeLE to decode it using little-endian byte ordering. dec.sequence.consume(dec.buffer) // equivalent to the above: // dec.sequence.decode(dec.buffer, 0) // dec.buffer.advance(8) // There are decode*() versions of all consume*() methods. Decode is like // consume but does not advance the buffer and takes an argument indicating // the offset of the data to decode. dec.log('found message header which indicates a payload of size ' + dec.dfSize)}// Parse chapo message body. Return true if current message in buffer is// finished with. If it is finished and parsed successfully the buffer// will be advanced past it.function parse_message(dec) { if (dec.buffer.sizeLessThan(dec.dfSize)) { dec.log('chapo message split across multiple packets') return false } // Buffer.substring operates in the same manner as String.substring // in JavaScript. dec.log("buffer contains complete chapo message '" + dec.buffer.substring(0, dec.dfSize) + "'") var pairIdx, nextPairIdx = -1 while (nextPairIdx < dec.dfSize) { pairIdx = nextPairIdx + 1 var sepIdx = dec.buffer.indexOfByteFrom(KEY_VAL_SEP_BYTE, pairIdx + 1) nextPairIdx = dec.buffer.indexOfByteFrom(PAIR_SEP_BYTE, sepIdx + 1) var key = dec.buffer.substring(pairIdx, sepIdx) if ("S" == key) { dfSymbolTicker.set(dec.buffer.substring(sepIdx + 1, nextPairIdx)) } else if ("3" == key) { // use + to get JavaScript to convert string into integer dec.marketId.fromUInt32(+dec.buffer.substring(sepIdx + 1, nextPairIdx)) } } // Set these datafields from decoder.UInt64 objects dfMarketId.fromUInt64(dec.marketId) dfSequence.fromUInt64(dec.sequence) // fromUInt32 works on normal JavaScript numbers. dfSize.fromUInt32(dec.dfSize) // Pass through to next decoder with no data which triggers a // collection of the datafields. nextDecoder(length) and // nextDecoder(offset, length) are also provided for passing a // subsection of the buffer over. dec.nextDecoder() // advance past current message dec.buffer.advance(dec.dfSize) // Make both integers 0 again. dec.marketId.setZero() dec.dfSize = 0 return true}// Called when decoder misses data// dec: object representing decoder that missed data// size: size of missed data.. has type decoder.UInt64()function on_missed_data(dec, size) { console.log("missed data of length: " + size.toDouble()) dec.foundMsgBegin = false dec.buffer.reset()}