Hacker News new | past | comments | ask | show | jobs | submit login
mJS – A new approach to embedded scripting (mongoose-iot.com)
91 points by dimonomid on Jan 25, 2017 | hide | past | favorite | 60 comments



25k of flash space sounds like a lot for what is effectively just a JavaScript parser and interpreter. I recall the days when you could fit a whole language's compiler into a few measly KB. 1KB RAM is also quite a lot for certain boards, especially if it has to be stack or SRAM.

How's the performance? Will my 72MHz Cortex or 16MHz Arduino be able to run interesting things with mJS? If I have to do everything through FFI, what's the benefit vs. e.g. C++11? The latter has nice language features too but compiles to much smaller native code!


Nice thing about pain-free FFI is that you can move functionality between the scripting-layer and engine-layer as you see fit. Start in JS, perhaps move pieces down to compiled-lang as they solidify or perf becomes an issue. Maybe only the high-level logic is JS. Or maybe you have JS as "plugin" extension points. Or one could design a domain-specialized "framework", and use JS as the embedding language for an embedded DSL. Acheivable performance would depend a lot on such architectural decisions.

With C++ one would unfortunately have to create C-wrappers, but with say something with easier C interop like Nim or Rust.


1kb RAM and 25k of flash is impressively low for Javascript engines for embedded system. I know it is still heavy for MCUs. Other JS engine implementations like Ducktape [1] and Jerryscript [2] takes more than 10kb of RAM and 150 of FLASH. Javascript is simply much heavier you might think. But I personally don't see any benefits neither.

[1] http://duktape.org/

[2] http://jerryscript.net/


Ideally none of the flash space is spent on a parser/compiler, that's quite wasteful if you could compile bytecode offline.

I don't know if you can do that with mJS, alas.


If you want to support eval, then the whole parser/compiler needs to be on the device.

But do you really need eval?


mJS appears to include the parser and compiler, as it seems capable of running code dynamically.


Will it run it in real time is the important thing.


I'd wager a large amount of that is handling symbol lookup for calling through the FFI.


I quote the mJS readme from the repo:

> In order to make FFI work, mJS must be able to get the address of a C function by its name. On POSIX systems, dlsym() API can do that. On Windows, GetProcAddress(). On embedded systems, a system resolver should be either manually written, or be implemented with some aid from a firmware linker script.

So I guess they don't provide any implementation for their FFI linking on embedded platforms? It's not clear if the 25k size figure includes any dlsym support, but if it requires instrumenting your firmware image on-disk then the size costs could balloon significantly.


Currently we use mJS in our Mongoose OS firmware framework. There, a symbol resolver is generated automatically from the configuration file which tells which symbols you'd like to use.

https://github.com/cesanta/mongoose-os/blob/master/fw/exampl...


That FFI interface looks way nicer than the node-ffi interface.

    let f = ffi('int gpio_write(int, int)');
vs.

    var current = ffi.Library(null, {
      'atoi': [ 'int', [ 'string' ] ]
    });


You could probably write a wrapper to go from the node way to their style.


This looks a lot like Lua's FFI interface[0], which is a compliment (but contradicts the statement that this sort of FFI is "the feature that no other engine implemented so far"). Nicely done.

[0] http://csl.sublevel3.org/post/luajit-cpp/


Nice! I wasn't aware of lua ffi api, thank you!


Is this a re-packaged v7 [0] (it's from Cesanta as well)? mjs.c is 477K. It looks like it as v7.c is 475K [1]. It shaves whole entire 1MB from my staticly linked builds (v7 1.9MB vs. mjs 998K), and almost 2MB from dynamically linked builds (v7 1.9MB vs. mjs 99K) on amd64. If it shares the same underlying architecture and api [2] then this is a pretty great achievement.

In their docs, they claim 25K storage and 10K RAM. Blog post claims 25K storage and 1K RAM.

[0] https://github.com/cesanta/mjs/blob/master/mjs.c

[1] https://github.com/cesanta/v7/blob/master/v7.c

[2] https://docs.cesanta.com/v7/master/#/v7-internals/


i wonder if the size could be reduced by replacing the yacc code with a hand-written parser.


There is no yacc code in either V7 or mJS.

V7 uses hand-written recursive-descent parser. Initially, it was using ordinary C functions, and that created a problem on systems with low stack size. E.g. each '(' starts statement parsing from the top, so 1 + (2 + (3 + (4 + 5))) consumed stack, and sometimes resulted in stack overflow in e.g. interrupt handlers or network callbacks.

Therefore we have rewritten recursive descent using C coroutines, and that is extremely sophisticated piece of work. See https://raw.githubusercontent.com/cesanta/v7/master/v7.c , search for #line 1 "v7/src/parser.c"

mJS on the other hand uses lemon parser generator - the one from sqlite project. It generates a LALR parser which is quite efficient in terms of memory.


So it is API compatible with V7?


Nope. It's similar though, cause some of the concepts, and the code, were reused. mJS does not need an embedding API, really. The intent is that FFI is used.


I've never worked with embedded systems, so forgive my ignorance. Why is this any more useful than just writing c code, if all the code that actually interacts with the hardware has to be written in c anyway?


1) A lot of embedded projects have HTTP interfaces for command & control. So that means you usually have somebody on the team who knows HTML & CSS & the giant pile of tech that is web dev these days. Which means they probably are more familiar with JS than C or any other more typical embedded scripting language like Lua.

