Module hat.duktape

Python ctypes wrapper for duktape JavaScript interpreter

Expand source code
"""Python ctypes wrapper for duktape JavaScript interpreter"""

from pathlib import Path
import ctypes
import sys


class Interpreter:
    """JavaScript interpreter

    High-level python wrapper to duktape JavaScript interpreter.

    Current implementation caches all function objects for preventing
    garbage collection.

    """

    def __init__(self):

        @_lib.duk_fatal_function
        def fatal_handler(udata, msg):
            try:
                print(f"duktape fatal error: {msg.decode('utf-8')}")
            finally:
                sys.exit(1)

        self._ctx = _lib.duk_create_heap(None, None, None, None, fatal_handler)
        self._fns = [fatal_handler]

    def __del__(self):
        _lib.duk_destroy_heap(self._ctx)
        self._fns = []

    def eval(self, code):
        """Evaluate JS code

        Args:
            code (str): JS code

        Returns:
            Any

        """
        top = _lib.duk_get_top(self._ctx)
        try:
            _lib.duk_eval_string(self._ctx, code.encode('utf-8'))
            return self._peek(-1)
        finally:
            _lib.duk_set_top(self._ctx, top)

    def get(self, name):
        """Get global value

        Args:
            name (str): global name

        Returns:
            Any

        """
        top = _lib.duk_get_top(self._ctx)
        try:
            key = name.encode('utf-8')
            _lib.duk_get_global_string(self._ctx, key)
            return self._peek(-1)
        finally:
            _lib.duk_set_top(self._ctx, top)

    def set(self, name, value):
        """Set global value

        Args:
            name (str): global name
            value (Any): value

        """
        top = _lib.duk_get_top(self._ctx)
        try:
            self._push(value)
            key = name.encode('utf-8')
            _lib.duk_put_global_string(self._ctx, key)
        finally:
            _lib.duk_set_top(self._ctx, top)

    def _peek(self, idx):
        if _lib.duk_is_null_or_undefined(self._ctx, idx):
            return
        if _lib.duk_is_boolean(self._ctx, idx):
            return self._peek_boolean(idx)
        if _lib.duk_is_number(self._ctx, idx):
            return self._peek_number(idx)
        if _lib.duk_is_string(self._ctx, idx):
            return self._peek_string(idx)
        if _lib.duk_is_array(self._ctx, idx):
            return self._peek_array(idx)
        if _lib.duk_is_function(self._ctx, idx):
            return self._peek_function(idx)
        if _lib.duk_is_object(self._ctx, idx):
            return self._peek_object(idx)
        raise ValueError()

    def _peek_boolean(self, idx):
        return bool(_lib.duk_get_boolean(self._ctx, idx))

    def _peek_number(self, idx):
        return _lib.duk_get_number(self._ctx, idx)

    def _peek_string(self, idx):
        return _lib.duk_get_string(self._ctx, idx).decode('utf-8')

    def _peek_array(self, idx):
        result = []
        top = _lib.duk_get_top(self._ctx)
        _lib.duk_enum(self._ctx, idx, _lib.DUK_ENUM_ARRAY_INDICES_ONLY)
        try:
            while _lib.duk_next(self._ctx, top, 1):
                result.append(self._peek(-1))
        finally:
            _lib.duk_set_top(self._ctx, top)
        return result

    def _peek_object(self, idx):
        result = {}
        top = _lib.duk_get_top(self._ctx)
        _lib.duk_enum(self._ctx, idx, _lib.DUK_ENUM_OWN_PROPERTIES_ONLY)
        try:
            while _lib.duk_next(self._ctx, top, 1):
                key = self._peek(-2)
                value = self._peek(-1)
                result[key] = value
        finally:
            _lib.duk_set_top(self._ctx, top)
        return result

    def _peek_function(self, idx):
        fn_ptr = _lib.duk_get_heapptr(self._ctx, idx)
        self._stash(fn_ptr)

        def wrapper(*args):
            top = _lib.duk_push_heapptr(self._ctx, fn_ptr)
            try:
                for arg in args:
                    self._push(arg)
                _lib.duk_call(self._ctx, len(args))
                return self._peek(-1)
            finally:
                _lib.duk_set_top(self._ctx, top)

        return wrapper

    def _push(self, value):
        if value is None:
            self._push_null()
        elif isinstance(value, bool):
            self._push_boolean(value)
        elif isinstance(value, int) or isinstance(value, float):
            self._push_number(value)
        elif isinstance(value, str):
            self._push_string(value)
        elif isinstance(value, list) or isinstance(value, tuple):
            self._push_array(value)
        elif isinstance(value, dict):
            self._push_object(value)
        elif callable(value):
            self._push_function(value)
        else:
            raise ValueError()

    def _push_null(self):
        _lib.duk_push_null(self._ctx)

    def _push_boolean(self, value):
        _lib.duk_push_boolean(self._ctx, value)

    def _push_number(self, value):
        _lib.duk_push_number(self._ctx, value)

    def _push_string(self, value):
        _lib.duk_push_string(self._ctx, value.encode('utf-8'))

    def _push_array(self, value):
        arr_idx = _lib.duk_push_array(self._ctx)
        try:
            for index, element in enumerate(value):
                self._push(element)
                if _lib.duk_put_prop_index(self._ctx, arr_idx,
                                           index) != 1:
                    raise Exception('could not add element to array')
        except Exception:
            _lib.duk_set_top(self._ctx, arr_idx)
            raise

    def _push_object(self, value):
        obj_idx = _lib.duk_push_object(self._ctx)
        try:
            for k, v in value.items():
                self._push(v)
                if _lib.duk_put_prop_string(self._ctx, obj_idx,
                                            k.encode('utf-8')) != 1:
                    raise Exception('could not add property to object')
        except Exception:
            _lib.duk_set_top(self._ctx, obj_idx)
            raise

    def _push_function(self, value):

        @_lib.duk_c_function
        def wrapper(ctx):
            args = []
            args_count = _lib.duk_get_top(self._ctx)
            try:
                for i in range(args_count):
                    args.append(self._peek(i))
                result = value(*args)
                self._push(result)
            except Exception:
                return _lib.DUK_RET_ERROR
            return 1

        _lib.duk_push_c_function(self._ctx, wrapper, _lib.DUK_VARARGS)
        self._fns.append(wrapper)

    def _stash(self, ptr):
        top = _lib.duk_get_top(self._ctx)
        try:
            ptr_id = int(ptr)
            _lib.duk_push_heap_stash(self._ctx)
            _lib.duk_push_heapptr(self._ctx, ptr)
            if _lib.duk_put_prop_index(self._ctx, -2, ptr_id) != 1:
                raise Exception("couldn't stash pointer")
        finally:
            _lib.duk_set_top(self._ctx, top)


