• Stars
    star
    1,581
  • Rank 29,599 (Top 0.6 %)
  • Language
    C
  • License
    Other
  • Created about 5 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A low footprint JavaScript engine for embedded systems

Elk: a tiny JS engine for embedded systems

Build Status License: AGPL/Commercial Code Coverage

Elk is a tiny embeddable JavaScript engine that implements a small but usable subset of ES6. It is designed for microcontroller development. Instead of writing firmware code entirely in C/C++, Elk allows to add JavaScript customisations to the firmware developed in C - which is a great way to let customers to extend/customise device functionality.

Elk features include:

  • Cross platform. Works anywhere from 8-bit microcontrollers to 64-bit servers
  • Zero dependencies. Builds cleanly by ISO C or ISO C++ compilers
  • Easy to embed: just copy elk.c and elk.h to your source tree
  • Small and simple embedding API
  • Does not use malloc. Operates with a given memory buffer only
  • Small footprint: about 20KB on flash/disk, about 100 bytes RAM for core VM
  • No bytecode. Interprets JS code directly

Elk approach is different from other scripting environments like micropython, which provide a complete JS API for everything. Elk is completely bare, it does not even have a standard library. All required functionality is supposed to be imported from C/C++ firmware, and JS code simply orchestrates things. That leaves Elk very minimal and tunable.

Below is a blinky demonstration on a classic Arduino Nano board which has 2K RAM and 30K flash (see full sketch):

Elk on Arduino Nano

JavaScript on ESP32

The Esp32JS Arduino sketch is an example of Elk integration with ESP32. Flash this sketch on your ESP32 board, go to http://elk-js.com, and get a JavaScript development environment instantly! All components, including ESP32 firmware and Web editor, are open. Here how it looks like:

The example JS firmware implements a classic blinky that uses timers imported from C.

Call JavaScript from C

#include <stdio.h>
#include "elk.h"

int main(void) {
  char mem[200];
  struct js *js = js_create(mem, sizeof(mem));  // Create JS instance
  jsval_t v = js_eval(js, "1 + 2 * 3", ~0);     // Execute JS code
  printf("result: %s\n", js_str(js, v));        // result: 7
  return 0;
}

Call C from JavaScript

This demonstrates how JS code can import and call existing C functions:

#include <stdio.h>
#include "elk.h"

// C function that adds two numbers. Will be called from JS
jsval_t sum(struct js *js, jsval_t *args, int nargs) {
  if (nargs != 2) return js_err(js, "2 args expected");
  double a = js_getnum(args[0]);  // Fetch 1st arg
  double b = js_getnum(args[1]);  // Fetch 2nd arg
  return js_mknum(a + b);
}

int main(void) {
  char mem[200];
  struct js *js = js_create(mem, sizeof(mem));      // Create JS instance
  js_set(js, js_glob(js), "sum", js_mkfun(sum)));   // Import sum()
  jsval_t result = js_eval(js, "sum(3, 4);", ~0);   // Call sum
  printf("result: %s\n", js_str(js, result));       // result: 7
  return 0;
}

Supported features

  • Operations: all standard JS operations except:
    • !=, ==. Use strict comparison !==, ===
    • No computed member access a[b]
    • No exponentiation operation a ** b
  • Typeof: typeof('a') === 'string'
  • For loop: for (...;...;...) ...
  • Conditional: if (...) ... else ...
  • Ternary operator a ? b : c
  • Simple types: let a, b, c = 12.3, d = 'a', e = null, f = true, g = false;
  • Functions: let f = function(x, y) { return x + y; };
  • Objects: let obj = {f: function(x) { return x * 2}}; obj.f(3);
  • Every statement must end with a semicolon ;
  • Strings are binary data chunks, not Unicode strings: 'Київ'.length === 8

Not supported features

  • No var, no const. Use let (strict mode only)
  • No do, switch, while. Use for
  • No => functions. Use let f = function(...) {...};
  • No arrays, closures, prototypes, this, new, delete
  • No standard library: no Date, Regexp, Function, String, Number

Performance

Since Elk parses and interprets JS code on the fly, it is not meant to be used in a performance-critical scenarios. For example, below are the numbers for a simple loop code on a different architectures.