2) C needs to be compiled and often flashed onto the device to run, sending new JS over the wire to execute is a lot more convenient, especially if you want customers to be able to customize their devices.


One possible use top of my head would be vendor customization. For example Foo Inc is manufacturing some hot IoT widget which is then sold and branded by Bar Ltd. Foo can take care of all the nitty gritty low level details and provide nice high level SDK for Bar to make their customizations.


The ability to script without the need of rebuilding/reflashing the whole firmware is often fairly useful. The most value is not in JS per se, but in the ability to script.


Ah, I can see how that is useful. Way up in the web dev world I don't often have to think of things like that :)


I'm wondering the same thing. What does having a JS syntax bring to embedded programming?


Scripting is rarely about syntax. It's about being able to load code dynamically, being able to create ad-hoc data structures, quick changes and distributing executables as source rather than binaries.


None of which are particularly appropriate for embedded programming.


I've adopted a rule where I try to replace a sentence, thought, or expression with, "That's different from how we do things right now," and then try to see if it changes the meaning of what was actually said. Mostly, this is useful when the comment serves to justify shooting down another idea.

"We can't do things that way, because we do things a different way", is something that many people will automatically recognize as a bad argument if you put it that way. (It's in the same bucket as, "Well, nobody has ever complained before.") But anyone—even by accident—can end up using this argument while saying different words. The effect is that it comes in a different package that's more difficult to spot. In the end, though, it's the same invalid discussion killer.

Anyway, that's a lot of exposition. What I came here to say is that your comment is tripping the filter for me right now.

What's inappropriate about a fast development feedback cycle when doing embedded programming?


The answer is nothing is wrong. Some form of shell, stack or scripting language is a super useful tool in the prototyping stages. It might not meet timing requirements for a final system, but at the beginning of a project it's great.


All of these which are very appropriate for a company who wants to sell hobbyist kits to kids who already know JavaScript, so they can built IoT toys quickly.

I understand your point. These tools aren't appropriate for serious embedded systems, but they're also not meant for them. They're meant for people who want to have fun and want to play with things quickly.


Agreed that on-a-whole JS is not appropriate for embedded devices. However allowing scripting may be useful in particular pieces of the code, to allow easy extension / customization.


I recall that very much the same opinions were expressed about the JS and backend programming, until node.js appeared. Now the reality is different.

To be honest, we do not think that JS is a good language for embedded. Like any other existing popular scripting language. Perhaps scripted Go would be a better choice.

The point is that in many, many cases scripting brings a lot of benefits to the embedded environment. It all depends on a specific tasks - for some tasks, scripting will never be appropriate.


I would prefer to see a scripted Go over JS for embedded environments.


a nice graphic on the layers of an embedded software is this: http://www.limifrog.io/wordpress/wp-content/uploads/2015/07/... - getting the toolchain running to blink a LED takes some efforts. With scripting, you could save time in the context of prototyping a connected experience (networking, hardware, network protocols)


The low-level hardware access is handled using C, but presumably people want to write higher-level code on top of that. It doesn't seem that unusual.


Haven't you heard that when you want to get close to the metal you drop down to node.js?


> One common thing these projects share is an attempt to implement the whole language specification, together with the more or less complete standard library

Not in the case of Lua. Well, the "whole language" part is correct, but Lua is a very tiny language spec. It's "standard library", however, is the opposite of "complete".


> none of the popular scripting languages have been designed for the embedded environment in the first place

Also false for Lua. It is designed to be embedded.


Embedded environment in this context means hardware low on resources, e.g. microcontrollers. Your perception of that word is "embedded into the C/C++ program". These are two different things. I agree that Lua (like some other languages) were designed to be embedded into C/C++ apps.



So is the only benefit of wasting that space and CPU so you can bill your platform/project/whatever as being "js" and C/C++-free? What does a no standard library JS buy you over a no standard library C++11 everyone else is using these days, besides the pain and trouble of dealing with a dynamically typed language without native debugging support?

Why is there so much stigma against _learning_ to code in something other than $favlang these days? Most hard core developers I know appreciate the importance of using the right tool for the job, I don't see embedded/desktop developers shying away from using whatever the native toolkit/language is for their chosen platform and instead shoehorning $x to fit as much as we see this constant trend to try to use "web tech" everywhere. (Scripting in embedded systems has long been a solved problem: use lua.)


For example, I'm working on a dashboard for wifi routers that displays information beyond what is provided by OpenWRT's ubus system. We're get this information from the kernel, but it needs to be converted to JSON to be consumed by the dashboard frontend. Right now, I'm converting the information to JSON using Go, but there are some platforms that Go can have problems compiling binaries for. We have also written a lot of C for these routers, but doing this kind of thing in C would just be overly cumbersome. Shell could be an option, but it is also cumbersome. Lua could be an option, but I don't know much about it and I don't know how well it supports JSON. Most other developers that will work on this don't know much Lua either.

mJS might also be an option. I don't really see what's wrong with that if we have the space for it.

This can probably get types from Flow, since it is valid es6. Flow has no compilation step or runtime component.

You may have a point about the debugging, but JS is pretty easy to debug by printing to stdout.


Lua is the easiest language to learn I ever saw. It has a fantastic json support with the cjson library (https://luarocks.org/modules/luarocks/lua-cjson). It's there by default on many firmwares. Do check the docs for nodemcu, for example: https://nodemcu.readthedocs.io/en/master/ (a firmware for the ESP microcontrollers)

Not to mention Lua itself can be used as a data / configuration language, their tables syntax is made especially for that. Normally when I work with Lua I don't feel the need to embed any other parsers for configuration, but cjson is there if you want.


_learning_ is an investment, and most times it doesn't make sense to invest years to learn a couple of languages to be efficient.

I'd say it's better to know one language and know it well.


Neat. Although languages which are "almost, but not quite <x>" are, I would say, harder for people who know <x> to learn than languages that are entirely different from <x>.


As light weight, easily embeddable JS engines go, I'd like to mention Duktape[0]. But the FFI of mJS does look quite a bit nicer.


Duktape is nice, and I've mentioned it in the article. However it's quite fat, and can't be used on some boards like ESP8266. It's flash footprint is Megabytes, whereas mJS flash footprint is ~25k.


Duktape website:

"Embeddable, portable, compact: can run on platforms with 192kB flash and 64kB system RAM"



Ah, thanks, wrong about flash footprint. Neil has done a great job adopting Duktape on ESP32.


Taking this example:

    let malloc = ffi('void *malloc(int)');
    let mem = malloc(10);
How do you manipulate that memory?

How can you do the following in mJS?

    int* mem = malloc(10);
    mem[0] = 3;
    mem[1] = 7;
    mem[2] = 12;
    int *rest = mem + 3;
There are some things you can do in C without functions. How does mJS achieve this?


Currently, mJS FFI is limited to functions only. And, to simple enough functions.

On how to access memory from mJS: write an accessor function!

    // C
    void setmem(uint8_t *ptr, int index, int value) {
      ptr[index] = value;
    }

    // mJS
    let setmem = ffi('void setmem(void *, int, int)');
    let malloc = ffi('void *malloc(int)');
    let mem = malloc(10);
    setmem(mem, 3, 5);


The common use case for doing that in other FFIs is not to use it in your own language, but to pass the pointer to another function. That is, you can translate

    int *buffer = malloc(10);
    SSL_read(ssl, buffer, 10);
into

    let buffer = ffi('void *malloc(int)');
    let SSL_read = ffi('int SSL_read(void *ssl, void *buf, int num)');
    let buffer = malloc(10);
    SSL_read(ssl, buffer, 10);
Your own language generally has ways to allocate memory—but does not necessarily have a way to generate a pointer, or to manually manage the lifetime of that memory.


This is neat. This isnt quite javascript (which is neither good nor bad) but I think has a lot of potential


Espruino's had an FFI interface for the last 3 years! Nice to see they did their research :)


Hi Gordon! Nice to meet you :) I am the author of the article, and I am quite fond of Espruino, it's a great project. Wasn't aware that Espruino has FFI API!


Hi! Thanks, v7 looked great too. mJS looks like a good idea - IMO it'd be nice to standardise a sane, minimal subset of JS that can be easily implemented - it'd be a huge help for all the developers of embedded JS interpreters, and could potentially be targeted by transpilers too.


Worth mentioning: https://github.com/elua

Allows to run Lua on bare metal, no OS involved. It's what the nodemcu firmware for the ESP cards is based on.


Seems a little crazy that there's no support for closures.


Yes. Closures add quite a bit of complexity and footprint. Anticipated use case was a short script that orchestrates the device logic, calling existing SDK functions.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: