Convert Figma logo to code with AI

cesanta logomjs

Embedded JavaScript engine for C/C++

1,901
172
1,901
180

Top Related Projects

Ultra-lightweight JavaScript engine for the Internet of Things.

The Espruino JavaScript interpreter - Official Repo

MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems

Quick Overview

MJS is a lightweight JavaScript engine designed for embedded systems and IoT devices. It aims to provide a subset of ES6 JavaScript functionality while maintaining a small footprint and efficient execution. MJS is particularly useful for resource-constrained environments where a full-featured JavaScript engine would be impractical.

Pros

  • Small footprint: MJS has a minimal memory and storage requirement, making it suitable for embedded systems.
  • Fast execution: Optimized for performance on low-power devices.
  • Easy integration: Can be easily embedded into C/C++ projects.
  • Subset of ES6: Supports modern JavaScript features while keeping the engine lightweight.

Cons

  • Limited functionality: Does not support all JavaScript features found in full-fledged engines.
  • Smaller ecosystem: Fewer libraries and resources compared to more popular JavaScript engines.
  • Learning curve: Developers need to understand the limitations and specific syntax of MJS.

Code Examples

  1. Basic variable declaration and arithmetic:
let x = 10;
let y = 5;
let result = x + y;
print(result);  // Output: 15
  1. Using functions and conditionals:
let max = function(a, b) {
  if (a > b) {
    return a;
  } else {
    return b;
  }
};

print(max(3, 7));  // Output: 7
  1. Working with arrays:
let arr = [1, 2, 3, 4, 5];
let sum = 0;

for (let i = 0; i < arr.length; i++) {
  sum += arr[i];
}

print(sum);  // Output: 15

Getting Started

To use MJS in your project:

  1. Clone the repository:

    git clone https://github.com/cesanta/mjs.git
    
  2. Include the necessary files in your C project:

    #include "mjs.h"
    
  3. Initialize MJS and run a script:

    struct mjs *mjs = mjs_create();
    mjs_exec(mjs, "print('Hello, MJS!');", NULL);
    mjs_destroy(mjs);
    

For more detailed instructions and API documentation, refer to the project's README and documentation in the repository.

Competitor Comparisons

Ultra-lightweight JavaScript engine for the Internet of Things.

Pros of JerryScript

  • More comprehensive and feature-rich JavaScript engine
  • Better performance and memory efficiency for larger applications
  • Wider community support and active development

Cons of JerryScript

  • Larger codebase and more complex implementation
  • Higher resource requirements for embedded systems
  • Steeper learning curve for integration and customization

Code Comparison

MJS:

#include "mjs.h"

static const char *js_code = "print('Hello, World!');";

int main(void) {
  struct mjs *mjs = mjs_create();
  mjs_exec(mjs, js_code, NULL);
  mjs_destroy(mjs);
  return 0;
}

JerryScript:

#include "jerryscript.h"

static const jerry_char_t script[] = "print('Hello, World!');";

int main(void) {
  jerry_init(JERRY_INIT_EMPTY);
  jerry_value_t ret_value = jerry_eval(script, sizeof(script) - 1, JERRY_PARSE_NO_OPTS);
  jerry_release_value(ret_value);
  jerry_cleanup();
  return 0;
}

Both examples demonstrate a simple "Hello, World!" program. JerryScript's API is more verbose and provides finer control over the execution environment, while MJS offers a more straightforward approach for basic scripting needs.

The Espruino JavaScript interpreter - Official Repo

Pros of Espruino

  • More comprehensive JavaScript implementation, supporting a wider range of ES6+ features
  • Extensive hardware support, including built-in libraries for various sensors and peripherals
  • Active community with regular updates and extensive documentation

Cons of Espruino

  • Larger memory footprint, which may be less suitable for very resource-constrained devices
  • Slower execution speed compared to mjs, especially for complex operations

Code Comparison

Espruino:

var pin = D13;
pinMode(pin, 'output');
setInterval(function() {
  digitalPulse(pin, 1, 500);
}, 1000);

mjs:

let PIN = 13;
GPIO.set_mode(PIN, GPIO.MODE_OUTPUT);
Timer.set(1000, Timer.REPEAT, function() {
  GPIO.toggle(PIN);
}, null);

Summary

Espruino offers a more feature-rich JavaScript environment with broader hardware support, making it ideal for complex IoT projects. mjs, on the other hand, provides a lightweight alternative with faster execution, better suited for simpler applications on resource-constrained devices. The code comparison illustrates the syntax differences, with Espruino using a more JavaScript-like approach, while mjs leans towards a C-style API.

MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems

Pros of MicroPython

  • More comprehensive and feature-rich implementation of Python for microcontrollers
  • Larger community and ecosystem, with extensive documentation and libraries
  • Supports a wider range of hardware platforms and development boards

Cons of MicroPython

  • Larger memory footprint, requiring more resources on the target device
  • Slower execution speed compared to mjs, especially for certain operations
  • More complex codebase, which can make it harder to customize or extend

Code Comparison

MicroPython example:

import machine
import time

led = machine.Pin(2, machine.Pin.OUT)

while True:
    led.value(not led.value())
    time.sleep(1)

mjs example:

let PIN = 2;
let value = 0;

GPIO.set_mode(PIN, GPIO.MODE_OUTPUT);

Timer.set(1000, true, function() {
  value = 1 - value;
  GPIO.write(PIN, value);
}, null);

Both examples demonstrate a simple LED blinking program, showcasing the syntax differences between MicroPython (Python-based) and mjs (JavaScript-based). MicroPython's code is more Pythonic and may be easier for Python developers to understand, while mjs offers a JavaScript-like syntax that might be more familiar to web developers.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

mJS: Restricted JavaScript engine

License

Overview

mJS is designed for microcontrollers with limited resources. Main design goals are: small footprint and simple C/C++ interoperability. mJS implements a strict subset of ES6 (JavaScript version 6):

  • Any valid mJS code is a valid ES6 code.
  • Any valid ES6 code is not necessarily a valid mJS code.

On 32-bit ARM mJS engine takes about 50k of flash memory, and less than 1k of RAM (see intro article). mJS is part of MongooseOS, where it enables scripting for IoT devices.

Restrictions

  • No standard library. No String, Number, RegExp, Date, Function, etc.
  • JSON.parse() and JSON.stringify() are available.
  • No closures, only lexical scoping (i.e. nested functions are allowed).
  • No exceptions.
  • No new. In order to create an object with a custom prototype, use Object.create(), which is available.
  • Strict mode only.
  • No var, only let.
  • No for..of, =>, destructors, generators, proxies, promises.
  • No getters, setters, valueOf, prototypes, classes, template strings.
  • No == or !=, only === and !==.
  • mJS strings are byte strings, not Unicode strings: 'ы'.length === 2, 'ы'[0] === '\xd1', 'ы'[1] === '\x8b'. mJS string can represent any binary data chunk.

Built-in API

