Library / SDK
cesanta/mjs avatar
cesanta/mjs

mJS: a restricted JavaScript engine for C/C++ microcontroller firmware

Embedded JavaScript engine for C/C++

2,061 stars185 forksCNOASSERTION

At a glance

What is it?
mJS is an embedded JavaScript engine that implements a strict ES6 subset, fitting in about 50k of flash and under 1k of RAM on 32-bit ARM. Its FFI layer imports C functions from JavaScript without glue code, but the language restrictions are extensive and the standard library is entirely absent.
Who is it for?
mJS fits MCU firmware projects running MongooseOS or a custom C environment that needs dynamic JavaScript scripting within tight memory limits. On 32-bit ARM the engine takes about 50k of flash and under 1k of RAM.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Running JavaScript on MCUs with 50k of flash and under 1k of RAM

mJS is a JavaScript interpreter written in C, targeted at microcontrollers where memory is measured in kilobytes rather than megabytes. On 32-bit ARM hardware, the engine occupies about 50k of flash and requires less than 1k of RAM, according to the README and the linked introduction article at mongoose-os.com. That footprint puts JavaScript scripting within reach of devices that cannot run a general-purpose operating system.

mJS is part of MongooseOS, providing the scripting layer for IoT device firmware. Its two stated design goals are a small binary footprint and simple C/C++ interoperability. Both goals require trade-offs: the language subset is narrow, the standard library is absent, and the execution model is simpler than a desktop runtime.

mJS implements a strict ES6 subset. Any valid mJS code is valid ES6 code, which means scripts can be tested in a standard JavaScript environment before deploying to the MCU. ES6 code that uses closures, classes, promises or the standard library will not run in mJS.

The ES6 features mJS removes and the restrictions that remain

mJS removes a large portion of ES6 to meet its memory target. No standard library is present: String, Number, RegExp, Date and Function objects are all absent. JSON.parse() and JSON.stringify() are the two exceptions that remain.

Closures are not supported, though nested functions are allowed. The restriction is that nested functions cannot capture variables from an outer scope. Exceptions are off entirely, which means error handling must use return values or die(). The new keyword is not available; instead, Object.create() is provided for creating objects with a custom prototype. Variable declarations use let only, since var is removed. Strict mode applies everywhere without an opt-out mechanism.

Several ES6 features are excluded entirely: for..of loops, arrow functions, destructuring, generators, proxies and promises. Getters, setters, valueOf, prototypes, classes and template strings are also removed. Equality comparisons accept only === and !==, with == and != absent.

String behavior reflects a deliberate choice: mJS strings are byte strings rather than Unicode. The README illustrates this with 'ы'.length === 2, since that character occupies two bytes in UTF-8. Byte-based strings let mJS hold arbitrary binary data, which suits embedded use cases dealing with byte buffers. Any code that assumes Unicode string length or character indexing will produce different results.

Embedding mjs.c and mjs.h as a single-file amalgamation

mJS does not distribute through a package registry. The project produces two amalgamated files at the repository root: mjs.c and mjs.h. Embedding the engine in a C project means adding those two files to the build system rather than installing a package.

For developers working outside MongooseOS, mjs.h provides the public API declarations and mjs.c provides the implementation. Both files are built from source under src/ using tools/amalgam.py, as shown in the repository Makefile. That Makefile uses C99 with -lm and builds the standalone binary using Docker images mgos/gcc and mgos/clang. It also exposes a CFLAGS_EXTRA variable for adding project-specific flags.

The Makefile covers Linux, macOS and Windows in its conditionals. On Linux, the build links with -ldl for dynamic symbol resolution. On macOS, it adds -D_DARWIN_C_SOURCE. On Windows, paths and compiler detection change via the Makefile's UNAME_S logic.

For integration steps specific to MongooseOS, the project homepage at mongoose-os.com is the relevant starting point, since the README links there rather than including embedded framework steps.

FFI: importing C functions from JavaScript without glue code

mJS's FFI (Foreign Function Interface) allows a JavaScript script to call an existing C function by declaring its signature as a string. No C wrapper function is needed. The README demonstrates this with a standard math function:

javascript
let floor = ffi('double floor(double)');
print(floor(1.23456));

Calling ffi with a type signature string returns a callable JavaScript function backed by the native C implementation. The FFI trampoline supports up to 6 32-bit arguments, or up to 2 64-bit arguments. Four argument types are supported: int, double, char * and void *. Use char * for NUL-terminated C strings and void * for any other pointer. Functions whose arguments include structs require a C-side wrapper that reduces the struct to supported types before the FFI layer sees it.

Finding C function addresses by name requires a symbol resolver. On POSIX systems, mJS uses dlsym(). On Windows, it uses GetProcAddress(). On embedded targets with no dynamic linker, the resolver must be written manually or generated with help from a firmware linker script. mJS uses a dlsym-compatible signature for its resolver interface.

Callbacks via the userdata marker and per-platform symbol resolution

Passing a JavaScript function to C code as a callback requires the C callback signature to include a void * user data pointer, marked as userdata in the mJS FFI signature. The README demonstrates this pattern with a timer example. The C function is declared as:

C
void timer(int seconds, void (*callback)(int, void *), void *user_data);

On the JavaScript side:

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

Timer.set(200, function(t) {
  print('Time now: ', t);
}, null);