class _Lib:

    def __init__(self, path):
        lib = ctypes.cdll.LoadLibrary(str(path))
        self._init_functions(lib)
        self._init_constants(lib)

    def _init_functions(self, lib):
        duk_size_t = ctypes.c_size_t
        duk_int_t = ctypes.c_int
        duk_uint_t = ctypes.c_uint
        duk_small_int_t = ctypes.c_int
        duk_small_uint_t = ctypes.c_uint
        duk_bool_t = duk_small_uint_t
        duk_idx_t = duk_int_t
        duk_uarridx_t = duk_uint_t
        duk_ret_t = duk_small_int_t
        duk_double_t = ctypes.c_double
        duk_context_p = ctypes.c_void_p

        self.duk_c_function = ctypes.CFUNCTYPE(duk_ret_t,
                                               duk_context_p)
        self.duk_fatal_function = ctypes.CFUNCTYPE(None,
                                                   ctypes.c_void_p,
                                                   ctypes.c_char_p)

        functions = [
            (None, 'duk_call', [duk_context_p,
                                duk_idx_t]),
            (duk_context_p, 'duk_create_heap', [ctypes.c_void_p,
                                                ctypes.c_void_p,
                                                ctypes.c_void_p,
                                                ctypes.c_void_p,
                                                self.duk_fatal_function]),
            (None, 'duk_destroy_heap', [duk_context_p]),
            (None, 'duk_enum', [duk_context_p,
                                duk_idx_t,
                                duk_uint_t]),
            (duk_int_t, 'duk_eval_raw', [duk_context_p,
                                         ctypes.c_char_p,
                                         duk_size_t,
                                         duk_uint_t]),
            (duk_bool_t, 'duk_get_boolean', [duk_context_p,
                                             duk_idx_t]),
            (duk_bool_t, 'duk_get_global_string', [duk_context_p,
                                                   ctypes.c_char_p]),
            (ctypes.c_void_p, 'duk_get_heapptr', [duk_context_p,
                                                  duk_idx_t]),
            (duk_double_t, 'duk_get_number', [duk_context_p,
                                              duk_idx_t]),
            (ctypes.c_char_p, 'duk_get_string', [duk_context_p,
                                                 duk_idx_t]),
            (duk_idx_t, 'duk_get_top', [duk_context_p]),
            (duk_uint_t, 'duk_get_type_mask', [duk_context_p,
                                               duk_idx_t]),
            (duk_bool_t, 'duk_is_array', [duk_context_p,
                                          duk_idx_t]),
            (duk_bool_t, 'duk_is_boolean', [duk_context_p,
                                            duk_idx_t]),
            (duk_bool_t, 'duk_is_function', [duk_context_p,
                                             duk_idx_t]),
            (duk_bool_t, 'duk_is_number', [duk_context_p,
                                           duk_idx_t]),
            (duk_bool_t, 'duk_is_object', [duk_context_p,
                                           duk_idx_t]),
            (duk_bool_t, 'duk_is_string', [duk_context_p,
                                           duk_idx_t]),
            (duk_bool_t, 'duk_next', [duk_context_p,
                                      duk_idx_t,
                                      duk_bool_t]),
            (duk_idx_t, 'duk_push_array', [duk_context_p]),
            (None, 'duk_push_boolean', [duk_context_p,
                                        duk_bool_t]),
            (duk_idx_t, 'duk_push_c_function', [duk_context_p,
                                                self.duk_c_function,
                                                duk_idx_t]),
            (duk_idx_t, 'duk_push_heapptr', [duk_context_p,
                                             ctypes.c_void_p]),
            (None, 'duk_push_heap_stash', [duk_context_p]),
            (None, 'duk_push_null', [duk_context_p]),
            (None, 'duk_push_number', [duk_context_p,
                                       duk_double_t]),
            (duk_idx_t, 'duk_push_object', [duk_context_p]),
            (ctypes.c_char_p, 'duk_push_string', [duk_context_p,
                                                  ctypes.c_char_p]),
            (duk_bool_t, 'duk_put_global_string', [duk_context_p,
                                                   ctypes.c_char_p]),
            (duk_bool_t, 'duk_put_prop_index', [duk_context_p,
                                                duk_idx_t,
                                                duk_uarridx_t]),
            (duk_bool_t, 'duk_put_prop_string', [duk_context_p,
                                                 duk_idx_t,
                                                 ctypes.c_char_p]),
            (None, 'duk_set_top', [duk_context_p,
                                   duk_idx_t])
        ]

        for restype, name, argtypes in functions:
            function = getattr(lib, name)
            function.argtypes = argtypes
            function.restype = restype
            setattr(self, name, function)

    def _init_constants(self, lib):
        DUK_ERR_ERROR = 1
        DUK_COMPILE_EVAL = (1 << 3)
        DUK_COMPILE_NOSOURCE = (1 << 9)
        DUK_COMPILE_STRLEN = (1 << 10)
        DUK_COMPILE_NOFILENAME = (1 << 11)
        DUK_TYPE_MASK_UNDEFINED = (1 << 1)
        DUK_TYPE_MASK_NULL = (1 << 2)

        self.DUK_ENUM_OWN_PROPERTIES_ONLY = (1 << 4)
        self.DUK_ENUM_ARRAY_INDICES_ONLY = (1 << 5)
        self.DUK_VARARGS = -1
        self.DUK_RET_ERROR = - DUK_ERR_ERROR

        self.duk_eval_string = lambda ctx, src: lib.duk_eval_raw(
            ctx, src, 0,
            0 | DUK_COMPILE_EVAL | DUK_COMPILE_NOSOURCE |
            DUK_COMPILE_STRLEN | DUK_COMPILE_NOFILENAME)

        self.duk_is_null_or_undefined = lambda ctx, idx: bool(
            lib.duk_get_type_mask(ctx, idx) &
            (DUK_TYPE_MASK_NULL | DUK_TYPE_MASK_UNDEFINED))