print(arg1, arg2, ...);
Print arguments to stdout, separated by space.
load('file.js', obj);
Execute file file.js. obj paramenter is optional. obj is a global namespace object. If not specified, a current global namespace is passed to the script, which allows file.js to modify the current namespace.
die(message);
Exit interpreter with the given error message
let value = JSON.parse(str);
Parse JSON string and return parsed value.
let str = JSON.stringify(value);
Get string representation of the mJS value.
let proto = {foo: 1}; let o = Object.create(proto);
Create an object with the provided prototype.
'some_string'.slice(start, end);
Return a substring between two indices. Example: 'abcdef'.slice(1,3) === 'bc';
'abc'.at(0);
Return numeric byte value at given string index. Example: 'abc'.at(0) === 0x61;
'abc'.indexOf(substr[, fromIndex]);
Return index of first occurence of substr within the string or `-1` if not found. 'abc'.indexOf('bc') === 1;
chr(n);
Return 1-byte string whose ASCII code is the integer `n`. If `n` is not numeric or outside of `0-255` range, `null` is returned. Example: chr(0x61) === 'a';
let a = [1,2,3,4,5]; a.splice(start, deleteCount, ...);
Change the contents of an array by removing existing elements and/or adding new elements. Example: let a = [1,2,3,4,5]; a.splice(1, 2, 100, 101, 102); a === [1,100,101,102,4,5];
let s = mkstr(ptrVar, length);
Create a string backed by a C memory chunk. A string s starts at memory location ptrVar, and is length bytes long.
let s = mkstr(ptrVar, offset, length, copy = false);
Like `mkstr(ptrVar, length)`, but string s starts at memory location ptrVar + offset, and the caller can specify whether the string needs to be copied to the internal mjs buffer. By default it's not copied.
let f = ffi('int foo(int)');
Import C function into mJS. See next section.
gc(full);
Perform garbage collection. If `full` is `true`, reclaim RAM to OS.

C/C++ interoperability

mJS requires no glue code. The mJS's Foreign Function Interface (FFI) allows the user to call an existing C function with an arbitrary signature. Currently mJS provides a simple implementation of the FFI trampoline that supports up to 6 32-bit arguments, or up to 2 64-bit arguments:

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

Function arguments should be simple: only int, double, char *, void * are supported. Use char * for NUL-terminated C strings, void * for any other pointers. In order to import more complex functions (e.g. the ones that use structures as arguments), write wrappers.

Callbacks

Callbacks are implemented similarly. Consider that you have a C function that takes a callback and user data void * pointer, which should be marked as userdata in the signature:

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

This is how to make an mJS callback - note the usage of userdata:

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

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

Symbol resolver

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. mJS resolver uses dlsym-compatible signature.

Converting structs to objects

mJS provides a helper to facilitate coversion of C structs to JS objects. The functions is called s2o and takes two parameters: foreign pointer to the struct and foreign pointer to the struct's descriptor which specifies names and offsets of the struct's members. Here's an simple example:

C/C++ side code:

#include "mjs.h"

struct my_struct {
  int a;
  const char *b;
  double c;
  struct mg_str d;
  struct mg_str *e;
  float f;
  bool g;
};

static const struct mjs_c_struct_member my_struct_descr[] = {
  {"a", offsetof(struct my_struct, a), MJS_STRUCT_FIELD_TYPE_INT, NULL},
  {"b", offsetof(struct my_struct, b), MJS_STRUCT_FIELD_TYPE_CHAR_PTR, NULL},
  {"c", offsetof(struct my_struct, c), MJS_STRUCT_FIELD_TYPE_DOUBLE, NULL},
  {"d", offsetof(struct my_struct, d), MJS_STRUCT_FIELD_TYPE_MG_STR, NULL},
  {"e", offsetof(struct my_struct, e), MJS_STRUCT_FIELD_TYPE_MG_STR_PTR, NULL},
  {"f", offsetof(struct my_struct, f), MJS_STRUCT_FIELD_TYPE_FLOAT, NULL},
  {"g", offsetof(struct my_struct, g), MJS_STRUCT_FIELD_TYPE_BOOL, NULL},
  {NULL, 0, MJS_STRUCT_FIELD_TYPE_INVALID, NULL},
};

const struct mjs_c_struct_member *get_my_struct_descr(void) {
  return my_struct_descr;
};

JS side code:

// Assuming `s` is a foreign pointer to an instance of `my_struct`, obtained elsewhere.
let sd = ffi('void *get_my_struct_descr(void)')();
let o = s2o(s, sd);
print(o.a, o.b);

Nested structs are also supported - use MJS_STRUCT_FIELD_TYPE_STRUCT field type and provide pointer to the definition:

struct my_struct2 {
  int8_t i8;
  int16_t i16;
  uint8_t u8;
  uint16_t u16;
};

static const struct mjs_c_struct_member my_struct2_descr[] = {
  {"i8", offsetof(struct my_struct2, i8), MJS_STRUCT_FIELD_TYPE_INT8, NULL},
  {"i16", offsetof(struct my_struct2, i16), MJS_STRUCT_FIELD_TYPE_INT16, NULL},
  {"u8", offsetof(struct my_struct2, u8), MJS_STRUCT_FIELD_TYPE_UINT8, NULL},
  {"u16", offsetof(struct my_struct2, u16), MJS_STRUCT_FIELD_TYPE_UINT16, NULL},
  {NULL, 0, MJS_STRUCT_FIELD_TYPE_INVALID, NULL},
};

struct my_struct {
  struct my_struct2 s;
  struct my_struct2 *sp;
};

static const struct mjs_c_struct_member my_struct_descr[] = {
  {"s", offsetof(struct my_struct, s), MJS_STRUCT_FIELD_TYPE_STRUCT, my_struct2_descr},
  {"sp", offsetof(struct my_struct, sp), MJS_STRUCT_FIELD_TYPE_STRUCT_PTR, my_struct2_descr},
  {NULL, 0, MJS_STRUCT_FIELD_TYPE_INVALID, NULL},
};

For complicated cases, a custom conversion function can be invoked that returns value:

mjs_val_t custom_value_func(struct mjs *mjs, void *ap) {
  /* Do something with ap, construct and return mjs_val_t */
}

static const struct mjs_c_struct_member my_struct_descr[] = {
  ...
  {"x", offsetof(struct my_struct, x), MJS_STRUCT_FIELD_TYPE_CUSTOM, custom_value_func},
  ...
};

Complete embedding example

We export C function foo to the JS environment and call it from the JS.

#include "strings.h"
#include "mjs.h"

void foo(int x) {
  printf("Hello %d!\n", x);
}

void *my_dlsym(void *handle, const char *name) {
  if (strcmp(name, "foo") == 0) return foo;
  return NULL;
}

int main(void) {
  struct mjs *mjs = mjs_create();
  mjs_set_ffi_resolver(mjs, my_dlsym);
  mjs_exec(mjs, "let f = ffi('void foo(int)'); f(1234)", NULL);
  return 0;
}

Compile & run:

$ cc main.c mjs.c -o /tmp/x && /tmp/x
Hello 1234!

Build stand-alone mJS binary

Build:

$ make

Use as a simple calculator:

$ ./build/mjs -e '1 + 2 * 3'
7

FFI standard C functions:

$ ./build/mjs -e 'ffi("double sin(double)")(1.23)'
0.942489

View generated bytecode:

$ ./build/mjs -l 3 -e '2 + 2'
------- MJS VM DUMP BEGIN
    DATA_STACK (0 elems):
    CALL_STACK (0 elems):
        SCOPES (1 elems):  [<object>]
  LOOP_OFFSETS (0 elems):
  CODE:
  0   BCODE_HDR [<stdin>] size:28
  21  PUSH_INT  2
  23  PUSH_INT  2
  25  EXPR      +
  27  EXIT
  28  NOP
------- MJS VM DUMP END
4

The stand-alone binary uses dlsym() symbol resolver, that's why ffi("double sin(double)")(1.23) works.

Licensing

mJS is released under commercial and GNU GPL v.2 open source licenses.

Commercial Projects: once your project becomes commercialised, GPLv2 licensing dictates that you need to either open your source fully or purchase a commercial license. Cesanta offer full, royalty-free commercial licenses without any GPL restrictions. If your needs require a custom license, we’d be happy to work on a solution with you. Contact us for pricing

Prototyping: While your project is still in prototyping stage and not for sale, you can use MJS’s open source code without license restrictions.