The userdata marker tells the FFI layer where to inject the callback context pointer. C functions that take no user data argument cannot receive mJS callbacks directly under this design. Any C API that uses a callback without a user data slot requires a wrapper that adds that parameter before the FFI layer can use it.

For the symbol resolver on embedded targets, the README notes that a system resolver should be either manually written or implemented with aid from a firmware linker script. On POSIX and Windows, the standard system APIs handle name-to-address resolution automatically.

Byte strings, the built-in API surface and what the standard library omits

String indexing in mJS returns individual bytes, not characters. The at() method returns the numeric byte value at a given index: 'abc'.at(0) === 0x61. The chr(n) function converts an integer to a one-byte string, returning null if n is outside the 0-255 range: chr(0x61) === 'a'.

The complete built-in string API covers slice(start, end), with 'abcdef'.slice(1,3) === 'bc' as the documented example; indexOf(substr) for searching; and at(index) for byte value access. Arrays have splice(start, deleteCount) for in-place modification. The mkstr function creates a string backed by a C memory chunk: mkstr(ptrVar, length) starts at ptrVar, while mkstr(ptrVar, offset, length, copy = false) allows offset-based access with optional copying to the internal mjs buffer.

Global functions include print for stdout output separated by spaces, load('file.js', obj) for executing a second script file with an optional namespace object, die(message) to exit the interpreter, and gc(full) to trigger garbage collection. Passing true to gc(full) reclaims RAM to the OS.

Everything else from the standard JavaScript library is absent. Math object methods, Date, RegExp and String prototype methods beyond the listed ones are not available. Scripts that need those functions must expose them through FFI-imported C functions.

When mJS is the wrong scripting layer, and the GPL-2 license implications

mJS fits one specific architecture: firmware where a C/C++ codebase provides the runtime environment and scripts loaded via load('file.js', obj) provide application logic. That separation is the engine's primary value. For projects where C/C++ logic is already modular and application configuration changes rarely, adding an interpreter layer brings complexity without a corresponding benefit. Pure C firmware compiles all logic together; mJS separates script from firmware at the cost of interpreter overhead and the restrictions that enable that footprint.

The restrictions rule out mJS for any script that needs closures to manage callback state, exceptions to handle error paths, or the standard library for string or number processing. Those features are removed at the design level and are not configurable. Scripts that fit the subset work as valid ES6 in a standard environment, which helps with testing, but the subset boundary is fixed.

On licensing: the README badge shows GPL-2. GitHub classifies the license as Other because the repository LICENSE file does not exactly match a standard SPDX identifier. Anyone distributing mJS or a project that embeds it must read the LICENSE file directly rather than relying on the badge alone.

One tagged release from 2018 and commits through September 2026

Only one versioned release appears in the repository: v1.26, tagged on 2018-02-08. No subsequent release has been tagged since then. For teams that want to pin a specific version, that 2018 tag is the only published reference point, and any commit after it has no corresponding version number.

Despite that release record, the repository is not archived, and the last push was on 2026-09-30. Code has continued to change since 2018 without producing a new release tag. A project embedding mJS at a recent commit cannot reference a version number in a bug report or changelog.

The project is maintained under the Cesanta organization. mJS is part of MongooseOS, and the MongooseOS site at mongoose-os.com connects the engine to a broader firmware ecosystem. Whether the September 2026 activity reflects ongoing MongooseOS maintenance or independent development is not stated in the README.

Editorial conclusion

mJS fits MCU firmware projects running MongooseOS or a custom C environment that needs dynamic JavaScript scripting within tight memory limits. On 32-bit ARM the engine takes about 50k of flash and under 1k of RAM. Skip it when scripts need closures, exceptions, the JavaScript standard library or any ES6 feature outside the documented subset, because those restrictions are fixed by design. Before adopting, read the LICENSE file to confirm the GPL-2 terms apply to your distribution, note that v1.26 from 2018-02-08 is the only tagged release, and verify that the C functions your scripts will call through FFI have signatures the trampoline supports.

Frequently asked questions

What is mJS?

mJS is a restricted JavaScript engine designed for microcontrollers with limited resources. It implements a strict ES6 subset, takes about 50k of flash and under 1k of RAM on 32-bit ARM, and is part of MongooseOS.

What JavaScript features does mJS not support?

mJS removes closures, exceptions, the new keyword, var declarations, arrow functions, for..of loops, generators, proxies, promises, classes, template strings, and the entire standard library. JSON.parse() and JSON.stringify() are the two standard library items that remain.

How does mJS call C functions from JavaScript?

mJS provides an FFI that imports C functions by declaring their type signature as a string using the ffi function. The trampoline supports up to 6 32-bit arguments or up to 2 64-bit arguments, with int, double, char * and void * as the supported argument types.

What license does mJS use?

The README badge shows GPL-2. GitHub classifies the license as Other because the repository LICENSE file does not match a standard identifier, so reading that file directly is necessary before distributing mJS or any project that embeds it.

How do I embed mJS in a C project?

The project distributes two amalgamated files, mjs.c and mjs.h, at the repository root. Adding those files to your C project build system is the embedding path. The repository Makefile builds the standalone binary using C99 with -lm.

Official sources

  1. cesanta/mjs on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cesanta-mjs.svg)](https://hysenlabs.com/projects/cesanta-mjs)