Smart.js - JavaScript for IoT devices

by Sergey Lyubka, Cesanta

@CesantaHQ

Agenda

  • What is JavaScript
  • Engines
  • Browser environment
    • window, document objects
  • Node.js environment
    • process object
  • Smart.js environment
    • Demo
    • Filesystem, Http, WebSocket, HW
  • V7 engine architecture
    • NaN packing, GC, coroutine based parser

What is JavaScript

  • Originally developed at Netscape by Brendan Eich, in10 days in May 1995
    • ​Original idea: embed Scheme into Netscape browser
    • Netscape strategy: develop a language for everyone
    • Netscape 2.0 release
  • Mocha -> LiveScript (Sep 1995) -> JavaScript (Sep 1995)
  • December 1995: server side JS
  • Nov 1996: application to ECMA International
  • Jun 1997: first ECMA-262 spec published
  • Dec 1999: ECMA-262 3rd edition
  • Dec 2009: ECMA-262 5th edition
  • Jun 2015: ECMA-262 6th edition

JavaScript engines

Name Application Technology
SpiderMonkey Firefox browser C/C++
V8 Chrome browser C/C++
Rhino Firefox browser Java
Ducktape embeddable C/C++
V7 embeddable C/C++

 (*) More complete list of engines at Wikipedia

JavaScript environment

JavaScript Engine

JS standard

library

Host program

Host Program API

JavaScript Application

Browser environment

JavaScript Engine

Object, Function, Date, String, Regex,

Browser (C/C++)

JavaScript Application

Error, Array, Math, eval(), parseInt(), ...

DOM API

STDLIB API

Document, Window, Event, Node, Element, URL, ...

WebSocket, document, window, location, ...

Browser environment

JavaScript Engine (V7, Rhino, ...)

Object, Function, Date, String, Regex,

Browser (C/C++)

document.write('hello world')

Error, Array, Math, eval(), parseInt(), ...

DOM API

STDLIB API

Document, Window, Event, Node, Element, URL, ...

document, window, location, ...

Node.js environment

JavaScript Engine (V8)

Object, Function, Date, String, Regex,

node process

 fs = require('fs'); fs.readFile('/etc/hosts', 'utf8', function (err,data) {
  console.log(arguments);
 });

Error, Array, Math, eval(), parseInt(), ...

Node.js API

STDLIB API

process, net, console, fs, http, os, ...

Smart.js environment

JavaScript Engine (V7)

Object, Function, Date, String, Regex,

Low-level SDK

GPIO.setmode(pin, 0, 0);
GPIO.write(pin, !level);

Error, Array, Math, eval(), parseInt(), ...

Smart.js API

STDLIB API

File, Websocket, Http, GPIO, I2C, 

Documentation is at https://cesanta.com/developer/smartjs

Demo with Nodemcu

ESP8266 - a typical IoT Device

  • 32-bit 160Mhz Tensilica processor
  • 50k RAM (*)
  • 512k (or 4M) Flash

Size comparison

Node.js (*)

ESP8266

Smart.js

180k

<1k

512k

Flash

RAM

RAM

Flash

Disk

RAM

50k

11M

7.6M

Smart.js Key Components

  • Mongoose networking library, C/C++
    • Mature - developed since 2004
    • TCP, UDP, HTTP, Websocket, DNS, MQTT, ...
    • Used in e.g. Apple maps, NASA, Samsung,  ...
  • V7 JavaScript engine
    • development started in 2013
    • smallest in the world
    • design goals: simple C/C++ API, small footprint

V7 JavaScript Engine

  • Careful data structure design
  • Multiple techniques to make footprint small
  • Few to note:
    • NaN packing
    • Coroutine-based parser

NaN Packing

//  Double-precision floating-point number, IEEE 754
//
//  64 bit (8 bytes) in total
//  1  bit sign
//  11 bits exponent
//  52 bits mantissa
// 
//      7         6        5        4        3        2        1        0
//  seeeeeee|eeeemmmm|mmmmmmmm|mmmmmmmm|mmmmmmmm|mmmmmmmm|mmmmmmmm|mmmmmmmm
//
//  11111111|11110000|00000000|00000000|00000000|00000000|00000000|00000001  NaN



