Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/cesanta/mjs
Embedded JavaScript engine for C/C++
https://github.com/cesanta/mjs
embedded esp32 esp8266 javascript js mcu
Last synced: 1 day ago
JSON representation
Embedded JavaScript engine for C/C++
- Host: GitHub
- URL: https://github.com/cesanta/mjs
- Owner: cesanta
- License: other
- Created: 2016-12-21T14:11:16.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2024-08-01T03:24:11.000Z (4 months ago)
- Last Synced: 2024-11-27T23:02:40.829Z (15 days ago)
- Topics: embedded, esp32, esp8266, javascript, js, mcu
- Language: C
- Homepage: https://mongoose-os.com
- Size: 5.46 MB
- Stars: 1,921
- Watchers: 74
- Forks: 176
- Open Issues: 191
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-mongoose-os - mJS - Embedded JavaScript engine for C/C++ (Awesome Mongoose OS [![Awesome](https://awesome.re/badge.svg)](https://awesome.re) / Community Tutorials)
- awesome-list - mjs
- AwesomeCppGameDev - mjs
- awesome-embedded - mJS from Mongoose OS
README
mJS: Restricted JavaScript engine
====================================[![License](https://img.shields.io/badge/license-GPL_2-green.svg)](https://github.com/cesanta/mjs/blob/master/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](https://mongoose-os.com/blog/mjs-a-new-approach-to-embedded-scripting/)).
mJS is part of [MongooseOS](https://mongoose-os.com),
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:
```javascript
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:
```C
void timer(int seconds, void (*callback)(int, void *), void *user_data);
```
This is how to make an mJS callback - note the usage of `userdata`:
```javascript
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:
```c
#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:
```js
// 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:
```c
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:
```c
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.
```c
#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): []
LOOP_OFFSETS (0 elems):
CODE:
0 BCODE_HDR [] 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](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
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](https://mongoose-os.com/contact.html)
Prototyping: While your project is still in prototyping stage and not for sale,
you can use MJS’s open source code without license restrictions.