if sys.platform == 'win32':
    _lib_suffix = '.dll'
elif sys.platform == 'darwin':
    _lib_suffix = '.dylib'
else:
    _lib_suffix = '.so'


_lib = _Lib(Path(__file__).parent / f'duktape{_lib_suffix}')

Classes

class Interpreter

JavaScript interpreter

High-level python wrapper to duktape JavaScript interpreter.

Current implementation caches all function objects for preventing garbage collection.

Expand source code
class Interpreter:
    """JavaScript interpreter

    High-level python wrapper to duktape JavaScript interpreter.

    Current implementation caches all function objects for preventing
    garbage collection.

    """

    def __init__(self):

        @_lib.duk_fatal_function
        def fatal_handler(udata, msg):
            try:
                print(f"duktape fatal error: {msg.decode('utf-8')}")
            finally:
                sys.exit(1)

        self._ctx = _lib.duk_create_heap(None, None, None, None, fatal_handler)
        self._fns = [fatal_handler]

    def __del__(self):
        _lib.duk_destroy_heap(self._ctx)
        self._fns = []

    def eval(self, code):
        """Evaluate JS code

        Args:
            code (str): JS code

        Returns:
            Any

        """
        top = _lib.duk_get_top(self._ctx)
        try:
            _lib.duk_eval_string(self._ctx, code.encode('utf-8'))
            return self._peek(-1)
        finally:
            _lib.duk_set_top(self._ctx, top)

    def get(self, name):
        """Get global value

        Args:
            name (str): global name

        Returns:
            Any

        """
        top = _lib.duk_get_top(self._ctx)
        try:
            key = name.encode('utf-8')
            _lib.duk_get_global_string(self._ctx, key)
            return self._peek(-1)
        finally:
            _lib.duk_set_top(self._ctx, top)

    def set(self, name, value):
        """Set global value

        Args:
            name (str): global name
            value (Any): value

        """
        top = _lib.duk_get_top(self._ctx)
        try:
            self._push(value)
            key = name.encode('utf-8')
            _lib.duk_put_global_string(self._ctx, key)
        finally:
            _lib.duk_set_top(self._ctx, top)

    def _peek(self, idx):
        if _lib.duk_is_null_or_undefined(self._ctx, idx):
            return
        if _lib.duk_is_boolean(self._ctx, idx):
            return self._peek_boolean(idx)
        if _lib.duk_is_number(self._ctx, idx):
            return self._peek_number(idx)
        if _lib.duk_is_string(self._ctx, idx):
            return self._peek_string(idx)
        if _lib.duk_is_array(self._ctx, idx):
            return self._peek_array(idx)
        if _lib.duk_is_function(self._ctx, idx):
            return self._peek_function(idx)
        if _lib.duk_is_object(self._ctx, idx):
            return self._peek_object(idx)
        raise ValueError()

    def _peek_boolean(self, idx):
        return bool(_lib.duk_get_boolean(self._ctx, idx))

    def _peek_number(self, idx):
        return _lib.duk_get_number(self._ctx, idx)

    def _peek_string(self, idx):
        return _lib.duk_get_string(self._ctx, idx).decode('utf-8')

    def _peek_array(self, idx):
        result = []
        top = _lib.duk_get_top(self._ctx)
        _lib.duk_enum(self._ctx, idx, _lib.DUK_ENUM_ARRAY_INDICES_ONLY)
        try:
            while _lib.duk_next(self._ctx, top, 1):
                result.append(self._peek(-1))
        finally:
            _lib.duk_set_top(self._ctx, top)
        return result

    def _peek_object(self, idx):
        result = {}
        top = _lib.duk_get_top(self._ctx)
        _lib.duk_enum(self._ctx, idx, _lib.DUK_ENUM_OWN_PROPERTIES_ONLY)
        try:
            while _lib.duk_next(self._ctx, top, 1):
                key = self._peek(-2)
                value = self._peek(-1)
                result[key] = value
        finally:
            _lib.duk_set_top(self._ctx, top)
        return result

    def _peek_function(self, idx):
        fn_ptr = _lib.duk_get_heapptr(self._ctx, idx)
        self._stash(fn_ptr)

        def wrapper(*args):
            top = _lib.duk_push_heapptr(self._ctx, fn_ptr)
            try:
                for arg in args:
                    self._push(arg)
                _lib.duk_call(self._ctx, len(args))
                return self._peek(-1)
            finally:
                _lib.duk_set_top(self._ctx, top)

        return wrapper

    def _push(self, value):
        if value is None:
            self._push_null()
        elif isinstance(value, bool):
            self._push_boolean(value)
        elif isinstance(value, int) or isinstance(value, float):
            self._push_number(value)
        elif isinstance(value, str):
            self._push_string(value)
        elif isinstance(value, list) or isinstance(value, tuple):
            self._push_array(value)
        elif isinstance(value, dict):
            self._push_object(value)
        elif callable(value):
            self._push_function(value)
        else:
            raise ValueError()

    def _push_null(self):
        _lib.duk_push_null(self._ctx)

    def _push_boolean(self, value):
        _lib.duk_push_boolean(self._ctx, value)

    def _push_number(self, value):
        _lib.duk_push_number(self._ctx, value)

    def _push_string(self, value):
        _lib.duk_push_string(self._ctx, value.encode('utf-8'))

    def _push_array(self, value):
        arr_idx = _lib.duk_push_array(self._ctx)
        try:
            for index, element in enumerate(value):
                self._push(element)
                if _lib.duk_put_prop_index(self._ctx, arr_idx,
                                           index) != 1:
                    raise Exception('could not add element to array')
        except Exception:
            _lib.duk_set_top(self._ctx, arr_idx)
            raise

    def _push_object(self, value):
        obj_idx = _lib.duk_push_object(self._ctx)
        try:
            for k, v in value.items():
                self._push(v)
                if _lib.duk_put_prop_string(self._ctx, obj_idx,
                                            k.encode('utf-8')) != 1:
                    raise Exception('could not add property to object')
        except Exception:
            _lib.duk_set_top(self._ctx, obj_idx)
            raise

    def _push_function(self, value):

        @_lib.duk_c_function
        def wrapper(ctx):
            args = []
            args_count = _lib.duk_get_top(self._ctx)
            try:
                for i in range(args_count):
                    args.append(self._peek(i))
                result = value(*args)
                self._push(result)
            except Exception:
                return _lib.DUK_RET_ERROR
            return 1

        _lib.duk_push_c_function(self._ctx, wrapper, _lib.DUK_VARARGS)
        self._fns.append(wrapper)

    def _stash(self, ptr):
        top = _lib.duk_get_top(self._ctx)
        try:
            ptr_id = int(ptr)
            _lib.duk_push_heap_stash(self._ctx)
            _lib.duk_push_heapptr(self._ctx, ptr)
            if _lib.duk_put_prop_index(self._ctx, -2, ptr_id) != 1:
                raise Exception("couldn't stash pointer")
        finally:
            _lib.duk_set_top(self._ctx, top)

