From e64637661be42bbbcb6af1b027863685af19bec4 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 13 Jun 2026 17:36:48 -0500 Subject: [PATCH 01/10] update --- cyjs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cyjs/__init__.py b/cyjs/__init__.py index 753f90f..7a3c5c3 100644 --- a/cyjs/__init__.py +++ b/cyjs/__init__.py @@ -11,7 +11,7 @@ ) __author__ = "Vizonex" -__version__ = "0.2.0" +__version__ = "0.2.1" __all__ = ( "CancelledError", From 09dac8ba686e851bfae8d39e6141d69d398c0bbc Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 13 Jun 2026 17:46:51 -0500 Subject: [PATCH 02/10] update submodules --- quickjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickjs b/quickjs index 2868a65..66adc82 160000 --- a/quickjs +++ b/quickjs @@ -1 +1 @@ -Subproject commit 2868a6589dec8aea72b522f0a68d082bd66e5ed9 +Subproject commit 66adc822eb0f0ac65e6b73d9a5c3fac50e43224e From 39311171656903b1474d8edbef881f250ae61694 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 13 Jun 2026 17:50:32 -0500 Subject: [PATCH 03/10] remove cutils.c --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 55fee90..cddcb56 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ map( str, [ - QUICKJS_DIR / "cutils.c", QUICKJS_DIR / "dtoa.c", QUICKJS_DIR / "libregexp.c", QUICKJS_DIR / "libunicode.c", From d78c413efefeb002b1c7dec0b2cb5552df2ea3b5 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 3 Jul 2026 22:16:25 -0500 Subject: [PATCH 04/10] implement script args and std helpers --- cyjs/__init__.py | 2 +- cyjs/_cyjs.pxd | 3 ++ cyjs/_cyjs.pyi | 14 +++++++++ cyjs/_cyjs.pyx | 25 ++++++++++++++++ cyjs/quickjs.pxd | 64 ++++++++++++++++++++++++++++++++++++++++- setup.py | 1 + tests/test_scripting.py | 10 +++++++ 7 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 tests/test_scripting.py diff --git a/cyjs/__init__.py b/cyjs/__init__.py index 7a3c5c3..0330d38 100644 --- a/cyjs/__init__.py +++ b/cyjs/__init__.py @@ -11,7 +11,7 @@ ) __author__ = "Vizonex" -__version__ = "0.2.1" +__version__ = "0.3.0" __all__ = ( "CancelledError", diff --git a/cyjs/_cyjs.pxd b/cyjs/_cyjs.pxd index cc74d86..9fba4f6 100644 --- a/cyjs/_cyjs.pxd +++ b/cyjs/_cyjs.pxd @@ -370,3 +370,6 @@ cdef class Context: JSClass js_cls ) + # Privated away from python, it's just a fancy + # little shortcut + cdef void free_value(self, JSValue value) diff --git a/cyjs/_cyjs.pyi b/cyjs/_cyjs.pyi index cd9ef38..fff418c 100644 --- a/cyjs/_cyjs.pyi +++ b/cyjs/_cyjs.pyi @@ -264,6 +264,7 @@ class Context: backtrace_barrier: bool = ..., promise: bool = ..., ) -> Any: ... + def add_class(self, js_cls: JSClass[Any]): """ binds a JSClass Globally to globalThis @@ -272,6 +273,19 @@ class Context: attribute of this context \ as a shortcut and calling `runtime.new_class(...)` beforehand """ + + def set_script_arguments(self, *args: str): + """adds script arguments simillar to js_std_add_helpers however + it only sends in the "scriptArgs" data, if empty + this will be an empty array.""" + + def add_std_print_handlers(self): + """ + implements console printing functions. + """ + + def add_std_helpers(self, *args): + """functions the same as js_std_add_helpers.""" class CancelledError(Exception): """Promise was rejected""" diff --git a/cyjs/_cyjs.pyx b/cyjs/_cyjs.pyx index 639914a..b1bd1de 100644 --- a/cyjs/_cyjs.pyx +++ b/cyjs/_cyjs.pyx @@ -1412,6 +1412,31 @@ cdef class Context: # type: ignore return to_python(self.ctx, obj) + cdef void free_value(self, JSValue value): + JS_FreeValue(self.ctx, value) + + def set_script_arguments(self, *args): + """adds script arguments simillar to js_std_add_helpers however + it only sends in the "scriptArgs" data, if empty + this will be an empty array.""" + self.set("scriptArgs", list(args)) + + def add_std_print_handlers(self): + """ + implements console printing functions. + """ + # NOTE: We can ignore argc, argv we have a faster method + # for that... + js_std_add_helpers(self.ctx, 0, NULL) + + def add_std_helpers(self, *args): + """functions the same as js_std_add_helpers.""" + js_std_add_helpers(self.ctx, 0, NULL) + self.set("scriptArgs", list(args)) + + + + # TODO: Soon as I figure out how to make callbacks and promises work... diff --git a/cyjs/quickjs.pxd b/cyjs/quickjs.pxd index 2d79ff0..96bcb21 100644 --- a/cyjs/quickjs.pxd +++ b/cyjs/quickjs.pxd @@ -162,7 +162,7 @@ cdef extern from "quickjs.h" nogil: void JS_ComputeMemoryUsage(JSRuntime*, JSMemoryUsage*) void JS_DumpMemoryUsage(FILE*, JSMemoryUsage*, JSRuntime*) - # Pull request for this one is pending: SEE: https://github.com/quickjs-ng/quickjs/pull/1284 + # Pull request for this one is pending: SEE: https:#github.com/quickjs-ng/quickjs/pull/1284 # ctypedef int (*JS_MemoryUsageCB)(void* opaque, const char* data, size_t data_len) noexcept with gil # int JS_WriteMemoryUsage(JS_MemoryUsageCB cb, const JSMemoryUsage *s, JSRuntime *rt, void* opaque) JSAtom JS_NewAtomLen(JSContext*, const char*, size_t) @@ -558,6 +558,68 @@ cdef extern from "quickjs.h" nogil: int JS_PROP_NORMAL int JS_PROP_GETSET + +ctypedef JSRuntime *(*runtime_func)() +ctypedef JSContext *(*context_func)(JSRuntime *rt) + +cdef extern from "quickjs-libc.h": + ctypedef uint8_t *JSLoadFileFunc(JSContext *ctx, size_t *pbuf_len, + const char *filename) + + JSModuleDef *js_init_module_std( + JSContext *ctx, + const char *module_name + ) + JSModuleDef *js_init_module_os( + JSContext *ctx, + const char *module_name + ) + JSModuleDef *js_init_module_bjson( + JSContext *ctx, + const char *module_name + ) + void js_std_add_helpers(JSContext *ctx, int argc, char **argv) + int js_std_loop(JSContext *ctx) + int js_std_loop_once(JSContext *ctx) + int js_std_poll_io(JSContext *ctx, int timeout_ms) + JSValue js_std_await(JSContext *ctx, JSValue obj) + void js_std_init_handlers(JSRuntime *rt) + void js_std_free_handlers(JSRuntime *rt) + void js_std_dump_error(JSContext *ctx) + uint8_t *js_load_file(JSContext *ctx, size_t *pbuf_len, + const char *filename) + int js_module_set_import_meta(JSContext *ctx, JSValue func_val, + bool use_realpath, bool is_main) + JSModuleDef *js_module_loader(JSContext *ctx, + const char *module_name, void *opaque, + JSValue attributes) + # like js_module_loader but does not load .so objects and the file reader + # is pluggable; js_module_loader is implemented in terms of js_module_load + JSModuleDef *js_module_load(JSContext *ctx, const char *module_name, + void *opaque, JSValue attributes, + JSLoadFileFunc *load_file) + int js_module_check_attributes(JSContext *ctx, void *opaque, + JSValue attributes) + void js_std_eval_binary(JSContext *ctx, const uint8_t *buf, + size_t buf_len, int flags) + void js_std_promise_rejection_tracker(JSContext *ctx, + JSValue promise, + JSValue reason, + bool is_handled, + void *opaque) + # Defaults to JS_NewRuntime, no-op if compiled without worker support. + # Call before creating the first worker thread. + void js_std_set_worker_new_runtime_func(runtime_func func); + # Defaults to JS_NewContext, no-op if compiled without worker support. + # Call before creating the first worker thread. + + void js_std_set_worker_new_context_func(context_func func) + + + + + + \ No newline at end of file diff --git a/setup.py b/setup.py index cddcb56..3043c83 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ QUICKJS_DIR / "libregexp.c", QUICKJS_DIR / "libunicode.c", QUICKJS_DIR / "quickjs.c", + QUICKJS_DIR / "quickjs-libc.c" ], ) ) diff --git a/tests/test_scripting.py b/tests/test_scripting.py new file mode 100644 index 0000000..9da70ce --- /dev/null +++ b/tests/test_scripting.py @@ -0,0 +1,10 @@ +from cyjs import Context + + +def test_script_args(ctx: Context): + ctx.set_script_arguments("cool.py", "--help") + assert ctx.get("scriptArgs").to_json() == b'cool.py,--help' + +def test_std_handlers(ctx: Context): + ctx.add_std_helpers("--help") + assert ctx.get("scriptArgs").to_json() == b'--help' From 366bf3428245dac5dd3585a564108a06f5f78394 Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:18:46 -0500 Subject: [PATCH 05/10] implement CMake and fix up tests (#25) * implement CMake and fix up tests * try labeling everything as static (linux) * use -fPIC on linux * maybe get rid of a section where there is header files? * do not use cmake on linux do this workaround instead * include header folder when compiling linux * please work * workaround all of linux with the old setup --- README.md | 43 ++++++++++ cyjs/_cyjs.pyx | 7 +- setup.py | 192 +++++++++++++++++++++++++++++++++++++++++--- tests/test_class.py | 7 +- 4 files changed, 231 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 35f125b..137613d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,49 @@ ECMAScript interpreter for Cython & Python built for - Being a good companion alongside [selectolax](https://github.com/rushter/selectolax) or beautiful-soup the choice is yours... - License friendly, after abandoning pyduktape due to the backend no longer being maintained but also having a pretty poor license all together, It inspired me to try something new for a change that could run newer HTML5 Javascript for any puzzle that is thrown your way. +## Installation +Installation of cyjs is pretty simplistic and you can get the released version from pypi +``` +pip install cyjs +``` + +### Using Unreleased Development Versions +If your planning to contribute or want in on anything newly +added you can clone the github repo there is however one +dependency needed which is CMake, it was chosen to keep the +setuptools section cleansed and allow the quickjs-ng contributors to help us with those sections. +You can install cmake using pip if you need something lazy and quick. It's recommended you install cmake globally if you don't have it yet. + +``` +pip install cmake +``` + +### UV (Recommended) +After getting cmake it is recommended to use uv to build and +install the cyjs library in development mode + +``` +uv sync +``` +Otherwise you can try this approch know that you need pytest if your planning to test anything locally. + +1. +``` +uv venv +``` + +2. +``` +uv pip install -e . +``` + +### Alternative Route + +``` +pip install -e . +``` + + ## Quick Example diff --git a/cyjs/_cyjs.pyx b/cyjs/_cyjs.pyx index 639914a..b797c46 100644 --- a/cyjs/_cyjs.pyx +++ b/cyjs/_cyjs.pyx @@ -4,11 +4,12 @@ from cpython.buffer cimport PyObject_CheckBuffer from cpython.bytes cimport PyBytes_FromStringAndSize from cpython.exc cimport (PyErr_CheckSignals, PyErr_NoMemory, PyErr_Occurred, PyErr_SetObject, PyErr_WriteUnraisable) -from cpython.list cimport PyList_AsTuple +from cpython.list cimport PyList_AsTuple, PyList_GET_SIZE from cpython.long cimport PyLong_AsLongAndOverflow, PyLong_FromString from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc -from cpython.object cimport PyObject_CallObject, PyObject_Str, Py_TYPE, PyObject_GetAttr, PyObject_SetAttr, PyObject_HasAttrString -from cpython.list cimport PyList_GET_SIZE +from cpython.object cimport (Py_TYPE, PyObject_CallObject, PyObject_GetAttr, + PyObject_HasAttrString, PyObject_SetAttr, + PyObject_Str) from cpython.tuple cimport PyTuple_GET_SIZE from cpython.type cimport PyType_Check from cpython.unicode cimport PyUnicode_FromString, PyUnicode_FromStringAndSize diff --git a/setup.py b/setup.py index cddcb56..75716f0 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,13 @@ -from setuptools import Extension, setup -from setuptools.command.build_ext import build_ext import os -from pathlib import Path +import shutil +import subprocess import sys +from pathlib import Path -use_system_lib = bool(int(os.environ.get("QUICKJS_USE_SYSTEM_LIB", 0))) +from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext +use_system_lib = bool(int(os.environ.get("QUICKJS_USE_SYSTEM_LIB", 0))) QUICKJS_DIR = Path("quickjs") @@ -157,10 +159,180 @@ def pyx_ext(file: str): ) + +class quickjs_build_cmake_ext(build_ext): + # Brought over from winloop since these can be very useful. + + user_options = build_ext.user_options + [ + ("cython-always", None, "run cythonize() even if .c files are present"), + ( + "cython-annotate", + None, + "Produce a colorized HTML version of the Cython source.", + ), + ("cython-directives=", None, "Cythion compiler directives"), + ] + + def initialize_options(self): + self.cython_always = False + self.cython_annotate = False + self.cython_directives = None + self.parallel = True + super().initialize_options() + + def add_include_dir(self, dir, force=False): + if use_system_lib and not force: + return + dirs = self.compiler.include_dirs + dirs.insert(0, dir) + self.compiler.set_include_dirs(dirs) + + def build_extensions(self): + if use_system_lib: + self.compiler.add_library("quickjs-ng") + build_ext.build_extensions(self) + return + + cmake_cmd = shutil.which("cmake") + + if not cmake_cmd: + raise RuntimeError("cyjs requires cmake") + quickjs_ng_vendor = os.path.join("quickjs") + build_temp = os.path.abspath(os.path.join(self.build_temp, "qjs-build")) + install_dir = os.path.abspath(os.path.join(self.build_temp, "qjs-install")) + os.makedirs(build_temp, exist_ok=True) + os.makedirs(install_dir, exist_ok=True) + + cmake_args = [ + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_CONFIGURATION_TYPES=Release", + f"-DCMAKE_INSTALL_PREFIX={install_dir}", + "-DQJS_BUILD_LIBC=ON", # We do need LIBC there are plans to implement it's things + "-DQJS_BUILD_CLI=OFF", # We don't need CLI it wastes time to make it. + "-DBUILD_SHARED_LIBS=false", + ] + + print(f"Configuring quickjs-ng with CMake in {build_temp}") + subprocess.check_call( + [cmake_cmd, os.path.abspath(quickjs_ng_vendor), *cmake_args], cwd=build_temp + ) + + print("Building quickjs-ng...") + build_args = ["--build", ".", "--config", "Release"] + + subprocess.check_call([cmake_cmd, *build_args], cwd=build_temp) + + install_args = ["--install", ".", "--config", "Release"] + subprocess.check_call([cmake_cmd, *install_args], cwd=build_temp) + + if sys.platform == "win32": + # Windows libraries + possible_paths = [ + os.path.join(install_dir, "lib", "qjs.lib"), + os.path.join(install_dir, "lib", "qjs_static.lib"), + os.path.join(install_dir, "lib", "libqjs.a"), # MinGW + ] + else: + possible_paths = [ + os.path.join(install_dir, "lib", "libqjs.a"), + os.path.join(install_dir, "lib64", "libqjs.a"), + ] + + lib_path = None + for path in possible_paths: + if os.path.exists(path): + lib_path = path + break + + # print("==== DEBUG ====") + # print(lib_path) + + if not lib_path: + raise RuntimeError( + f"Could not find installed cyjs library in {install_dir}.\n" + f"Checked: {', '.join(possible_paths)}" + ) + + self.extensions[0].extra_objects = [lib_path] + + # self.add_include_dir(os.path.join(install_dir, "include")) + self.add_include_dir(quickjs_ng_vendor) + + build_ext.build_extensions(self) + + # Copied from winloop + def finalize_options(self): + need_cythonize = self.cython_always + cfiles = {} + + for extension in self.distribution.ext_modules: + for i, sfile in enumerate(extension.sources): + if sfile.endswith(".pyx"): + prefix, _ = os.path.splitext(sfile) + cfile = prefix + ".c" + + if os.path.exists(cfile) and not self.cython_always: + extension.sources[i] = cfile + else: + if os.path.exists(cfile): + cfiles[cfile] = os.path.getmtime(cfile) + else: + cfiles[cfile] = 0 + need_cythonize = True + + # from winloop & cyares + if need_cythonize: + # import pkg_resources + + # Double check Cython presence in case setup_requires + # didn't go into effect (most likely because someone + # imported Cython before setup_requires injected the + # correct egg into sys.path. + try: + import Cython # type: ignore # noqa: F401 + except ImportError: + raise RuntimeError("please install cython to compile cyjs from source") + + from Cython.Build import cythonize + + directives = {} + if self.cython_directives: + for directive in self.cython_directives.split(","): + k, _, v = directive.partition("=") + if v.lower() == "false": + v = False + if v.lower() == "true": + v = True + directives[k] = v + self.cython_directives = directives + + self.distribution.ext_modules[:] = cythonize( + self.distribution.ext_modules, + compiler_directives=directives, + annotate=self.cython_annotate, + emit_linenums=self.debug, + # Try using a cache to help with compiling as well... + cache=True, + ) + + return super().finalize_options() + + if __name__ == "__main__": - setup( - ext_modules=[ - pyx_ext("_cyjs"), - ], - cmdclass={"build_ext": quickjs_build_ext}, - ) + if sys.platform != "linux": + setup( + ext_modules=[ + Extension( + "cyjs._cyjs", + ["cyjs/_cyjs.pyx"] + ), + ], + cmdclass={"build_ext": quickjs_build_cmake_ext}, + ) + else: + setup( + ext_modules=[ + pyx_ext("_cyjs"), + ], + cmdclass={"build_ext": quickjs_build_ext}, + ) diff --git a/tests/test_class.py b/tests/test_class.py index cfc52a2..a412ad0 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -14,11 +14,13 @@ class X: assert js_cls.type == X assert js_cls.runtime == rt + class Point: def __init__(self, x: int, y: int): self.x = x self.y = y + def test_new_class_with_init(ctx: Context) -> None: ctx.add_class(ctx.runtime.new_class(Point)) point: Point = ctx.eval("new Point(1, 2)") @@ -27,11 +29,6 @@ def test_new_class_with_init(ctx: Context) -> None: def test_new_class_with_init_exception(ctx: Context) -> None: - class Point: - def __init__(self, x: int, y: int): - self.x = x - self.y = y - ctx.add_class(ctx.runtime.new_class(Point)) with pytest.raises(TypeError): # Would be the same as Point(1) in python but new syntax because it allocates memory From cd84731c2eebc60884d598893c4d3212b54e08eb Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 3 Jul 2026 22:16:25 -0500 Subject: [PATCH 06/10] implement script args and std helpers --- cyjs/__init__.py | 2 +- cyjs/_cyjs.pxd | 3 ++ cyjs/_cyjs.pyi | 14 +++++++++ cyjs/_cyjs.pyx | 25 ++++++++++++++++ cyjs/quickjs.pxd | 64 ++++++++++++++++++++++++++++++++++++++++- setup.py | 1 + tests/test_scripting.py | 10 +++++++ 7 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 tests/test_scripting.py diff --git a/cyjs/__init__.py b/cyjs/__init__.py index 7a3c5c3..0330d38 100644 --- a/cyjs/__init__.py +++ b/cyjs/__init__.py @@ -11,7 +11,7 @@ ) __author__ = "Vizonex" -__version__ = "0.2.1" +__version__ = "0.3.0" __all__ = ( "CancelledError", diff --git a/cyjs/_cyjs.pxd b/cyjs/_cyjs.pxd index cc74d86..9fba4f6 100644 --- a/cyjs/_cyjs.pxd +++ b/cyjs/_cyjs.pxd @@ -370,3 +370,6 @@ cdef class Context: JSClass js_cls ) + # Privated away from python, it's just a fancy + # little shortcut + cdef void free_value(self, JSValue value) diff --git a/cyjs/_cyjs.pyi b/cyjs/_cyjs.pyi index cd9ef38..fff418c 100644 --- a/cyjs/_cyjs.pyi +++ b/cyjs/_cyjs.pyi @@ -264,6 +264,7 @@ class Context: backtrace_barrier: bool = ..., promise: bool = ..., ) -> Any: ... + def add_class(self, js_cls: JSClass[Any]): """ binds a JSClass Globally to globalThis @@ -272,6 +273,19 @@ class Context: attribute of this context \ as a shortcut and calling `runtime.new_class(...)` beforehand """ + + def set_script_arguments(self, *args: str): + """adds script arguments simillar to js_std_add_helpers however + it only sends in the "scriptArgs" data, if empty + this will be an empty array.""" + + def add_std_print_handlers(self): + """ + implements console printing functions. + """ + + def add_std_helpers(self, *args): + """functions the same as js_std_add_helpers.""" class CancelledError(Exception): """Promise was rejected""" diff --git a/cyjs/_cyjs.pyx b/cyjs/_cyjs.pyx index b797c46..28c11d4 100644 --- a/cyjs/_cyjs.pyx +++ b/cyjs/_cyjs.pyx @@ -1413,6 +1413,31 @@ cdef class Context: # type: ignore return to_python(self.ctx, obj) + cdef void free_value(self, JSValue value): + JS_FreeValue(self.ctx, value) + + def set_script_arguments(self, *args): + """adds script arguments simillar to js_std_add_helpers however + it only sends in the "scriptArgs" data, if empty + this will be an empty array.""" + self.set("scriptArgs", list(args)) + + def add_std_print_handlers(self): + """ + implements console printing functions. + """ + # NOTE: We can ignore argc, argv we have a faster method + # for that... + js_std_add_helpers(self.ctx, 0, NULL) + + def add_std_helpers(self, *args): + """functions the same as js_std_add_helpers.""" + js_std_add_helpers(self.ctx, 0, NULL) + self.set("scriptArgs", list(args)) + + + + # TODO: Soon as I figure out how to make callbacks and promises work... diff --git a/cyjs/quickjs.pxd b/cyjs/quickjs.pxd index 2d79ff0..96bcb21 100644 --- a/cyjs/quickjs.pxd +++ b/cyjs/quickjs.pxd @@ -162,7 +162,7 @@ cdef extern from "quickjs.h" nogil: void JS_ComputeMemoryUsage(JSRuntime*, JSMemoryUsage*) void JS_DumpMemoryUsage(FILE*, JSMemoryUsage*, JSRuntime*) - # Pull request for this one is pending: SEE: https://github.com/quickjs-ng/quickjs/pull/1284 + # Pull request for this one is pending: SEE: https:#github.com/quickjs-ng/quickjs/pull/1284 # ctypedef int (*JS_MemoryUsageCB)(void* opaque, const char* data, size_t data_len) noexcept with gil # int JS_WriteMemoryUsage(JS_MemoryUsageCB cb, const JSMemoryUsage *s, JSRuntime *rt, void* opaque) JSAtom JS_NewAtomLen(JSContext*, const char*, size_t) @@ -558,6 +558,68 @@ cdef extern from "quickjs.h" nogil: int JS_PROP_NORMAL int JS_PROP_GETSET + +ctypedef JSRuntime *(*runtime_func)() +ctypedef JSContext *(*context_func)(JSRuntime *rt) + +cdef extern from "quickjs-libc.h": + ctypedef uint8_t *JSLoadFileFunc(JSContext *ctx, size_t *pbuf_len, + const char *filename) + + JSModuleDef *js_init_module_std( + JSContext *ctx, + const char *module_name + ) + JSModuleDef *js_init_module_os( + JSContext *ctx, + const char *module_name + ) + JSModuleDef *js_init_module_bjson( + JSContext *ctx, + const char *module_name + ) + void js_std_add_helpers(JSContext *ctx, int argc, char **argv) + int js_std_loop(JSContext *ctx) + int js_std_loop_once(JSContext *ctx) + int js_std_poll_io(JSContext *ctx, int timeout_ms) + JSValue js_std_await(JSContext *ctx, JSValue obj) + void js_std_init_handlers(JSRuntime *rt) + void js_std_free_handlers(JSRuntime *rt) + void js_std_dump_error(JSContext *ctx) + uint8_t *js_load_file(JSContext *ctx, size_t *pbuf_len, + const char *filename) + int js_module_set_import_meta(JSContext *ctx, JSValue func_val, + bool use_realpath, bool is_main) + JSModuleDef *js_module_loader(JSContext *ctx, + const char *module_name, void *opaque, + JSValue attributes) + # like js_module_loader but does not load .so objects and the file reader + # is pluggable; js_module_loader is implemented in terms of js_module_load + JSModuleDef *js_module_load(JSContext *ctx, const char *module_name, + void *opaque, JSValue attributes, + JSLoadFileFunc *load_file) + int js_module_check_attributes(JSContext *ctx, void *opaque, + JSValue attributes) + void js_std_eval_binary(JSContext *ctx, const uint8_t *buf, + size_t buf_len, int flags) + void js_std_promise_rejection_tracker(JSContext *ctx, + JSValue promise, + JSValue reason, + bool is_handled, + void *opaque) + # Defaults to JS_NewRuntime, no-op if compiled without worker support. + # Call before creating the first worker thread. + void js_std_set_worker_new_runtime_func(runtime_func func); + # Defaults to JS_NewContext, no-op if compiled without worker support. + # Call before creating the first worker thread. + + void js_std_set_worker_new_context_func(context_func func) + + + + + + \ No newline at end of file diff --git a/setup.py b/setup.py index 75716f0..f2c1abd 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ QUICKJS_DIR / "libregexp.c", QUICKJS_DIR / "libunicode.c", QUICKJS_DIR / "quickjs.c", + QUICKJS_DIR / "quickjs-libc.c" ], ) ) diff --git a/tests/test_scripting.py b/tests/test_scripting.py new file mode 100644 index 0000000..9da70ce --- /dev/null +++ b/tests/test_scripting.py @@ -0,0 +1,10 @@ +from cyjs import Context + + +def test_script_args(ctx: Context): + ctx.set_script_arguments("cool.py", "--help") + assert ctx.get("scriptArgs").to_json() == b'cool.py,--help' + +def test_std_handlers(ctx: Context): + ctx.add_std_helpers("--help") + assert ctx.get("scriptArgs").to_json() == b'--help' From 1fe8e54bfe95202ec466bf5fd3b4a95207dfca31 Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:27:49 -0500 Subject: [PATCH 07/10] implement CMake and fix up tests (#25) (#26) * implement CMake and fix up tests * try labeling everything as static (linux) * use -fPIC on linux * maybe get rid of a section where there is header files? * do not use cmake on linux do this workaround instead * include header folder when compiling linux * please work * workaround all of linux with the old setup --- README.md | 43 ++++++++++ cyjs/_cyjs.pyx | 7 +- setup.py | 192 +++++++++++++++++++++++++++++++++++++++++--- tests/test_class.py | 7 +- 4 files changed, 231 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 35f125b..137613d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,49 @@ ECMAScript interpreter for Cython & Python built for - Being a good companion alongside [selectolax](https://github.com/rushter/selectolax) or beautiful-soup the choice is yours... - License friendly, after abandoning pyduktape due to the backend no longer being maintained but also having a pretty poor license all together, It inspired me to try something new for a change that could run newer HTML5 Javascript for any puzzle that is thrown your way. +## Installation +Installation of cyjs is pretty simplistic and you can get the released version from pypi +``` +pip install cyjs +``` + +### Using Unreleased Development Versions +If your planning to contribute or want in on anything newly +added you can clone the github repo there is however one +dependency needed which is CMake, it was chosen to keep the +setuptools section cleansed and allow the quickjs-ng contributors to help us with those sections. +You can install cmake using pip if you need something lazy and quick. It's recommended you install cmake globally if you don't have it yet. + +``` +pip install cmake +``` + +### UV (Recommended) +After getting cmake it is recommended to use uv to build and +install the cyjs library in development mode + +``` +uv sync +``` +Otherwise you can try this approch know that you need pytest if your planning to test anything locally. + +1. +``` +uv venv +``` + +2. +``` +uv pip install -e . +``` + +### Alternative Route + +``` +pip install -e . +``` + + ## Quick Example diff --git a/cyjs/_cyjs.pyx b/cyjs/_cyjs.pyx index b1bd1de..28c11d4 100644 --- a/cyjs/_cyjs.pyx +++ b/cyjs/_cyjs.pyx @@ -4,11 +4,12 @@ from cpython.buffer cimport PyObject_CheckBuffer from cpython.bytes cimport PyBytes_FromStringAndSize from cpython.exc cimport (PyErr_CheckSignals, PyErr_NoMemory, PyErr_Occurred, PyErr_SetObject, PyErr_WriteUnraisable) -from cpython.list cimport PyList_AsTuple +from cpython.list cimport PyList_AsTuple, PyList_GET_SIZE from cpython.long cimport PyLong_AsLongAndOverflow, PyLong_FromString from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc -from cpython.object cimport PyObject_CallObject, PyObject_Str, Py_TYPE, PyObject_GetAttr, PyObject_SetAttr, PyObject_HasAttrString -from cpython.list cimport PyList_GET_SIZE +from cpython.object cimport (Py_TYPE, PyObject_CallObject, PyObject_GetAttr, + PyObject_HasAttrString, PyObject_SetAttr, + PyObject_Str) from cpython.tuple cimport PyTuple_GET_SIZE from cpython.type cimport PyType_Check from cpython.unicode cimport PyUnicode_FromString, PyUnicode_FromStringAndSize diff --git a/setup.py b/setup.py index 3043c83..f2c1abd 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,13 @@ -from setuptools import Extension, setup -from setuptools.command.build_ext import build_ext import os -from pathlib import Path +import shutil +import subprocess import sys +from pathlib import Path -use_system_lib = bool(int(os.environ.get("QUICKJS_USE_SYSTEM_LIB", 0))) +from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext +use_system_lib = bool(int(os.environ.get("QUICKJS_USE_SYSTEM_LIB", 0))) QUICKJS_DIR = Path("quickjs") @@ -158,10 +160,180 @@ def pyx_ext(file: str): ) + +class quickjs_build_cmake_ext(build_ext): + # Brought over from winloop since these can be very useful. + + user_options = build_ext.user_options + [ + ("cython-always", None, "run cythonize() even if .c files are present"), + ( + "cython-annotate", + None, + "Produce a colorized HTML version of the Cython source.", + ), + ("cython-directives=", None, "Cythion compiler directives"), + ] + + def initialize_options(self): + self.cython_always = False + self.cython_annotate = False + self.cython_directives = None + self.parallel = True + super().initialize_options() + + def add_include_dir(self, dir, force=False): + if use_system_lib and not force: + return + dirs = self.compiler.include_dirs + dirs.insert(0, dir) + self.compiler.set_include_dirs(dirs) + + def build_extensions(self): + if use_system_lib: + self.compiler.add_library("quickjs-ng") + build_ext.build_extensions(self) + return + + cmake_cmd = shutil.which("cmake") + + if not cmake_cmd: + raise RuntimeError("cyjs requires cmake") + quickjs_ng_vendor = os.path.join("quickjs") + build_temp = os.path.abspath(os.path.join(self.build_temp, "qjs-build")) + install_dir = os.path.abspath(os.path.join(self.build_temp, "qjs-install")) + os.makedirs(build_temp, exist_ok=True) + os.makedirs(install_dir, exist_ok=True) + + cmake_args = [ + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_CONFIGURATION_TYPES=Release", + f"-DCMAKE_INSTALL_PREFIX={install_dir}", + "-DQJS_BUILD_LIBC=ON", # We do need LIBC there are plans to implement it's things + "-DQJS_BUILD_CLI=OFF", # We don't need CLI it wastes time to make it. + "-DBUILD_SHARED_LIBS=false", + ] + + print(f"Configuring quickjs-ng with CMake in {build_temp}") + subprocess.check_call( + [cmake_cmd, os.path.abspath(quickjs_ng_vendor), *cmake_args], cwd=build_temp + ) + + print("Building quickjs-ng...") + build_args = ["--build", ".", "--config", "Release"] + + subprocess.check_call([cmake_cmd, *build_args], cwd=build_temp) + + install_args = ["--install", ".", "--config", "Release"] + subprocess.check_call([cmake_cmd, *install_args], cwd=build_temp) + + if sys.platform == "win32": + # Windows libraries + possible_paths = [ + os.path.join(install_dir, "lib", "qjs.lib"), + os.path.join(install_dir, "lib", "qjs_static.lib"), + os.path.join(install_dir, "lib", "libqjs.a"), # MinGW + ] + else: + possible_paths = [ + os.path.join(install_dir, "lib", "libqjs.a"), + os.path.join(install_dir, "lib64", "libqjs.a"), + ] + + lib_path = None + for path in possible_paths: + if os.path.exists(path): + lib_path = path + break + + # print("==== DEBUG ====") + # print(lib_path) + + if not lib_path: + raise RuntimeError( + f"Could not find installed cyjs library in {install_dir}.\n" + f"Checked: {', '.join(possible_paths)}" + ) + + self.extensions[0].extra_objects = [lib_path] + + # self.add_include_dir(os.path.join(install_dir, "include")) + self.add_include_dir(quickjs_ng_vendor) + + build_ext.build_extensions(self) + + # Copied from winloop + def finalize_options(self): + need_cythonize = self.cython_always + cfiles = {} + + for extension in self.distribution.ext_modules: + for i, sfile in enumerate(extension.sources): + if sfile.endswith(".pyx"): + prefix, _ = os.path.splitext(sfile) + cfile = prefix + ".c" + + if os.path.exists(cfile) and not self.cython_always: + extension.sources[i] = cfile + else: + if os.path.exists(cfile): + cfiles[cfile] = os.path.getmtime(cfile) + else: + cfiles[cfile] = 0 + need_cythonize = True + + # from winloop & cyares + if need_cythonize: + # import pkg_resources + + # Double check Cython presence in case setup_requires + # didn't go into effect (most likely because someone + # imported Cython before setup_requires injected the + # correct egg into sys.path. + try: + import Cython # type: ignore # noqa: F401 + except ImportError: + raise RuntimeError("please install cython to compile cyjs from source") + + from Cython.Build import cythonize + + directives = {} + if self.cython_directives: + for directive in self.cython_directives.split(","): + k, _, v = directive.partition("=") + if v.lower() == "false": + v = False + if v.lower() == "true": + v = True + directives[k] = v + self.cython_directives = directives + + self.distribution.ext_modules[:] = cythonize( + self.distribution.ext_modules, + compiler_directives=directives, + annotate=self.cython_annotate, + emit_linenums=self.debug, + # Try using a cache to help with compiling as well... + cache=True, + ) + + return super().finalize_options() + + if __name__ == "__main__": - setup( - ext_modules=[ - pyx_ext("_cyjs"), - ], - cmdclass={"build_ext": quickjs_build_ext}, - ) + if sys.platform != "linux": + setup( + ext_modules=[ + Extension( + "cyjs._cyjs", + ["cyjs/_cyjs.pyx"] + ), + ], + cmdclass={"build_ext": quickjs_build_cmake_ext}, + ) + else: + setup( + ext_modules=[ + pyx_ext("_cyjs"), + ], + cmdclass={"build_ext": quickjs_build_ext}, + ) diff --git a/tests/test_class.py b/tests/test_class.py index cfc52a2..a412ad0 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -14,11 +14,13 @@ class X: assert js_cls.type == X assert js_cls.runtime == rt + class Point: def __init__(self, x: int, y: int): self.x = x self.y = y + def test_new_class_with_init(ctx: Context) -> None: ctx.add_class(ctx.runtime.new_class(Point)) point: Point = ctx.eval("new Point(1, 2)") @@ -27,11 +29,6 @@ def test_new_class_with_init(ctx: Context) -> None: def test_new_class_with_init_exception(ctx: Context) -> None: - class Point: - def __init__(self, x: int, y: int): - self.x = x - self.y = y - ctx.add_class(ctx.runtime.new_class(Point)) with pytest.raises(TypeError): # Would be the same as Point(1) in python but new syntax because it allocates memory From 03d448e4a33ed726d5e8c0ee2a51485bb569d34a Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 4 Jul 2026 15:35:01 -0500 Subject: [PATCH 08/10] set -fvisibility=hidden on linux --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f2c1abd..4c839da 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ "-Wno-unused-result", "-Wno-stringop-truncation", "-Wno-array-bounds", + "-fvisibility=hidden" ] ) From 25309facf447e00e171cfd97b2d2c51de6eecd58 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 4 Jul 2026 15:38:04 -0500 Subject: [PATCH 09/10] add -D_DEFAULT_SOURCE linux --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4c839da..83359a6 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,8 @@ "-Wno-unused-result", "-Wno-stringop-truncation", "-Wno-array-bounds", - "-fvisibility=hidden" + "-fvisibility=hidden", + "-D_DEFAULT_SOURCE" ] ) From 6b96df86834a1075dfec8e5f9195e9161cc7fabd Mon Sep 17 00:00:00 2001 From: Vizonex Date: Sat, 4 Jul 2026 15:40:35 -0500 Subject: [PATCH 10/10] add -D_GNU_SOURCE with it --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 83359a6..e4584d5 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,8 @@ "-Wno-stringop-truncation", "-Wno-array-bounds", "-fvisibility=hidden", - "-D_DEFAULT_SOURCE" + "-D_DEFAULT_SOURCE", + "-D_GNU_SOURCE" ] )