for (let i = 0; i < 100; i++) true;
// 97 milliseconds on a 16Mhz 8-bit Atmega328P (Arduino Uno and alike)
// 16 milliseconds on a 48Mhz SAMD21
//  5 milliseconds on a 133Mhz Raspberry RP2040
//  2 milliseconds on a 240Mhz ESP32

Build options

Available preprocessor definitions:

Name Default Description
JS_EXPR_MAX 20 Maximum tokens in expression. Expression evaluation function declares an on-stack array jsval_t stk[JS_EXPR_MAX];. Increase to allow very long expressions. Reduce to save C stack space.
JS_DUMP undefined Define to enable js_dump(struct js *) function which prints JS memory internals to stdout

Note: on ESP32 or ESP8266, compiled functions go into the .text ELF section and subsequently into the IRAM MCU memory. It is possible to save IRAM space by copying Elk code into the irom section before linking. First, compile the object file, then rename .text section, e.g. for ESP32:

$ xtensa-esp32-elf-gcc $CFLAGS elk.c -c elk.tmp
$ xtensa-esp32-elf-objcopy --rename-section .text=.irom0.text elk.tmp elk.o

Note: Elk uses snprintf() standard function to format numbers (double). On some architectures, for example AVR Arduino, that standard function does not support float formatting - therefore printing numbers may output nothing or ? symbols.

API reference

js_create()

struct js *js_create(void *buf, size_t len);

Initialize JS engine in a given memory block. Elk will only use that memory block to hold its runtime, and never use any extra memory. Return: a non-NULL opaque pointer on success, or NULL when len is too small. The minimum len is about 100 bytes.

The given memory buffer is laid out in the following way:

  | <-------------------------------- len ------------------------------> |
  | struct js, ~100 bytes  |   runtime vars    |    free memory           | 

js_eval()

jsval_t js_eval(struct js *, const char *buf, size_t len);

Evaluate JS code in buf, len and return result of the evaluation. During the evaluation, Elk stores variables in the "runtime" memory section. When js_eval() returns, Elk does not keep any reference to the evaluated code: all strings, functions, etc, are copied to the runtime.

Important note: the returned result is valid only before the next call to js_eval(). The reason is that js_eval() triggers a garbage collection. A garbage collection is mark-and-sweep, run before every top-level statement gets executed.

The runtime footprint is as follows:

  • An empty object is 8 bytes
  • Each object property is 16 bytes
  • A string is 4 bytes + string length, aligned to 4 byte boundary
  • A C stack usage is ~200 bytes per nested expression evaluation

js_str()

const char *js_str(struct js *, jsval_t val);

Stringify JS value val and return a pointer to a 0-terminated result. The string is allocated in the "free" memory section. If there is no enough space there, an empty string is returned. The returned pointer is valid until the next js_eval() call.

js_glob()

jsval_t js_glob(struct js *);

Return global JS object, i.e. a root namespace.

js_mk*()

jsval_t js_mkundef(void);  // Create undefined
jsval_t js_mknull(void);   // Create null, null, true, false
jsval_t js_mktrue(void);   // Create true
jsval_t js_mkfalse(void);  // Create false
jsval_t js_mkstr(struct js *, const void *, size_t);           // Create string
jsval_t js_mknum(double);                                      // Create number
jsval_t js_mkerr(struct js *js, const char *fmt, ...);         // Create error
jsval_t js_mkfun(jsval_t (*fn)(struct js *, jsval_t *, int));  // Create func
jsval_t js_mkobj(struct js *);                                 // Create object
void js_set(struct js *, jsval_t, const char *, jsval_t);      // Set obj attr

Create JS values from C values

js_get*()

enum { JS_UNDEF, JS_NULL, JS_TRUE, JS_FALSE, JS_STR, JS_NUM, JS_ERR, JS_PRIV };
int js_type(jsval_t val);       // Return JS value type
double js_getnum(jsval_t val);  // Get number
int js_getbool(jsval_t val);    // Get boolean, 0 or 1
char *js_getstr(struct js *js, jsval_t val, size_t *len);  // Get string

Extract C values from JS values

js_chkargs()

bool js_chkargs(jsval_t *args, int nargs, const char *spec);

A helper function that checks a validity of the arguments passed to a function. A spec is a 0-terminated string where each character represents a type of the expected argument: b for bool, d for number, s for string, j for any other JS value.

