There are many projects out there that aim to bring scripting on microcontrollers. There are several JavaScript projects, like Duktape, Espruino, Jerryscript, MuJS, V7. There is MicroPython, there is Lua, and others.

One common thing these projects share is an attempt to implement the whole language specification, together with the more or less complete standard library. This is good and bad. The good part is obvious. What's about the bad part?

First, none of the popular scripting languages have been designed for the embedded environment in the first place. They drag along some obscure constructs that take precious space, but have very little practical usage in the embedded context.

Second, in order to export a hardware-specific functionality into the scripting environment (e.g. a certain sensor API, or a certain LCD display API), a glue code must be written. And that glue code should be maintained. That, again, takes precious space and increases overall complexity.

mJS - a restricted JavaScript engine

So, please welcome mJS - a new kid on the block, a new JavaScript engine. It takes a radically different approach:

  1. mJS does NOT implement the whole language, but a limited subset.
  2. mJS has NO standard library.
  3. mJS has NO glue code.

That makes mJS fit into less than 50k of flash space (!) and less than 1k of RAM (!!). That is hard to beat.

Fair enough. But how can THAT be useful, one might ask. No standard library? No glue code? Seriously?

Yes. Seriously.

The mJS's power is in having an ability to call C SDK functions directly.

FFI - calling C SDK functions directly

FFI means Foreign Function Interface. In mJS context, it's an ability to load and call C functions directly. How does mJS do that? It has to know 2 things: first, an address of the C function, and second, a signature of the C function. Then it marshals JS arguments into C values, puts them to where ABI demands (e.g. on the CPU stack), and jumps to the function's address. Practically it looks like this:

let func = ffi('int gpio_write(int, int)');
func(2, 1);

This snippet loads a C SDK function gpio_write(int pin, int value) and calls it, setting GPIO pin 2 to the high voltage level.

That's all. Need other functionality from the SDK, or 3rd party library? Just load it on demand. You can even do things like

let malloc = ffi('void *malloc(int)');
let mem = malloc(10);

Not saying that you should to that, but you can. Also, you can marshal C callbacks:

let Timer = {
  set: ffi('void timer(int, void (*)(int, userdata), userdata)')
};

Timer.set(100, function(time) {
  print('Time now: ', time);
}, true);

Do you need an embedding API, then? No, you don't. And you don't need any glue code either. And you don't need a standard library as well.

See how mJS is used in our Mongoose OS example firmware. It runs just fine on MCUs like ESP8266 with ~50k of RAM, as well on other MCUs like CC3220, STM32F4 or ESP32.

Nice. Can I use it?

Sure! With any C/C++ software of yours, for example with your specific firmware. See https://github.com/cesanta/mjs for the embedding example.

Let us know how it goes. Feel free to ask a question on our forum, or contact us directly on any question.