//  V7 NaN-packing:
//    sign and exponent is 0xffff
//    4 bits specify type
//    48 bits specify value
//
//  11111111|1111xxxx|00000000|00000000|00000000|00000000|00000000|00000000  NaN
//   NaN marker |type|  48-bit placeholder for values: pointers, strings
//

typedef uint64_t v7_val_t;

#define V7_TAG_BOOLEAN ((uint64_t) 0xFFFC << 48)
#define V7_TAG_FUNCTION ((uint64_t) 0xFFF5 << 48)  /* JavaScript function */
#define V7_TAG_OBJECT ((uint64_t) 0xFFFF << 48)    /* JavaScript object */
#define V7_TAG_REGEXP ((uint64_t) 0xFFF2 << 48)    /* RegExp */
  • The largest JavaScript scalar type is Numbe - IEEE 754 double
  • 1 sign bit, 11 bits exponent, 52 bits mantissa
  • If exp bits are all 1 and the mantissa is non-0, the number is NaN

Coroutine Based Parser


   var x = 1 + (2 + (3 + (4 + (5 + (6 + (7))))));
  • Why this could be a problem?

Coroutine Based Parser


   var x = 1 + (2 + (3 + (4 + (5 + (6 + (7))))));
  • Recursive-descent parser is used
  • Expression parsing results in the function call chain, according to JavaScript grammar
  • parse_statement() calls parse_var() calls parse_binary()...

Coroutine Based Parser

