# Python wrapper for emscripten_* C functions # Copyright (C) 2018, 2019, 2020 Sylvain Beucler # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. This file is offered as-is, # without any warranty. # http://docs.cython.org/en/latest/src/tutorial/strings.html#auto-encoding-and-decoding # Most of our strings are converted from/to JS through emscripten stringToUTF8/UTF8ToString # cython: c_string_type=unicode, c_string_encoding=utf8 # Note: Py->C auto-UTF-8 (instead of .encode('UTF-8')) not supported for Py2 # Note: causes issues with Typed Memoryviews from __future__ import print_function cdef extern from "emscripten.h": ctypedef void (*em_callback_func)() ctypedef void (*em_arg_callback_func)(void*) ctypedef void (*em_async_wget_onload_func)(void*, void*, int) void emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop) void emscripten_set_main_loop_arg(em_arg_callback_func func, void *arg, int fps, int simulate_infinite_loop) void emscripten_cancel_main_loop() void emscripten_exit_with_live_runtime() void emscripten_run_script(const char *script) int emscripten_run_script_int(const char *script) char *emscripten_run_script_string(const char *script) #void emscripten_async_wget(const char* url, const char* file, em_str_callback_func onload, em_str_callback_func onerror) void emscripten_async_wget_data(const char* url, void *arg, em_async_wget_onload_func onload, em_arg_callback_func onerror) void emscripten_async_call(em_arg_callback_func func, void *arg, int millis) void emscripten_sleep(unsigned int ms) void emscripten_wget(const char* url, const char* file) void emscripten_wget_data(const char* url, void** pbuffer, int* pnum, int *perror) # Emterpreter-only #void emscripten_sleep_with_yield(unsigned int ms) enum: EM_LOG_CONSOLE EM_LOG_WARN EM_LOG_ERROR EM_LOG_C_STACK EM_LOG_JS_STACK EM_LOG_DEMANGLE EM_LOG_NO_PATHS EM_LOG_FUNC_PARAMS int emscripten_get_compiler_setting(const char *name) void emscripten_debugger() void emscripten_log(int flags, const char* format, ...) int emscripten_get_callstack(int flags, char *out, int maxbytes) LOG_CONSOLE = EM_LOG_CONSOLE LOG_WARN = EM_LOG_WARN LOG_ERROR = EM_LOG_ERROR LOG_C_STACK = EM_LOG_C_STACK LOG_JS_STACK = EM_LOG_JS_STACK LOG_DEMANGLE = EM_LOG_DEMANGLE LOG_NO_PATHS = EM_LOG_NO_PATHS LOG_FUNC_PARAMS = EM_LOG_FUNC_PARAMS # https://cython.readthedocs.io/en/latest/src/tutorial/memory_allocation.html from libc.stdlib cimport malloc, free from cpython.mem cimport PyMem_Malloc, PyMem_Free # https://github.com/cython/cython/wiki/FAQ#what-is-the-difference-between-pyobject-and-object from cpython.ref cimport PyObject, Py_XINCREF, Py_XDECREF from cpython.buffer cimport PyBuffer_FillInfo #cdef extern from "stdio.h": # int puts(const char *s); # C callback - no memory management # Take a single Python object and calls it # Kept for documentation cdef void callpyfunc(void *py_function): # not necessary as we're using a no-threading Python #PyEval_InitThreads() # Call Python function from C using ()() f = py_function f() # C callbacks - memory management cdef struct pycaller: PyObject* py_function PyObject* py_arg # can be: set, None or NULL cdef pycaller* pycaller_create(PyObject* py_function, PyObject* py_arg): cdef pycaller* c = PyMem_Malloc(sizeof(pycaller)) c.py_function = py_function c.py_arg = py_arg Py_XINCREF(c.py_function) if c.py_arg != NULL: Py_XINCREF(c.py_arg) return c cdef void pycaller_free(pycaller *c): if c.py_arg != NULL: Py_XDECREF(c.py_arg) Py_XDECREF(c.py_function) PyMem_Free(c) # Take a Python object and calls it ONCE on passed argument # C callback for e.g. emscripten_async_call cdef void pycaller_callback_once(void* p): pycaller_callback_recurring(p) pycaller_free(p) # Take a Python object and calls it on passed argument # C callback for e.g. emscripten_set_main_loop_arg cdef void pycaller_callback_recurring(void* p): cdef pycaller* c = p py_function = (c.py_function) if c.py_arg != NULL: py_arg = (c.py_arg) py_function(py_arg) else: py_function() cdef pycaller* main_loop = NULL def set_main_loop_arg(py_function, py_arg, fps, simulate_infinite_loop): set_main_loop_arg_c(py_function, py_arg, fps, simulate_infinite_loop) def set_main_loop(py_function, fps, simulate_infinite_loop): set_main_loop_arg_c(py_function, NULL, fps, simulate_infinite_loop) # handle py_arg == NULL != None cdef set_main_loop_arg_c(PyObject* py_function, PyObject* py_arg, fps, simulate_infinite_loop): global main_loop if main_loop == NULL: main_loop = pycaller_create(py_function, py_arg) else: pass # invalid, let emscripten_set_main_loop_arg() abort emscripten_set_main_loop_arg(pycaller_callback_recurring, main_loop, fps, simulate_infinite_loop) def cancel_main_loop(): global main_loop emscripten_cancel_main_loop() if main_loop != NULL: pycaller_free(main_loop) main_loop = NULL # import emscripten,sys # emscripten.set_main_loop(lambda: sys.stdout.write("main_loop\n"), 2, 0) # emscripten.cancel_main_loop() # emscripten.set_main_loop(lambda: sys.stdout.write("main_loop\n"), -1, 1) # emscripten.set_main_loop_arg(lambda a: sys.stdout.write(a), "main_loop_arg\n", 2, 0) def async_call(py_function, py_arg, millis): cdef pycaller* c = pycaller_create(py_function, py_arg) emscripten_async_call(pycaller_callback_once, c, millis) # emscripten.async_call(lambda a: sys.stdout.write(a), "async_call_arg\n", 1000) def exit_with_live_runtime(): emscripten_exit_with_live_runtime(); def sleep(ms): emscripten_sleep(ms) # Emterpreter-only #def sleep_with_yield(ms): # emscripten_sleep_with_yield(ms) def run_script(script): emscripten_run_script(script.encode('UTF-8')); def run_script_int(script): return emscripten_run_script_int(script.encode('UTF-8')); def run_script_string(script): return emscripten_run_script_string(script.encode('UTF-8')); # async_wget # Requires a C function without parameter, while we need to set # callpyfunc_arg as callback (so we can call a Python function) # Perhaps doable if we maintain a list of Python callbacks indexed by 'file' (and ignore potential conflict) # Or dynamically generate C callbacks in WebAssembly but I doubt that's simple. # Or implement it with async_wget_data + write output file manually (implies an additional copy) #def async_wget(url, file, onload, onerror): # pass cdef class pycaller_async_wget: cdef onload cdef onerror cdef arg def __cinit__(self, onload, onerror, arg): self.onload = onload self.onerror = onerror self.arg = arg #def __dealloc__(self): # print("dealloc") cdef void pycaller_callback_async_wget_onload(void* p, void* buf, int size): c = p # https://cython.readthedocs.io/en/latest/src/tutorial/strings.html#passing-byte-strings py_buf = (buf)[:size] # copy c.onload(c.arg, py_buf) Py_XDECREF(p) # 'buf' freed right after by emscripten cdef void pycaller_callback_async_wget_onerror(void* p): c = p if c.onerror is not None: c.onerror(c.arg) Py_XDECREF(p) def async_wget_data(url, arg, onload, onerror=None): cdef pycaller_async_wget c = pycaller_async_wget(onload, onerror, arg) cdef PyObject* p = c Py_XINCREF(p) # survive until callback emscripten_async_wget_data(url.encode('UTF-8'), p, pycaller_callback_async_wget_onload, pycaller_callback_async_wget_onerror) # emscripten.async_wget_data('/', {'a':1}, lambda arg,buf: sys.stdout.write(repr(arg)+"\n"+repr(buf)+"\n"), lambda arg: sys.stdout.write(repr(arg)+"\nd/l error\n")) # emscripten.async_wget_data('https://bank.confidential/', None, None, lambda arg: sys.stdout.write("d/l error\n")) # emscripten.async_wget_data('https://bank.confidential/', None, None) # requires -s RETAIN_COMPILER_SETTINGS=1 (otherwise Exception) def get_compiler_setting(name): cdef void* amb = emscripten_get_compiler_setting(name.encode('UTF-8')) # can be int or char*, use heuristic # otherwise we could whitelist all known string parameters, if that's possible if amb < 1000: return amb else: return amb # c_string_encoding # emscripten.get_compiler_setting('EMULATE_FUNCTION_POINTER_CASTS') # 1 # emscripten.get_compiler_setting('OPT_LEVEL') # 3 # emscripten.get_compiler_setting('EMSCRIPTEN_VERSION') # u'1.39.0' # emscripten.get_compiler_setting('non-existent') # u'invalid compiler setting: non-existent' # emscripten.get_compiler_setting('EXPORTED_FUNCTIONS') # u'invalid compiler setting: EXPORTED_FUNCTIONS' # :( def debugger(): emscripten_debugger() # open the JavaScript console # emscripten.debugger() def log(flags, fmt, *args): # No variadic function support in Cython? # No va_arg variant for emscripten_log either. # Let's offer limited support cdef char* cstr cdef char* cformat pystrfmt = fmt.encode('UTF-8') cformat = pystrfmt if len(args) == 0: emscripten_log(flags, cformat) elif len(args) > 0: if len(args) == 1: arg = args[0] if type(arg) == int: emscripten_log(flags, cformat, arg) elif type(arg) == float: emscripten_log(flags, cformat, arg) elif type(arg) in (str, unicode): pystr = arg.encode('UTF-8') cstr = pystr emscripten_log(flags, cformat, cstr) else: pystr = ("emscripten.log: unsupported argument " + str(type(arg))).encode('UTF-8') cstr = pystr emscripten_log(flags, cstr) else: emscripten_log(flags, "emscripten.log: only up to 2 arguments are supported") # import emscripten; emscripten.log(0, "hello %02d", 1) # import emscripten; emscripten.log(emscripten.LOG_WARN|emscripten.LOG_CONSOLE|emscripten.LOG_C_STACK, "warning!") # emscripten_log doesn't to properly support UTF-8 # import emscripten; emscripten.log(0, u"é") # import emscripten; emscripten.log(0, "%s", u"é") def get_callstack(flags): cdef int size = emscripten_get_callstack(flags, NULL, 0) # "subsequent calls will carry different line numbers, so it is # best to allocate a few bytes extra to be safe" size += 1024 cdef char* buf = PyMem_Malloc(size) emscripten_get_callstack(flags, buf, size) cdef object ret = buf # c_string_encoding PyMem_Free(buf) return ret # from emscripten import * # print(get_callstack(0)) # print(get_callstack(LOG_C_STACK|LOG_JS_STACK|LOG_DEMANGLE|LOG_NO_PATHS|LOG_FUNC_PARAMS)) # Pseudo-synchronous, requires ASYNCIFY def wget(url, file): return emscripten_wget(url.encode('UTF-8'), file.encode('UTF-8')) # emscripten.wget('/hello', '/hello'); open('/hello','rb').read() # Notes: # - FS error if file already exists # - Download indicator showing up not going away # - Download progress bar showing up not going away on error # Wrap a malloc'd buffer with buffer interface and automatic free() cdef class MallocBuffer: cdef char *buf cdef int size def __init__(self): raise Exception("MallocBuffer: constructor not available from Python") # constructor from non-Python parameters (__cinit__ don't accept them) @staticmethod cdef MallocBuffer from_string_and_size(char* buf, int size): cdef MallocBuffer ret = MallocBuffer.__new__(MallocBuffer) ret.buf = buf ret.size = size return ret def __dealloc__(self): free(self.buf) def __getbuffer__(self, Py_buffer *view, int flags): is_readonly = 0 PyBuffer_FillInfo(view, self, self.buf, self.size, is_readonly, flags) def __releasebuffer__(self, Py_buffer *view): pass # Pseudo-synchronous, requires ASYNCIFY def wget_data(url): cdef char* buf cdef int num, error emscripten_wget_data(url.encode('UTF-8'), &buf, &num, &error) if error != 0: return None pybuf = MallocBuffer.from_string_and_size(buf, num) return pybuf # import emscripten,cStringIO; r = emscripten.wget_data('/hello'); cStringIO.StringIO(r).read(); memoryview(r).tobytes() # Non-API utility def syncfs(): emscripten_run_script(r""" FS.syncfs(false, function(err) { if (err) { console.trace(); console.log(err, err.message); Module.print("Warning: write error: " + err.message + "\n"); } }); """);