diff --git a/emscripten/.gitignore b/emscripten/.gitignore new file mode 100644 index 0000000000000..d5700888a3a2e --- /dev/null +++ b/emscripten/.gitignore @@ -0,0 +1,2 @@ +node_modules/ + diff --git a/emscripten/Makefile b/emscripten/Makefile new file mode 100644 index 0000000000000..86b79afc3f27e --- /dev/null +++ b/emscripten/Makefile @@ -0,0 +1,65 @@ +include ../py/mkenv.mk + +# clang has slightly different options to GCC +CLANG = 1 +EMSCRIPTEN = 1 + +# qstr definitions (must come before including py.mk) +QSTR_DEFS = qstrdefsport.h + +# include py core make definitions +include ../py/py.mk + +CC = emcc -s RESERVED_FUNCTION_POINTERS=20 +CPP = gcc -E +CLANG = 1 +SIZE = echo +LD = emcc -s RESERVED_FUNCTION_POINTERS=20 + +INC += -I. +INC += -I.. +INC += -I$(BUILD) + +CFLAGS = $(INC) -Wall -Werror -ansi -std=gnu99 $(COPT) + +#Debugging/Optimization +ifeq ($(DEBUG), 1) +CFLAGS += -O0 +CC += -g4 +LD += -g4 +else +CFLAGS += -Os -DNDEBUG +endif + +CFLAGS += -D MICROPY_NLR_SETJMP=1 +CFLAGS += -D MICROPY_USE_INTERNAL_PRINTF=0 + +LD = $(CC) +LDFLAGS = -Wl,-map,$@.map -Wl,-dead_strip -Wl,-no_pie -s EXPORTED_FUNCTIONS="['_mp_js_init', '_mp_js_run']" + +LIBS = + +SRC_C = \ + main.c \ + stdio_core.c \ + lib/utils/stdout_helpers.c \ + lib/utils/pyexec.c \ + lib/mp-readline/readline.c \ + $(BUILD)/_frozen_mpy.c \ + +OBJ = $(PY_O) $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) + +all: $(BUILD)/micropython.js + +$(BUILD)/_frozen_mpy.c: frozentest.mpy $(BUILD)/genhdr/qstrdefs.generated.h + $(ECHO) "MISC freezing bytecode" + $(Q)../tools/mpy-tool.py -f -q $(BUILD)/genhdr/qstrdefs.preprocessed.h -mlongint-impl=none $< > $@ + +$(BUILD)/micropython.js: $(OBJ) + $(ECHO) "LINK $@" + $(Q)$(LD) $(LDFLAGS) -o $@ $^ $(LIBS) + +run: + node repl.js + +include ../py/mkrules.mk diff --git a/emscripten/README.md b/emscripten/README.md new file mode 100644 index 0000000000000..08a65cb77064e --- /dev/null +++ b/emscripten/README.md @@ -0,0 +1,13 @@ +# MicroPython on Emscripten + +Follow the instructions for getting started with Emscripten [here](http://kripken.github.io/emscripten-site/docs/getting_started/downloads.html). + +Then you can run + +```bash +source ./emsdk_env.sh +cd {micropython directory}/emscripten +make EMSCRIPTEN=1 -j +node build/firmware.js +``` + diff --git a/emscripten/frozentest.mpy b/emscripten/frozentest.mpy new file mode 100644 index 0000000000000..c8345b1910611 Binary files /dev/null and b/emscripten/frozentest.mpy differ diff --git a/emscripten/frozentest.py b/emscripten/frozentest.py new file mode 100644 index 0000000000000..0f99b74297fbb --- /dev/null +++ b/emscripten/frozentest.py @@ -0,0 +1,7 @@ +print('uPy') +print('a long string that is not interned') +print('a string that has unicode αβγ chars') +print(b'bytes 1234\x01') +print(123456789) +for i in range(4): + print(i) diff --git a/emscripten/main.c b/emscripten/main.c new file mode 100644 index 0000000000000..a466a8e8693c1 --- /dev/null +++ b/emscripten/main.c @@ -0,0 +1,174 @@ +#include +#include +#include +#include +#include + +#include "py/nlr.h" +#include "py/compile.h" +#include "py/runtime.h" +#include "py/repl.h" +#include "py/gc.h" +#include "lib/utils/pyexec.h" + +#include "emscripten.h" + +// TODO: make this work properly with emscripten +#ifdef _WIN32 +#define PATHLIST_SEP_CHAR ';' +#else +#define PATHLIST_SEP_CHAR ':' +#endif + +void do_str(const char *src, mp_parse_input_kind_t input_kind) { + mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0); + if (lex == NULL) { + printf("MemoryError: lexer could not allocate memory\n"); + return; + } + + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + qstr source_name = lex->source_name; + mp_parse_tree_t parse_tree = mp_parse(lex, input_kind); + mp_obj_t module_fun = mp_compile(&parse_tree, source_name, MP_EMIT_OPT_NONE, true); + mp_call_function_0(module_fun); + nlr_pop(); + } else { + // uncaught exception + mp_obj_print_exception(&mp_plat_print, (mp_obj_t)nlr.ret_val); + } +} + +static char *stack_top; + +#if MICROPY_ENABLE_GC +static char heap[2048]; +#endif + +struct mp_js_context_t { + int(*import_stat) (const char *); + const char * (*read_file) (const char *); +}; + +static struct mp_js_context_t mp_js_context = { + .import_stat = NULL, + .read_file = NULL +}; + +void mp_js_init(int(*import_stat) (const char *), const char * (*read_file) (const char *)) { + gc_init(heap, heap + sizeof(heap)); + mp_init(); + + char *home = getenv("HOME"); + char *path = getenv("MICROPYPATH"); + if (path == NULL) { + #ifdef MICROPY_PY_SYS_PATH_DEFAULT + path = MICROPY_PY_SYS_PATH_DEFAULT; + #else + path = "~/.micropython/lib:/usr/lib/micropython"; + #endif + } + mp_uint_t path_num = 1; // [0] is for current dir (or base dir of the script) + for (char *p = path; p != NULL; p = strchr(p, PATHLIST_SEP_CHAR)) { + path_num++; + if (p != NULL) { + p++; + } + } + mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_path), path_num); + mp_obj_t *path_items; + mp_obj_list_get(mp_sys_path, &path_num, &path_items); + path_items[0] = MP_OBJ_NEW_QSTR(MP_QSTR_); + + { + char *p = path; + for (mp_uint_t i = 1; i < path_num; i++) { + char *p1 = strchr(p, PATHLIST_SEP_CHAR); + if (p1 == NULL) { + p1 = p + strlen(p); + } + if (p[0] == '~' && p[1] == '/' && home != NULL) { + // Expand standalone ~ to $HOME + int home_l = strlen(home); + vstr_t vstr; + vstr_init(&vstr, home_l + (p1 - p - 1) + 1); + vstr_add_strn(&vstr, home, home_l); + vstr_add_strn(&vstr, p + 1, p1 - p - 1); + path_items[i] = mp_obj_new_str_from_vstr(&mp_type_str, &vstr); + } else { + path_items[i] = MP_OBJ_NEW_QSTR(qstr_from_strn(p, p1 - p)); + } + p = p1 + 1; + } + } + + // register file stat and file load functions from JS-side + mp_js_context.import_stat = import_stat; + mp_js_context.read_file = read_file; +} + +void mp_js_run(const char * code) { + do_str(code, MP_PARSE_FILE_INPUT); +} + +void gc_collect(void) { + // WARNING: This gc_collect implementation doesn't try to get root + // pointers from CPU registers, and thus may function incorrectly. + void *dummy; + gc_collect_start(); + gc_collect_root(&dummy, ((mp_uint_t)stack_top - (mp_uint_t)&dummy) / sizeof(mp_uint_t)); + gc_collect_end(); + gc_dump_info(); +} + +mp_lexer_t *mp_lexer_new_from_file(const char *filename) { + if (mp_js_context.read_file == NULL) { + return NULL; + } + + const char * code_buf = mp_js_context.read_file(filename); + + if (code_buf == NULL) { + return NULL; + } + + mp_lexer_t* lex = mp_lexer_new_from_str_len(qstr_from_str(filename), code_buf, strlen(code_buf), 0); + return lex; +} + +mp_import_stat_t mp_import_stat(const char *path) { + if (mp_js_context.import_stat == NULL) { + return MP_IMPORT_STAT_NO_EXIST; + } + + int s = mp_js_context.import_stat(path); + + if (s == -1) { + return MP_IMPORT_STAT_NO_EXIST; + } else if (s == 1) { + return MP_IMPORT_STAT_DIR; + } else { + return MP_IMPORT_STAT_FILE; + } +} + +mp_obj_t mp_builtin_open(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) { + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_open_obj, 1, mp_builtin_open); + +void nlr_jump_fail(void *val) { +} + +void NORETURN __fatal_error(const char *msg) { + while (1); +} + +#ifndef NDEBUG +void MP_WEAK __assert_func(const char *file, int line, const char *func, const char *expr) { + printf("Assertion '%s' failed, at file %s:%d\n", expr, file, line); + __fatal_error("Assertion failed"); +} +#endif + diff --git a/emscripten/main.js b/emscripten/main.js new file mode 100644 index 0000000000000..e91333da1f55b --- /dev/null +++ b/emscripten/main.js @@ -0,0 +1,32 @@ + +const mpy = require('./build/micropython.js'); + +module.exports.run = function(code) { + mpy.ccall('mp_js_run', 'null', ['string'], [code]); +} + +module.exports.init = function(opts) { + var import_wrapped = function(file_ptr) { + var filename = mpy.Pointer_stringify(file_ptr); + + if (!opts.import_stat != null) { + return opts.import_stat(filename); + } else { + return 0; + } + } + + var read_wrapped = function(file_ptr) { + var filename = mpy.Pointer_stringify(file_ptr); + + if (opts.read_file != null) { + return opts.read_file(filename); + } else { + return 0; + } + } + + mpy.ccall('mp_js_init', 'null', ['int', 'int'], [mpy.Runtime.addFunction(import_wrapped), mpy.Runtime.addFunction(read_wrapped)]); +} + +module.exports.Module = mpy; diff --git a/emscripten/mpconfigport.h b/emscripten/mpconfigport.h new file mode 100644 index 0000000000000..8812c9b4dd891 --- /dev/null +++ b/emscripten/mpconfigport.h @@ -0,0 +1,98 @@ +#include + +// options to control how Micro Python is built + +#define MICROPY_QSTR_BYTES_IN_HASH (1) +#define MICROPY_QSTR_EXTRA_POOL mp_qstr_frozen_const_pool +#define MICROPY_ALLOC_PATH_MAX (256) +#define MICROPY_ALLOC_PARSE_CHUNK_INIT (16) +#define MICROPY_EMIT_X64 (0) +#define MICROPY_EMIT_THUMB (0) +#define MICROPY_EMIT_INLINE_THUMB (0) +#define MICROPY_COMP_MODULE_CONST (0) +#define MICROPY_COMP_CONST (0) +#define MICROPY_COMP_DOUBLE_TUPLE_ASSIGN (0) +#define MICROPY_COMP_TRIPLE_TUPLE_ASSIGN (0) +#define MICROPY_MEM_STATS (0) +#define MICROPY_DEBUG_PRINTERS (0) +#define MICROPY_ENABLE_GC (1) +#define MICROPY_GC_ALLOC_THRESHOLD (0) +#define MICROPY_REPL_EVENT_DRIVEN (0) +#define MICROPY_HELPER_REPL (1) +#define MICROPY_HELPER_LEXER_UNIX (0) +#define MICROPY_ENABLE_SOURCE_LINE (0) +#define MICROPY_ENABLE_DOC_STRING (0) +#define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_TERSE) +#define MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG (0) +#define MICROPY_PY_ASYNC_AWAIT (0) +#define MICROPY_PY_BUILTINS_BYTEARRAY (0) +#define MICROPY_PY_BUILTINS_MEMORYVIEW (0) +#define MICROPY_PY_BUILTINS_ENUMERATE (0) +#define MICROPY_PY_BUILTINS_FILTER (0) +#define MICROPY_PY_BUILTINS_FROZENSET (0) +#define MICROPY_PY_BUILTINS_REVERSED (0) +#define MICROPY_PY_BUILTINS_SET (0) +#define MICROPY_PY_BUILTINS_SLICE (0) +#define MICROPY_PY_BUILTINS_PROPERTY (0) +#define MICROPY_PY_BUILTINS_MIN_MAX (0) +#define MICROPY_PY___FILE__ (0) +#define MICROPY_PY_GC (0) +#define MICROPY_PY_ARRAY (0) +#define MICROPY_PY_ATTRTUPLE (0) +#define MICROPY_PY_COLLECTIONS (0) +#define MICROPY_PY_MATH (0) +#define MICROPY_PY_CMATH (0) +#define MICROPY_PY_IO (0) +#define MICROPY_PY_STRUCT (0) +#define MICROPY_PY_SYS (0) +#define MICROPY_MODULE_FROZEN_MPY (1) +#define MICROPY_CPYTHON_COMPAT (0) +#define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_NONE) +#define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_NONE) + +// type definitions for the specific machine + +#define BYTES_PER_WORD (4) + +#define MICROPY_MAKE_POINTER_CALLABLE(p) ((void*)((mp_uint_t)(p) | 1)) + +// This port is intended to be 32-bit, but unfortunately, int32_t for +// different targets may be defined in different ways - either as int +// or as long. This requires different printf formatting specifiers +// to print such value. So, we avoid int32_t and use int directly. +#define UINT_FMT "%u" +#define INT_FMT "%d" +typedef int mp_int_t; // must be pointer size +typedef unsigned mp_uint_t; // must be pointer size + +typedef long mp_off_t; + +#define MP_PLAT_PRINT_STRN(str, len) mp_hal_stdout_tx_strn_cooked(str, len) + +// extra built in names to add to the global namespace +#define MICROPY_PORT_BUILTINS \ + { MP_OBJ_NEW_QSTR(MP_QSTR_open), (mp_obj_t)&mp_builtin_open_obj }, + +// We need to provide a declaration/definition of alloca() +#include + +#define MICROPY_HW_BOARD_NAME "emscripten" +#define MICROPY_HW_MCU_NAME "asmjs" + +#ifdef __linux__ +#define MICROPY_MIN_USE_STDOUT (1) +#endif + +#ifdef __APPLE__ +#define MICROPY_MIN_USE_STDOUT (1) +#endif + +#ifdef __thumb__ +#define MICROPY_MIN_USE_CORTEX_CPU (1) +#define MICROPY_MIN_USE_STM32_MCU (1) +#endif + +#define MP_STATE_PORT MP_STATE_VM + +#define MICROPY_PORT_ROOT_POINTERS \ + const char *readline_hist[8]; diff --git a/emscripten/mphalport.h b/emscripten/mphalport.h new file mode 100644 index 0000000000000..60d68bd2d6d50 --- /dev/null +++ b/emscripten/mphalport.h @@ -0,0 +1,2 @@ +static inline mp_uint_t mp_hal_ticks_ms(void) { return 0; } +static inline void mp_hal_set_interrupt_char(char c) {} diff --git a/emscripten/package.json b/emscripten/package.json new file mode 100644 index 0000000000000..be2c30881bc35 --- /dev/null +++ b/emscripten/package.json @@ -0,0 +1,28 @@ +{ + "name": "micropython", + "version": "1.8.6", + "description": "micropython compiled to ASM.js with emscripten", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/micropython/micropython.git" + }, + "keywords": [ + "python", + "emscripten", + "repl" + ], + "bin": "./server.js", + "author": "Matthew Else ", + "license": "MIT", + "bugs": { + "url": "https://github.com/micropython/micropython/issues" + }, + "homepage": "https://github.com/micropython/micropython#readme", + "dependencies": { + "minimist": "^1.2.0" + } +} diff --git a/emscripten/qstrdefsport.h b/emscripten/qstrdefsport.h new file mode 100644 index 0000000000000..b9cbb1c649450 --- /dev/null +++ b/emscripten/qstrdefsport.h @@ -0,0 +1,8 @@ +// qstrs specific to this port +Q(file) +Q(mode) +Q(encoding) +Q(r) +Q(buffering) +Q(heap_lock) +Q(heap_unlock) diff --git a/emscripten/server.js b/emscripten/server.js new file mode 100755 index 0000000000000..5c65fe624831c --- /dev/null +++ b/emscripten/server.js @@ -0,0 +1,50 @@ +#!/usr/bin/env node + +var repl = require('repl'); +var mpy = require('./main.js'); +var argv = require('minimist')(process.argv.slice(2)); +var fs = require('fs'); + +function mpy_eval(cmd, context, filename, callback) { + callback(null, mpy.run(cmd)); +} + +mpy.init({ + import_stat: function(filename) { + try { + var stat = fs.statSync(filename); + + if (stat.isDirectory()) { + return 1; + } else if (stat.isFile()) { + return 0; + } + } catch (e) { + return -1; + } + + return -1; + }, + read_file: function(filename) { + var content = fs.readFileSync(filename, 'utf-8'); + var ptr = mpy.Module.allocate(mpy.Module.intArrayFromString(content), 'i8', mpy.Module.ALLOC_STACK); + + return ptr; + } +}); + +if (argv._.length == 1) { + var code = fs.readFileSync(argv._[0], "utf-8"); + mpy.run(code); +} else if (argv.c) { + var code = argv.c; + mpy.run(code); +} else if (argv.m) { + var code = 'import ' + argv.m; + mpy.run(code); +} else if (argv._.length == 0) { + console.log("MicroPython on asmjs with emscripten"); + console.log("Type \"help()\" for more information."); + repl.start({prompt: '> ', eval: mpy_eval}); +} + diff --git a/emscripten/stdio_core.c b/emscripten/stdio_core.c new file mode 100644 index 0000000000000..1733d1e2631d8 --- /dev/null +++ b/emscripten/stdio_core.c @@ -0,0 +1,14 @@ +#include +#include "stdio.h" +#include "py/mpconfig.h" + +// Receive single character +int mp_hal_stdin_rx_chr(void) { + unsigned char c = fgetc(stdin); + return c; +} + +// Send string of given length +void mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) { + fwrite(str, len, 1, stdout); +} diff --git a/py/py.mk b/py/py.mk index c71ab7a0c8d7b..9ba133521d399 100644 --- a/py/py.mk +++ b/py/py.mk @@ -101,11 +101,18 @@ endif # py object files PY_O_BASENAME = \ mpstate.o \ - nlrx86.o \ + +# these object files can only be built if we're not using emscripten, since +# the input files necessary to build them are ASM files. +ifneq ($(EMSCRIPTEN), 1) +PY_O_BASENAME += nlrx86.o \ nlrx64.o \ nlrthumb.o \ nlrxtensa.o \ - nlrsetjmp.o \ + +endif + +PY_O_BASENAME += nlrsetjmp.o \ malloc.o \ gc.o \ qstr.o \