(lldb) bt
* thread #1: tid = 0x59fbd, 0x00000001000312c7 v7`get_tok [inlined] parse_number(s=<unavailable>, end=0x0000000100801560, num=0x00000001008015b0) at v7.c:3306, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001000312c7 v7`get_tok [inlined] parse_number(s=<unavailable>, end=0x0000000100801560, num=0x00000001008015b0) at v7.c:3306
    frame #1: 0x00000001000312c7 v7`get_tok(s=0x0000000100801560, n=0x00000001008015b0, prev_tok=<unavailable>) + 695 at v7.c:3470
    frame #2: 0x000000010000a440 v7`parse_prefix [inlined] next_tok + 100 at v7.c:7485
    frame #3: 0x000000010000a3dc v7`parse_prefix(v7=0x0000000100801200, a=0x0000000100200100) + 1068 at v7.c:7745
    frame #4: 0x0000000100031ded v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=11, pos=63) + 45 at v7.c:7812
    frame #5: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=10, pos=63) + 73 at v7.c:7814
    frame #6: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=9, pos=63) + 73 at v7.c:7814
    frame #7: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=8, pos=63) + 73 at v7.c:7814
    frame #8: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=7, pos=63) + 73 at v7.c:7814
    frame #9: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=6, pos=63) + 73 at v7.c:7814
    frame #10: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=5, pos=63) + 73 at v7.c:7814
    frame #11: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=4, pos=63) + 73 at v7.c:7814
    frame #12: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=3, pos=63) + 73 at v7.c:7814
    frame #13: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=2, pos=63) + 73 at v7.c:7814
    frame #14: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=1, pos=63) + 73 at v7.c:7814
    frame #15: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=0, pos=63) + 73 at v7.c:7814
    frame #16: 0x0000000100030f13 v7`parse_var [inlined] parse_assign(v7=0x0000000100801200, a=0x0000000100200100) + 14 at v7.c:7855
    frame #17: 0x0000000100030f05 v7`parse_var(v7=0x0000000100801200, a=0x0000000100200100) + 741 at v7.c:7888
    frame #18: 0x000000010002e38e v7`parse_statement(v7=0x0000000100801200, a=0x0000000100200100) + 5614 at v7.c:8146
    frame #19: 0x000000010002c66b v7`parse_body(v7=0x0000000100801200, a=0x0000000100200100, end=TOK_CLOSE_CURLY) + 571 at v7.c:8223
    frame #20: 0x000000010002cc43 v7`parse_funcdecl(v7=0x0000000100801200, a=0x0000000100200100, require_named=<unavailable>, reserved_name=<unavailable>) + 1331 at v7.c:8190
    frame #21: 0x00000001000327e5 v7`parse_newexpr(v7=0x0000000100801200, a=0x0000000100200100) + 485 at v7.c:7643
    frame #22: 0x000000010000a6a3 v7`parse_prefix [inlined] parse_callexpr(v7=0x0000000100801200, a=0x0000000100200100) + 15 at v7.c:7695
    frame #23: 0x000000010000a694 v7`parse_prefix [inlined] parse_postfix(a=0x0000000100200100) + 4 at v7.c:7717
    frame #24: 0x000000010000a690 v7`parse_prefix(v7=0x0000000100801200, a=0x0000000100200100) + 1760 at v7.c:7777
    frame #25: 0x0000000100031ded v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=11, pos=32) + 45 at v7.c:7812
    frame #26: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=10, pos=32) + 73 at v7.c:7814
    frame #27: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=9, pos=32) + 73 at v7.c:7814
    frame #28: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=8, pos=32) + 73 at v7.c:7814
    frame #29: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=7, pos=32) + 73 at v7.c:7814
    frame #30: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=6, pos=32) + 73 at v7.c:7814
    frame #31: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=5, pos=32) + 73 at v7.c:7814
    frame #32: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=4, pos=32) + 73 at v7.c:7814
    frame #33: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=3, pos=32) + 73 at v7.c:7814
    frame #34: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=2, pos=32) + 73 at v7.c:7814
    frame #35: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=1, pos=32) + 73 at v7.c:7814
    frame #36: 0x0000000100031e09 v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=0, pos=32) + 73 at v7.c:7814
    frame #37: 0x0000000100031f8f v7`parse_binary(v7=0x0000000100801200, a=0x0000000100200100, level=<unavailable>, pos=5) + 463 at v7.c:7842
    frame #38: 0x000000010002d153 v7`parse_statement [inlined] parse_assign(v7=0x0000000100801200, a=0x0000000100200100) + 947 at v7.c:7855
    frame #39: 0x000000010002d13c v7`parse_statement [inlined] parse_expression(a=0x0000000100200100) + 4 at v7.c:7862
    frame #40: 0x000000010002d138 v7`parse_statement(v7=0x0000000100801200, a=0x0000000100200100) + 920 at v7.c:8157
    frame #41: 0x000000010002c66b v7`parse_body(v7=0x0000000100801200, a=0x0000000100200100, end=TOK_END_OF_INPUT) + 571 at v7.c:8223
    frame #42: 0x0000000100001a74 v7`parse [inlined] parse_script(v7=0x0000000100801200, a=0x0000000100200100) + 398 at v7.c:8249
    frame #43: 0x00000001000018e6 v7`parse(verbose=<unavailable>, v7=<unavailable>, a=<unavailable>, src=<unavailable>) + 230 at v7.c:8275
    frame #44: 0x000000010000bacd v7`v7_exec_with(v7=0x0000000100801200, res=0x00007fff5fbfefb0, src=0x0000000100035710, w=18445899648779419648) + 285 at v7.c:9909
    frame #45: 0x0000000100009891 v7`init_stdlib [inlined] v7_exec(res=0x0000000000000366, src=<unavailable>) + 15745 at v7.c:9931
    frame #46: 0x000000010000986e v7`init_stdlib [inlined] init_js_stdlib at v7.c:13545
    frame #47: 0x000000010000986e v7`init_stdlib(v7=0x0000000100801200) + 15710 at v7.c:13532
    frame #48: 0x00000001000017da v7`v7_create + 426 at v7.c:7022
    frame #49: 0x0000000100012ec9 v7`v7_main(argc=3, argv=0x00007fff5fbff920, init_func=0x0000000000000000) + 57 at v7.c:12156
    frame #50: 0x000000010001346b v7`main(argc=<unavailable>, argv=<unavailable>) + 11 at v7.c:12221
    frame #51: 0x00007fff908615fd libdyld.dylib`start + 1

Ending up with C call stack like this:

Coroutine Based Parser

  • Embedded systems do not have gigabytes of stack size
  • Kilobytes, sometimes less than 1 kilobyte
  • What to do?
  • Using parser generators?
    • Result code is too big
  • Flatten the parser?
    • Loose algorithmic clarity of the parser, by having function call chain
  • Coroutines!
  • Code is on Github, function parse()
    • Careful: that code violates any coding standard

Coroutine Based Parser

  • Traditional function: 1 entry point, and multiple return points
  • Coroutine: multiple entry points and multiple return points
  • It preserves state
co = coroutine.create(function(n)
  for i = 1, n do
    coroutine.yield(i)
  end
  coroutine.yield(-1)
end)