Usage example - a C function that implements a JS function greater_than(number1, number2):

static jsval_t js_gt(struct js *js, jsval_t *args, int nargs) {
  if (!js_chkargs(args, nargs, "dd")) return js_mkerr(js, "bad args!");
  return js_getnum(args[0]) > js_getnum(args[1]) ? js_mktrue() : js_mkfalse();
}

js_setmaxcss()

void js_setmaxcss(struct js *, size_t max);

Set maximum allowed C stack size usage

js_stats()

void js_stats(struct js *, size_t *total, size_t *min, size_t *cstacksize);

Return resource usage statistics: total for total usable JS memory, min for the lowest free JS memory observed (low watermark), and cstacksize for the largest C stack usage observed.

js_dump()

void js_dump(struct js *);

Print debug info about the current JS state to stdout. Requires -DJS_DUMP

LICENSE

Dual license: AGPLv3 or commercial. For commercial licensing, technical support and integration help, please contact us at https://cesanta.com/contact.html

More Repositories

1

mongoose

Embedded Web Server
C
11,050
star
2

mongoose-os

Mongoose OS - an IoT Firmware Development Framework. Supported microcontrollers: ESP32, ESP8266, CC3220, CC3200, STM32F4, STM32L4, STM32F7. Amazon AWS IoT, Microsoft Azure, Google IoT Core integrated. Code in C or JavaScript.
C
2,508
star
3

mjs

Embedded JavaScript engine for C/C++
C
1,903
star
4

v7

Embedded JavaScript engine for C/C++
C
1,405
star
5

docker_auth

Authentication server for Docker Registry 2
Go
1,276
star
6

frozen

JSON parser and generator for C/C++ with scanf/printf like interface. Targeting embedded systems.
C
691
star
7

slre

Super Light Regexp engine for C/C++
C
520
star
8

fossa

Async non-blocking multi-protocol networking library for C/C++
C
440
star
9

mjson

C/C++ JSON parser, emitter, JSON-RPC engine for embedded systems
C
402
star
10

ssl_wrapper

Wrap plain TCP traffic into SSL
C
85
star
11

mongoose-os-smart-light

An example of full IOT product based on Mongoose OS
JavaScript
43
star
12

mDash

Arduino / ESP-IDF library for mdash.net IoT service
C
33
star
13

mos-tool

The Mongoose OS command line tool
24
star
14

str

A single header string library for microcontrollers - printf, json, etc
C
23
star
15

mongoose-os-docs

Mongoose OS Documentation
HTML
19
star
16

polar

PorarSSL <-> OpenSSL compatibility layer
C
17
star
17

validate-json

JSON validation tool and library
Go
16
star
18

stm32-bluepill

STM32 BluePill baremetal firmware for remote control via a CCM module
C
12
star
19

mdash-smart-light

a full IoT product reference design
JavaScript
10
star
20

mongoose-os-ide

VSCode extension for Mongoose OS
JavaScript
9
star
21

mip

Mini TCP/IP stack for embedded devices
C
7
star
22

arduino-drivers

C
6
star
23

homebrew-mos

Ruby
6
star
24

ucl

UCL handling library in Go
Go
5
star
25

gopro

go tcp and serial protocol proxy and dumper
Go
5
star
26

tcpuart

TCPUART
Makefile
5
star
27

mongoose-os-device-simulator

C
4
star
28

micro-printf

Tiny extendable printf for microcontrollers
C
3
star
29

mongoose-esp-idf

Mongoose Library component for ESP-IDF
CMake
3
star
30

goxnet

Fork of golang.org/x/net/websocket with fixes
Go
3
star
31

clubby_demo_android

Java
2
star
32

ubjson

Go
2
star
33

mongoose-wizard

2
star
34

vcon-app-example

A complete fleet dashboard built on https://vcon.io IoT platform.
JavaScript
2
star
35

aws-pico-tutorial

HTML
1
star
36

ccm-test-fw

Test firmware projects for CCM
C
1
star
37

mongoose-iot-examples

C
1
star
38

simplelink_mbed

simplelink SPI driver for mbed
C
1
star
39

gerrit-test

Test for gerrithub
C
1
star
40

binary

Makefile
1
star