Methods

def eval(self, code)

Evaluate JS code

Args

code : str
JS code

Returns

Any

Expand source code
def eval(self, code):
    """Evaluate JS code

    Args:
        code (str): JS code

    Returns:
        Any

    """
    top = _lib.duk_get_top(self._ctx)
    try:
        _lib.duk_eval_string(self._ctx, code.encode('utf-8'))
        return self._peek(-1)
    finally:
        _lib.duk_set_top(self._ctx, top)
def get(self, name)

Get global value

Args

name : str
global name

Returns

Any

Expand source code
def get(self, name):
    """Get global value

    Args:
        name (str): global name

    Returns:
        Any

    """
    top = _lib.duk_get_top(self._ctx)
    try:
        key = name.encode('utf-8')
        _lib.duk_get_global_string(self._ctx, key)
        return self._peek(-1)
    finally:
        _lib.duk_set_top(self._ctx, top)
def set(self, name, value)

Set global value

Args

name : str
global name
value : Any
value
Expand source code
def set(self, name, value):
    """Set global value

    Args:
        name (str): global name
        value (Any): value

    """
    top = _lib.duk_get_top(self._ctx)
    try:
        self._push(value)
        key = name.encode('utf-8')
        _lib.duk_put_global_string(self._ctx, key)
    finally:
        _lib.duk_set_top(self._ctx, top)