print(coroutine.resume(co, 3));
print(coroutine.resume(co));
print(coroutine.resume(co));
print(coroutine.resume(co));


$ lua co.lua
true	1
true	2
true	3
true	-1

Coroutine Based Parser

lsm@callisto ~/src/cesanta.com]$ lldb v7/v7
(lldb) target create "v7/v7"
Current executable set to 'v7/v7' (x86_64).
(lldb) b parse_number
Breakpoint 1: 2 locations.
(lldb) process launch -- -e 'var x = 1 + (2 + (3))'
Process 10349 launched: '/Users/lsm/src/cesanta.com/v7/v7' (x86_64)
Process 10349 stopped
* thread #1: tid = 0x5a2b8, 0x000000010000af64 v7`parse_number(s=0x0000000100037604, end=0x00000001008015c8, num=0x0000000100801618) + 20 at tokenizer.c:132, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x000000010000af64 v7`parse_number(s=0x0000000100037604, end=0x00000001008015c8, num=0x0000000100801618) + 20 at tokenizer.c:132
   129 	}
   130
   131 	static void parse_number(const char *s, const char **end, double *num) {
-> 132 	  *num = strtod(s, (char **) end);
   133 	}
   134
   135 	static enum v7_tok parse_str_literal(const char **p) {
(lldb) bt
* thread #1: tid = 0x5a2b8, 0x000000010000af64 v7`parse_number(s=0x0000000100037604, end=0x00000001008015c8, num=0x0000000100801618) + 20 at tokenizer.c:132, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x000000010000af64 v7`parse_number(s=0x0000000100037604, end=0x00000001008015c8, num=0x0000000100801618) + 20 at tokenizer.c:132
    frame #1: 0x000000010000a4da v7`get_tok(s=0x00000001008015c8, n=0x0000000100801618, prev_tok=TOK_OPEN_PAREN) + 874 at tokenizer.c:296
    frame #2: 0x00000001000194f4 v7`next_tok(v7=0x0000000100801200) + 196 at parser.c:1248
    frame #3: 0x000000010001630d v7`parser_cr_exec(p_ctx=0x00007fff5fbfe9d8, v7=0x0000000100801200, a=0x0000000100200160) + 8157 at parser.c:1836
    frame #4: 0x000000010001980f v7`parse(v7=0x0000000100801200, a=0x0000000100200160, src=0x0000000100037580, verbose=1, is_json=0) + 415 at parser.c:2454
    frame #5: 0x000000010001b454 v7`i_exec(v7=0x0000000100801200, src=0x0000000100037580, src_len=0, res=0x00007fff5fbfed10, w=18445899648779419648, is_json=0, fr=0) + 612 at interpreter.c:1838
    frame #6: 0x000000010001d4ad v7`v7_exec(v7=0x0000000100801200, src=0x0000000100037580, res=0x00007fff5fbfed10) + 93 at interpreter.c:1923
    frame #7: 0x000000010002302c v7`init_js_stdlib(v7=0x0000000100801200) + 60 at js_stdlib.c:132
    frame #8: 0x0000000100022fa6 v7`init_stdlib(v7=0x0000000100801200) + 710 at stdlib.c:199
    frame #9: 0x0000000100012440 v7`v7_create_opt(opts=v7_create_opts at 0x00007fff5fbfedb0) + 480 at vm.c:1831
    frame #10: 0x000000010003645d v7`v7_main(argc=3, argv=0x00007fff5fbff720, init_func=0x0000000000000000, fini_func=0x0000000000000000) + 1181 at main.c:126
    frame #11: 0x0000000100036b92 v7`main(argc=3, argv=0x00
  • Coroutine based parser reduces call chain 
  • From 40+ frames to just 5:

References

  • Code on Github:
    • https://github.com/cesanta/mongoose
    • https://github.com/cesanta/v7
    • https://github.com/cesanta/smart.js
  • Documentation:
    • https://www.cesanta.com/developer/

Thank You!

contact me at

sergey.lyubka@cesanta.com

Smart.js - JavaScript for IoT devices

By Sergey Lyubka

Smart.js - JavaScript for IoT devices

Describes the architecture of Smart.js, a JavaScript-enabled firmware for the embedded devices

  • 4,042