├── ruff.toml ├── pytest.ini ├── docs ├── source │ ├── samples.rst │ ├── api │ │ ├── api.rst │ │ ├── multithread.rst │ │ ├── engine.rst │ │ └── context.rst │ ├── index.rst │ ├── build.rst │ └── conf.py ├── Makefile └── make.bat ├── .gitignore ├── examples ├── simple.js ├── simple.py ├── meaning.py ├── circle.py ├── console.py └── global.py ├── CREDITS.txt ├── src ├── STPyV8.cpp ├── Platform.h ├── Config.h ├── Isolate.h ├── Locker.cpp ├── Locker.h ├── utf8.h ├── Context.h ├── Platform.cpp ├── Isolate.cpp ├── Utils.h ├── Engine.h ├── Utils.cpp ├── Exception.h ├── Context.cpp ├── Engine.cpp ├── Wrapper.h └── utf8 │ ├── unchecked.h │ └── checked.h ├── tests ├── test_Isolate.py ├── test_ICU.py ├── test_Locker.py ├── test_Thread.py ├── test_Context.py └── test_Engine.py ├── .github └── workflows │ ├── semgrep.yml │ ├── sdist.yml │ ├── osx.yml │ ├── linux.yml │ ├── osx-arm64.yml │ └── windows.yml ├── settings.py ├── setup.py ├── README.md ├── LICENSE.txt └── STPyV8.py /ruff.toml: -------------------------------------------------------------------------------- 1 | exclude = ["docs"] 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | -------------------------------------------------------------------------------- /docs/source/samples.rst: -------------------------------------------------------------------------------- 1 | .. _samples: 2 | 3 | Samples 4 | ========================== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | .cache 4 | .cipd 5 | *.egg-info 6 | .gclient* 7 | depot_tools 8 | v8 9 | switch_repo* 10 | *.pyc 11 | *.cmake 12 | .idea 13 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | var x = 3; 2 | console.log('x is ' + x); 3 | x = x * x; 4 | console.log('x is ' + x); 5 | console.error('CPU core temperature critical'); 6 | 7 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | devm18426 - Windows support 2 | Stefano 'antelox' Antenucci - Extremely relevant contributions to the manylinux wheels build workflow on different versions of Python 3 | -------------------------------------------------------------------------------- /docs/source/api/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | Public API 4 | =============== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | context 10 | engine 11 | wrapper 12 | multithread 13 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # simple.py - bind a javascript function to python function 4 | 5 | import STPyV8 6 | 7 | with STPyV8.JSContext() as ctxt: 8 | upcase = ctxt.eval(""" 9 | ( (lowerString) => { 10 | return lowerString.toUpperCase(); 11 | }) 12 | """) 13 | print(upcase("hello world!")) 14 | -------------------------------------------------------------------------------- /examples/meaning.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # meaning.py - call a python method from javascript code 4 | 5 | import STPyV8 6 | 7 | 8 | class MyClass(STPyV8.JSClass): 9 | def reallyComplexFunction(self, addme): 10 | return 10 * 3 + addme 11 | 12 | 13 | my_class = MyClass() 14 | 15 | with STPyV8.JSContext(my_class) as ctxt: 16 | meaning = ctxt.eval("this.reallyComplexFunction(2) + 10;") 17 | print("The meaning of life: " + str(meaning)) 18 | -------------------------------------------------------------------------------- /src/STPyV8.cpp: -------------------------------------------------------------------------------- 1 | #include "Config.h" 2 | #include "Exception.h" 3 | #include "Wrapper.h" 4 | #include "Context.h" 5 | #include "Engine.h" 6 | #include "Locker.h" 7 | 8 | 9 | BOOST_PYTHON_MODULE(_STPyV8) 10 | { 11 | CJavascriptException::Expose(); 12 | CWrapper::Expose(); 13 | CContext::Expose(); 14 | CEngine::Expose(); 15 | CLocker::Expose(); 16 | } 17 | 18 | 19 | #define INIT_MODULE PyInit__STPyV8 20 | extern "C" PyObject* INIT_MODULE(); 21 | -------------------------------------------------------------------------------- /tests/test_Isolate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | 6 | import STPyV8 7 | 8 | 9 | class TestIsolate(unittest.TestCase): 10 | def testBase(self): 11 | with STPyV8.JSIsolate() as isolate: 12 | self.assertIsNotNone(isolate.current) 13 | self.assertFalse(isolate.locked) 14 | 15 | def testEnterLeave(self): 16 | with STPyV8.JSIsolate() as isolate: 17 | self.assertIsNotNone(isolate.current) 18 | -------------------------------------------------------------------------------- /examples/circle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # circle.py - call a method in a javascript class from python 4 | 5 | import STPyV8 6 | 7 | with STPyV8.JSContext() as ctxt: 8 | ctxt.eval(""" 9 | class Circle { 10 | constructor(radius) { 11 | this.radius = radius; 12 | } 13 | get area() { 14 | return this.calcArea() 15 | } 16 | calcArea() { 17 | return 3.14 * this.radius * this.radius; 18 | } 19 | } 20 | """) 21 | circle = ctxt.eval("new Circle(10)") 22 | print("Area of the circle: " + str(circle.area)) 23 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: 0 0 * * * 7 | name: Semgrep config 8 | jobs: 9 | semgrep: 10 | name: semgrep/ci 11 | runs-on: ubuntu-latest 12 | env: 13 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 14 | SEMGREP_URL: https://cloudflare.semgrep.dev 15 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 16 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 17 | container: 18 | image: semgrep/semgrep 19 | steps: 20 | - uses: actions/checkout@v4 21 | - run: semgrep ci 22 | -------------------------------------------------------------------------------- /src/Platform.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Config.h" 8 | 9 | 10 | class CPlatform 11 | { 12 | private: 13 | static bool inited; 14 | static std::unique_ptr platform; 15 | 16 | constexpr static const char *icu_data_system = ICU_DATA_SYSTEM; 17 | constexpr static const char *icu_data_user = ICU_DATA_USER; 18 | 19 | const char *GetICUDataFile(); 20 | 21 | std::string argv; 22 | public: 23 | CPlatform() : argv(std::string()) {}; 24 | CPlatform(std::string argv0) : argv(argv0) {}; 25 | ~CPlatform() {}; 26 | void Init(); 27 | }; 28 | -------------------------------------------------------------------------------- /examples/console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # console.py - run javascipt using a custom console 4 | 5 | from logging import getLogger, basicConfig, INFO 6 | import STPyV8 as V8 7 | 8 | basicConfig(format="%(asctime)-15s %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 9 | log = getLogger("myapp") 10 | log.setLevel(INFO) 11 | 12 | 13 | class Console(object): 14 | def log(self, message): 15 | log.info(message) 16 | 17 | def error(self, message): 18 | log.error(message) 19 | 20 | 21 | class Global(V8.JSClass): 22 | custom_console = Console() 23 | 24 | 25 | def load(js_file): 26 | with open(js_file, "r") as f: 27 | return f.read() 28 | 29 | 30 | def run(js_file): 31 | with V8.JSContext(Global()) as ctxt: 32 | ctxt.eval("this.console = custom_console;") 33 | ctxt.eval(load(js_file)) 34 | 35 | 36 | run("simple.js") 37 | -------------------------------------------------------------------------------- /examples/global.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # global.py - pass different global objects to javascript 4 | 5 | import STPyV8 as V8 6 | 7 | 8 | class Global(V8.JSClass): 9 | version = "1.0" 10 | 11 | def hello(self, name): 12 | return "Hello " + name 13 | 14 | 15 | with V8.JSContext(Global()) as ctxt: 16 | print(ctxt.eval("version")) # 1.0 17 | print(ctxt.eval("hello('World')")) # Hello World 18 | print(ctxt.eval("hello.toString()")) # function () { [native code] } 19 | 20 | # "simulate" a browser, defining the js 'window' object in python 21 | # see https://github.com/buffer/thug for a robust implementation 22 | 23 | 24 | class window(V8.JSClass): 25 | def alert(self, message): 26 | print("Popping up an alert with message " + message) 27 | 28 | 29 | with V8.JSContext(window()) as browser_ctxt: 30 | browser_ctxt.eval("alert('Hello')") 31 | -------------------------------------------------------------------------------- /src/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Enable the built-in Python property support 4 | #define SUPPORT_PROPERTY 1 5 | 6 | // Enable the object lifecycle tracing support 7 | #define SUPPORT_TRACE_LIFECYCLE 1 8 | 9 | // ICU data file 10 | 11 | #if defined(__linux) 12 | # define ICU_DATA_SYSTEM "/usr/share/stpyv8/icudtl.dat" 13 | # define ICU_DATA_USER ".local/share/stpyv8/icudtl.dat" 14 | #elif defined(__APPLE__) 15 | # define ICU_DATA_SYSTEM "/Library/Application Support/STPyV8/icudtl.dat" 16 | # define ICU_DATA_USER "Library/Application Support/STPyV8/icudtl.dat" 17 | #elif defined(_WIN32) 18 | # define ICU_DATA_SYSTEM "\\STPyV8\\icudtl.dat" 19 | # define ICU_DATA_USER "\\STPyV8\\icudtl.dat" 20 | #elif defined (_WIN64) 21 | # define ICU_DATA_SYSTEM "\\STPyV8\\icudtl.dat" 22 | # define ICU_DATA_USER "\\STPyV8\\icudtl.dat" 23 | #else 24 | # define ICU_DATA_SYSTEM nullptr 25 | # define ICU_DATA_USER nullptr 26 | #endif 27 | -------------------------------------------------------------------------------- /docs/source/api/multithread.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: STPyV8 2 | :noindex: 3 | 4 | .. testsetup:: * 5 | 6 | from STPyV8 import * 7 | 8 | .. _multithread: 9 | 10 | Multi-Thread and Lock 11 | ========================== 12 | 13 | The Javascript Thread and Isolate 14 | --------------------------------- 15 | 16 | V8 isolates have completely separate states. Objects from one isolate must not be used in other isolates. When V8 is initialized a default isolate is implicitly created and entered. The embedder can create additional isolates and use them in parallel in multiple threads. An isolate can be entered by at most one thread at any given time. The Locker/Unlocker API can be used to synchronize. 17 | 18 | 19 | JSIsolate 20 | --------- 21 | 22 | .. autoclass:: JSIsolate 23 | :members: 24 | :inherited-members: 25 | 26 | .. automethod:: __enter__() -> JSIsolate object 27 | 28 | .. automethod:: __exit__(exc_type, exc_value, traceback) -> None 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | -------------------------------------------------------------------------------- /tests/test_ICU.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import unittest 7 | 8 | ICU_DATA_FOLDERS_UNIX = ( 9 | "/usr/share/stpyv8", 10 | os.path.expanduser("~/.local/share/stpyv8"), 11 | ) 12 | ICU_DATA_FOLDERS_OSX = ( 13 | "/Library/Application Support/STPyV8", 14 | os.path.expanduser("~/Library/Application Support/STPyV8"), 15 | ) 16 | ICU_DATA_FOLDERS_WINDOWS = ( 17 | os.path.join(os.environ["PROGRAMDATA"], "STPyV8") 18 | if "PROGRAMDATA" in os.environ 19 | else None, 20 | os.path.join(os.environ["APPDATA"], "STPyV8") if "APPDATA" in os.environ else None, 21 | ) 22 | 23 | icu_data_folders = None 24 | if os.name in ("posix",): 25 | icu_data_folders = ( 26 | ICU_DATA_FOLDERS_OSX if sys.platform in ("darwin",) else ICU_DATA_FOLDERS_UNIX 27 | ) 28 | else: 29 | icu_data_folders = ICU_DATA_FOLDERS_WINDOWS 30 | 31 | 32 | class TestICU(unittest.TestCase): 33 | def testIcu(self): 34 | icu_paths = (os.path.join(folder, "icudtl.dat") for folder in icu_data_folders) 35 | self.assertTrue(any(os.path.exists(icu) for icu in icu_paths)) 36 | -------------------------------------------------------------------------------- /src/Isolate.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "Exception.h" 6 | 7 | class CIsolate 8 | { 9 | v8::Isolate *m_isolate; 10 | bool m_owner; 11 | void Init(bool owner); 12 | 13 | static constexpr int KB = 1024; 14 | static constexpr int MB = KB * 1024; 15 | static constexpr size_t heap_increase = 8 * MB; 16 | static constexpr size_t heap_max_increase = 64 * MB; 17 | public: 18 | CIsolate(); 19 | CIsolate(bool owner); 20 | CIsolate(v8::Isolate *isolate); 21 | ~CIsolate(void); 22 | 23 | v8::Isolate *GetIsolate(void); 24 | 25 | CJavascriptStackTracePtr GetCurrentStackTrace(int frame_limit, 26 | v8::StackTrace::StackTraceOptions options); 27 | 28 | static py::object GetCurrent(void); 29 | static size_t NearHeapLimitCallback(void* data, size_t current_heap_limit, 30 | size_t initial_heap_limit); 31 | 32 | void Enter(void) { 33 | m_isolate->Enter(); 34 | } 35 | 36 | void Leave(void) { 37 | m_isolate->Exit(); 38 | } 39 | 40 | void Dispose(void) { 41 | m_isolate->Dispose(); 42 | } 43 | 44 | bool IsLocked(void) { 45 | return v8::Locker::IsLocked(m_isolate); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/Locker.cpp: -------------------------------------------------------------------------------- 1 | #include "Locker.h" 2 | 3 | bool CLocker::s_preemption = false; 4 | 5 | void CLocker::enter(void) 6 | { 7 | Py_BEGIN_ALLOW_THREADS 8 | 9 | m_locker.reset(new v8::Locker(m_isolate.get() ? m_isolate->GetIsolate() : v8::Isolate::GetCurrent())); 10 | 11 | Py_END_ALLOW_THREADS 12 | } 13 | 14 | void CLocker::leave(void) 15 | { 16 | Py_BEGIN_ALLOW_THREADS 17 | 18 | m_locker.reset(); 19 | 20 | Py_END_ALLOW_THREADS 21 | } 22 | 23 | bool CLocker::IsLocked() 24 | { 25 | return v8::Locker::IsLocked(v8::Isolate::GetCurrent()); 26 | } 27 | 28 | void CLocker::Expose(void) 29 | { 30 | py::class_("JSLocker", py::no_init) 31 | .def(py::init<>()) 32 | .def(py::init((py::arg("isolate")))) 33 | 34 | .add_static_property("locked", &CLocker::IsLocked, 35 | "whether or not the locker is locked by the current thread.") 36 | 37 | .def("entered", &CLocker::entered) 38 | .def("enter", &CLocker::enter) 39 | .def("leave", &CLocker::leave) 40 | ; 41 | 42 | py::class_("JSUnlocker") 43 | .def("entered", &CUnlocker::entered) 44 | .def("enter", &CUnlocker::enter) 45 | .def("leave", &CUnlocker::leave) 46 | ; 47 | } 48 | -------------------------------------------------------------------------------- /src/Locker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Exception.h" 4 | #include "Context.h" 5 | #include "Utils.h" 6 | 7 | 8 | class CLocker 9 | { 10 | static bool s_preemption; 11 | 12 | std::unique_ptr m_locker; 13 | CIsolatePtr m_isolate; 14 | public: 15 | CLocker() {} 16 | CLocker(CIsolatePtr isolate) : m_isolate(isolate) {} 17 | ~CLocker() 18 | { 19 | if (NULL != m_locker.get()) 20 | m_locker.release(); 21 | } 22 | 23 | bool entered(void) 24 | { 25 | return NULL != m_locker.get(); 26 | } 27 | 28 | void enter(void); 29 | void leave(void); 30 | 31 | static bool IsLocked(); 32 | 33 | static void Expose(void); 34 | }; 35 | 36 | 37 | class CUnlocker 38 | { 39 | std::unique_ptr m_unlocker; 40 | public: 41 | bool entered(void) { 42 | return NULL != m_unlocker.get(); 43 | } 44 | 45 | void enter(void) 46 | { 47 | Py_BEGIN_ALLOW_THREADS 48 | 49 | m_unlocker.reset(new v8::Unlocker(v8::Isolate::GetCurrent())); 50 | 51 | Py_END_ALLOW_THREADS 52 | } 53 | 54 | void leave(void) 55 | { 56 | Py_BEGIN_ALLOW_THREADS 57 | 58 | m_unlocker.reset(); 59 | 60 | Py_END_ALLOW_THREADS 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /tests/test_Locker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | 6 | import STPyV8 7 | 8 | 9 | class TestLocker(unittest.TestCase): 10 | def testLocker(self): 11 | with STPyV8.JSIsolate(): 12 | self.assertFalse(STPyV8.JSLocker.locked) 13 | 14 | with STPyV8.JSLocker() as outter_locker: 15 | self.assertTrue(STPyV8.JSLocker.locked) 16 | 17 | self.assertTrue(outter_locker) 18 | 19 | with STPyV8.JSLocker() as inner_locker: 20 | self.assertTrue(STPyV8.JSLocker.locked) 21 | 22 | self.assertTrue(outter_locker) 23 | self.assertTrue(inner_locker) 24 | 25 | with STPyV8.JSUnlocker(): 26 | self.assertFalse(STPyV8.JSLocker.locked) 27 | 28 | self.assertTrue(outter_locker) 29 | self.assertTrue(inner_locker) 30 | 31 | self.assertTrue(STPyV8.JSLocker.locked) 32 | 33 | self.assertFalse(STPyV8.JSLocker.locked) 34 | 35 | locker = STPyV8.JSLocker() 36 | 37 | with STPyV8.JSContext(): 38 | self.assertRaises(RuntimeError, locker.__enter__) 39 | self.assertRaises(RuntimeError, locker.__exit__, None, None, None) 40 | 41 | del locker 42 | -------------------------------------------------------------------------------- /.github/workflows/sdist.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish Python source distribution to PyPI 3 | on: 4 | release: 5 | types: [published] 6 | jobs: 7 | build: 8 | name: Build distribution 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: 3.x 16 | - name: Install dependencies 17 | run: | 18 | python3 -m pip install --upgrade pip setuptools wheel 19 | - name: Install pypa/build 20 | run: | 21 | python3 -m pip install build --user 22 | - name: Build a source tarball 23 | run: python3 setup.py sdist 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: python-package-distributions 28 | path: dist/ 29 | publish-to-pypi: 30 | name: Publish Python source distribution to PyPI 31 | runs-on: ubuntu-latest 32 | needs: [build] 33 | environment: 34 | name: pypi 35 | url: https://pypi.org/p/stpyv8 36 | permissions: 37 | id-token: write 38 | steps: 39 | - name: Download all the dists 40 | uses: actions/download-artifact@v4 41 | with: 42 | name: python-package-distributions 43 | path: dist/ 44 | - name: Publish distribution to PyPI 45 | uses: pypa/gh-action-pypi-publish@release/v1 46 | -------------------------------------------------------------------------------- /src/utf8.h: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Nemanja Trifunovic 2 | 3 | /* 4 | Permission is hereby granted, free of charge, to any person or organization 5 | obtaining a copy of the software and accompanying documentation covered by 6 | this license (the "Software") to use, reproduce, display, distribute, 7 | execute, and transmit the Software, and to prepare derivative works of the 8 | Software, and to permit third-parties to whom the Software is furnished to 9 | do so, all subject to the following: 10 | 11 | The copyright notices in the Software and this entire statement, including 12 | the above license grant, this restriction and the following disclaimer, 13 | must be included in all copies of the Software, in whole or in part, and 14 | all derivative works of the Software, unless such copies or derivative 15 | works are solely in the form of machine-executable object code generated by 16 | a source language processor. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 21 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 22 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | 28 | #ifndef UTF8_FOR_CPP_2675DCD0_9480_4c0c_B92A_CC14C027B731 29 | #define UTF8_FOR_CPP_2675DCD0_9480_4c0c_B92A_CC14C027B731 30 | 31 | #include "utf8/checked.h" 32 | #include "utf8/unchecked.h" 33 | 34 | #endif // header guard 35 | -------------------------------------------------------------------------------- /src/Context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "Isolate.h" 7 | #include "Platform.h" 8 | #include "Wrapper.h" 9 | #include "Utils.h" 10 | 11 | 12 | class CContext; 13 | class CIsolate; 14 | 15 | typedef std::shared_ptr CContextPtr; 16 | typedef std::shared_ptr CIsolatePtr; 17 | 18 | 19 | class CContext 20 | { 21 | py::object m_global; 22 | v8::Persistent m_context; 23 | public: 24 | CContext(v8::Handle context); 25 | CContext(const CContext& context); 26 | CContext(py::object global); 27 | 28 | ~CContext() 29 | { 30 | m_context.Reset(); 31 | } 32 | 33 | v8::Handle Handle(void) const { 34 | return v8::Local::New(v8::Isolate::GetCurrent(), m_context); 35 | } 36 | 37 | py::object GetGlobal(void); 38 | 39 | py::str GetSecurityToken(void); 40 | void SetSecurityToken(py::str token); 41 | 42 | bool IsEntered(void) { 43 | return !m_context.IsEmpty(); 44 | } 45 | void Enter(void) { 46 | v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); 47 | Handle()->Enter(); 48 | } 49 | void Leave(void) { 50 | v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); 51 | Handle()->Exit(); 52 | } 53 | 54 | py::object Evaluate(const std::string& src, const std::string name = std::string(), 55 | int line = -1, int col = -1); 56 | py::object EvaluateW(const std::wstring& src, const std::wstring name = std::wstring(), 57 | int line = -1, int col = -1); 58 | 59 | static py::object GetEntered(void); 60 | static py::object GetCurrent(void); 61 | static py::object GetCalling(void); 62 | static bool InContext(void) { 63 | return v8::Isolate::GetCurrent()->InContext(); 64 | } 65 | 66 | static void Expose(void); 67 | }; 68 | -------------------------------------------------------------------------------- /src/Platform.cpp: -------------------------------------------------------------------------------- 1 | #include "libplatform/libplatform.h" 2 | 3 | #include "Platform.h" 4 | 5 | std::unique_ptr CPlatform::platform; 6 | bool CPlatform::inited = false; 7 | 8 | void CPlatform::Init() 9 | { 10 | if(inited) 11 | return; 12 | 13 | v8::V8::InitializeICUDefaultLocation(argv.c_str(), GetICUDataFile()); 14 | v8::V8::InitializeExternalStartupData(argv.c_str()); 15 | 16 | platform = v8::platform::NewDefaultPlatform(); 17 | 18 | v8::V8::InitializePlatform(platform.get()); 19 | v8::V8::Initialize(); 20 | 21 | inited = true; 22 | } 23 | 24 | const char * CPlatform::GetICUDataFile() 25 | { 26 | #if defined(_WIN32) || defined (_WIN64) 27 | boost::filesystem::path icu_data_path = getenv("APPDATA"); 28 | #else 29 | boost::filesystem::path icu_data_path = getenv("HOME"); 30 | #endif 31 | if (icu_data_user == nullptr) 32 | return nullptr; 33 | 34 | if (boost::filesystem::is_directory(icu_data_path)) { 35 | icu_data_path /= icu_data_user; 36 | 37 | std::string icu_data_path_str = icu_data_path.string(); 38 | const char *icu_data_path_ptr = icu_data_path_str.c_str(); 39 | 40 | std::ifstream ifile(icu_data_path_ptr); 41 | if (ifile.good()) 42 | return icu_data_path_ptr; 43 | } 44 | 45 | if (icu_data_system != nullptr) { 46 | #if defined(_WIN32) || defined (_WIN64) 47 | boost::filesystem::path icu_windows_data_path = getenv("PROGRAMDATA"); 48 | if (boost::filesystem::is_directory(icu_windows_data_path)) { 49 | icu_windows_data_path /= icu_data_system; 50 | 51 | std::string icu_windows_data_path_str = icu_windows_data_path.string(); 52 | const char *icu_windows_data_path_ptr = icu_windows_data_path_str.c_str(); 53 | 54 | std::ifstream ifile(icu_windows_data_path_ptr); 55 | if (ifile.good()) 56 | return icu_windows_data_path_ptr; 57 | } 58 | #else 59 | std::ifstream ifile(icu_data_system); 60 | if (ifile.good()) 61 | return icu_data_system; 62 | #endif 63 | } 64 | 65 | return nullptr; 66 | } 67 | -------------------------------------------------------------------------------- /src/Isolate.cpp: -------------------------------------------------------------------------------- 1 | #include "Context.h" 2 | #include "Wrapper.h" 3 | #include "Engine.h" 4 | 5 | #include "libplatform/libplatform.h" 6 | 7 | 8 | size_t CIsolate::NearHeapLimitCallback(void *data, 9 | size_t current_heap_limit, size_t initial_heap_limit) 10 | { 11 | v8::Isolate *isolate = (v8::Isolate *)data; 12 | 13 | if (current_heap_limit - initial_heap_limit > heap_max_increase) 14 | return current_heap_limit; 15 | 16 | isolate->AdjustAmountOfExternalAllocatedMemory(heap_increase); 17 | return current_heap_limit + heap_increase; 18 | } 19 | 20 | void CIsolate::Init(bool owner) 21 | { 22 | m_owner = owner; 23 | 24 | v8::Isolate::CreateParams create_params; 25 | create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); 26 | m_isolate = v8::Isolate::New(create_params); 27 | m_isolate->AddNearHeapLimitCallback(NearHeapLimitCallback, m_isolate); 28 | } 29 | 30 | CIsolate::CIsolate(bool owner) 31 | { 32 | CIsolate::Init(owner); 33 | } 34 | 35 | CIsolate::CIsolate() 36 | { 37 | CIsolate::Init(false); 38 | } 39 | 40 | CIsolate::CIsolate(v8::Isolate *isolate) : m_isolate(isolate), m_owner(false) 41 | { 42 | } 43 | 44 | CIsolate::~CIsolate(void) 45 | { 46 | if (m_owner) m_isolate->Dispose(); 47 | } 48 | 49 | v8::Isolate *CIsolate::GetIsolate(void) 50 | { 51 | return m_isolate; 52 | } 53 | 54 | CJavascriptStackTracePtr CIsolate::GetCurrentStackTrace(int frame_limit, 55 | v8::StackTrace::StackTraceOptions options = v8::StackTrace::kOverview) 56 | { 57 | return CJavascriptStackTrace::GetCurrentStackTrace(m_isolate, frame_limit, options); 58 | } 59 | 60 | py::object CIsolate::GetCurrent(void) 61 | { 62 | v8::Isolate *isolate = v8::Isolate::GetCurrent(); 63 | if(isolate == nullptr || (!isolate->IsInUse())) 64 | { 65 | return py::object(); 66 | } 67 | 68 | v8::HandleScope handle_scope(isolate); 69 | 70 | return !isolate ? py::object() : 71 | py::object(py::handle<>(boost::python::converter::shared_ptr_to_python( 72 | CIsolatePtr(new CIsolate(isolate))))); 73 | } 74 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. STPyV8 documentation master file 2 | 3 | .. _index: 4 | .. py:module:: STPyV8 5 | 6 | Welcome to STPyV8's documentation! 7 | ================================== 8 | 9 | STPyV8 is a Python wrapper for the Google V8 engine [#f1]_ acting as a bridge between the Python and JavaScript 10 | objects and supporting the embedding of the Google V8 engine in a Python script. 11 | 12 | Creating and entering a context to evaluate Javascript code can be done in a few lines of code. 13 | 14 | .. doctest:: 15 | 16 | >>> ctxt = JSContext() # create a context with an implicit global object 17 | >>> ctxt.enter() # enter the context (also support with statement) 18 | >>> ctxt.eval("1+2") # evalute the Javascript expression and return a native Python integer 19 | 3 20 | >>> ctxt.leave() # leave the context and release the related resources 21 | 22 | You also could invoke Javascript functions from Python, or viceversa. 23 | 24 | .. doctest:: 25 | 26 | >>> class Global(JSClass): # define a compatible Javascript class 27 | ... def hello(self): # define a method 28 | ... print("Hello World") 29 | ... 30 | >>> ctxt2 = JSContext(Global()) # create another context with the global object 31 | >>> ctxt2.enter() 32 | >>> ctxt2.eval("hello()") # call the global object from Javascript 33 | Hello World 34 | 35 | .. toctree:: 36 | :maxdepth: 2 37 | 38 | build 39 | samples 40 | api/api 41 | 42 | Indices and tables 43 | ================== 44 | 45 | * :ref:`genindex` 46 | * :ref:`modindex` 47 | * :ref:`search` 48 | 49 | .. rubric:: Footnotes 50 | 51 | .. [#f1] `Google V8 ` 52 | 53 | V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. 54 | 55 | It is used in Chrome and in Node.js, among others. It implements ECMAScript and WebAssembly, and runs 56 | on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors. 57 | 58 | V8 can run standalone, or can be embedded into any C++ application. 59 | -------------------------------------------------------------------------------- /tests/test_Thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import threading 6 | import unittest 7 | import pytest 8 | 9 | import STPyV8 10 | 11 | 12 | class TestThread(unittest.TestCase): 13 | @pytest.mark.order(1) 14 | def testMultiPythonThread(self): 15 | class Global: 16 | count = 0 17 | started = threading.Event() 18 | finished = threading.Semaphore(0) 19 | 20 | def sleep(self, ms): 21 | time.sleep(ms / 1000.0) 22 | 23 | self.count += 1 24 | 25 | g = Global() 26 | 27 | def run(): 28 | with STPyV8.JSIsolate(): 29 | with STPyV8.JSContext(g) as ctxt: 30 | ctxt.eval( 31 | """ 32 | started.wait(); 33 | 34 | for (i=0; i<10; i++) 35 | { 36 | sleep(100); 37 | } 38 | 39 | finished.release(); 40 | """ 41 | ) 42 | 43 | t = threading.Thread(target=run) 44 | t.start() 45 | 46 | now = time.time() 47 | 48 | self.assertEqual(0, g.count) 49 | 50 | g.started.set() 51 | g.finished.acquire() 52 | 53 | self.assertEqual(10, g.count) 54 | 55 | self.assertTrue((time.time() - now) >= 1) 56 | t.join() 57 | 58 | @pytest.mark.order(2) 59 | def testMultiJavascriptThread(self): 60 | class Global(STPyV8.JSContext): 61 | result = [] 62 | 63 | def add(self, value): 64 | with STPyV8.JSUnlocker(): 65 | self.result.append(value) 66 | 67 | g = Global() 68 | 69 | def run(): 70 | with STPyV8.JSIsolate(): 71 | with STPyV8.JSContext(g) as ctxt: 72 | ctxt.eval( 73 | """ 74 | for (i=0; i<10; i++) 75 | add(i); 76 | """ 77 | ) 78 | 79 | threads = [threading.Thread(target=run), threading.Thread(target=run)] 80 | 81 | with STPyV8.JSLocker(): 82 | for t in threads: 83 | t.start() 84 | 85 | for t in threads: 86 | t.join() 87 | 88 | self.assertEqual(20, len(g.result)) 89 | -------------------------------------------------------------------------------- /src/Utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef _WIN32 6 | #ifdef DEBUG 7 | # pragma warning( push ) 8 | #endif 9 | 10 | #pragma warning( disable : 4100 ) // 'identifier' : unreferenced formal parameter 11 | #pragma warning( disable : 4121 ) // 'symbol' : alignment of a member was sensitive to packing 12 | #pragma warning( disable : 4127 ) // conditional expression is constant 13 | #pragma warning( disable : 4189 ) // 'identifier' : local variable is initialized but not referenced 14 | #pragma warning( disable : 4244 ) // 'argument' : conversion from 'type1' to 'type2', possible loss of data 15 | #pragma warning( disable : 4505 ) // 'function' : unreferenced local function has been removed 16 | #pragma warning( disable : 4512 ) // 'class' : assignment operator could not be generated 17 | #pragma warning( disable : 4800 ) // 'type' : forcing value to bool 'true' or 'false' (performance warning) 18 | #pragma warning( disable : 4996 ) // 'function': was declared deprecated 19 | 20 | #ifndef _WIN64 21 | # ifndef _USE_32BIT_TIME_T 22 | # define _USE_32BIT_TIME_T 23 | # endif 24 | #endif 25 | #else 26 | # if !defined(__GNUC__) || (__GNUC__ <= 4 && __GNUC_MINOR__ < 7) 27 | #include 28 | using std::isnan; 29 | 30 | #ifndef isfinite 31 | # include 32 | namespace std { 33 | inline bool isfinite(double val) { 34 | return val <= std::numeric_limits::max(); 35 | } 36 | } 37 | # endif 38 | #endif 39 | 40 | #include 41 | #define strnicmp strncasecmp 42 | 43 | #define _countof(array) (sizeof(array)/sizeof(array[0])) 44 | #endif // _WIN32 45 | 46 | #if defined(__APPLE__) && !defined(isalnum) 47 | #undef LONG_BIT 48 | #include 49 | #undef isalnum 50 | #undef isalpha 51 | #undef islower 52 | #undef isspace 53 | #undef isupper 54 | #undef tolower 55 | #undef toupper 56 | #endif // __APPLE__ 57 | 58 | # if BOOST_VERSION / 100 % 1000 < 50 59 | # undef TIME_UTC 60 | # endif 61 | 62 | #include 63 | 64 | #include 65 | namespace py = boost::python; 66 | 67 | #ifdef _WIN32 68 | #undef FP_NAN 69 | #undef FP_INFINITE 70 | #undef FP_ZERO 71 | #undef FP_SUBNORMAL 72 | #undef FP_NORMAL 73 | #endif 74 | 75 | #ifdef _WIN32 76 | #ifdef DEBUG 77 | # pragma warning( pop ) 78 | #endif 79 | #endif 80 | 81 | #if defined(__GNUC__) 82 | #define UNUSED_VAR(x) x __attribute__((unused)) 83 | #elif defined(__APPLE__) 84 | #define UNUSED_VAR(x) x __unused 85 | #else 86 | #define UNUSED_VAR(x) x 87 | #endif 88 | 89 | #define PyInt_Check PyLong_Check 90 | #define PyInt_AsUnsignedLongMask PyLong_AsUnsignedLong 91 | #define PySlice_Cast(obj) obj 92 | 93 | v8::Handle ToString(const std::string& str); 94 | v8::Handle ToString(const std::wstring& str); 95 | v8::Handle ToString(py::object str); 96 | 97 | v8::Handle DecodeUtf8(const std::string& str); 98 | const std::string EncodeUtf8(const std::wstring& str); 99 | 100 | struct CPythonGIL 101 | { 102 | PyGILState_STATE m_state; 103 | 104 | CPythonGIL(); 105 | ~CPythonGIL(); 106 | }; 107 | -------------------------------------------------------------------------------- /docs/source/build.rst: -------------------------------------------------------------------------------- 1 | .. _build: 2 | 3 | Build and Install 4 | ================= 5 | 6 | Requirements 7 | ------------ 8 | 9 | Boost 10 | ^^^^^ 11 | 12 | STPyV8 makes use of `Boost.Python `_ for interoperability. 13 | 14 | Most Linux distributions provide easy to install Boost packages and this is the suggested way to install the library. 15 | If packages are not available for your distribution download or install `the latest version 16 | `_ of Boost and follow `the getting started guide 17 | `_ to build the library. 18 | 19 | Python wheels 20 | ^^^^^^^^^^^^^ 21 | 22 | STPyV8 is avaliable on PyPI (starting from release v12.0.267.16) and officially supports 23 | Python 3.9+ 24 | 25 | .. code-block:: sh 26 | $ pip install stpyv8 27 | 28 | Be aware that, starting from STPyV8 v12.0.267.14, installing boost-python and some other 29 | boost dependencies is not required anymore while it is still required if you're installing 30 | older versions (see later for details). Most Linux distributions and MacOS provide easy to 31 | install Boost packages and this is the suggested way to install the library in case you 32 | need it. 33 | 34 | If you are planning to install a version older than v12.0.267.16 you should use one of 35 | the Python wheels provided at `STPyV8 Releases `. 36 | The wheels are automatically generated using Github Actions and multiple platforms and 37 | Python versions are supported. In such case, you need to download the zip file for the 38 | proper platform and Python version. Each zip file contains the ICU data file icudtl.dat 39 | and the wheel itself. First of all you should copy icudtl.data to the STPyV8 ICU data 40 | folder (Linux: /usr/share/stpyv8, MacOS: /Library/Application Support/STPyV8/) and then 41 | install/upgrade STPyV8 using pip. 42 | 43 | For instance, the following steps show how to install on MacOS 44 | 45 | .. code-block:: sh 46 | 47 | $ unzip stpyv8-macos-10.15-python-3.9.zip 48 | Archive: stpyv8-macos-10.15-python-3.9.zip 49 | inflating: stpyv8-macos-10.15-3.9/icudtl.dat 50 | inflating: stpyv8-macos-10.15-3.9/stpyv8-9.9.115.8-cp39-cp39-macosx_10_15_x86_64.whl 51 | $ cd stpyv8-macos-10.15-3.9 52 | $ sudo mv icudtl.dat /Library/Application\ Support/STPyV8 53 | $ pip install --upgrade stpyv8-9.9.115.8-cp39-cp39-macosx_10_15_x86_64.whl 54 | Processing ./stpyv8-9.9.115.8-cp39-cp39-macosx_10_15_x86_64.whl 55 | Installing collected packages: stpyv8 56 | Successfully installed stpyv8-9.9.115.8 57 | 58 | If no wheels are provided for your platform and Python version you are required to build STPyV8. 59 | 60 | Build Steps 61 | ----------- 62 | 63 | .. code-block:: sh 64 | 65 | $ python setup.py build 66 | $ sudo python setup.py install 67 | 68 | Optionally you can run STPyV8 tests (pytest is required) 69 | 70 | .. code-block:: sh 71 | 72 | $ pytest tests 73 | 74 | If you want to build a distribution package for Linux/Mac run setup.py with the bdist command 75 | 76 | .. code-block:: sh 77 | 78 | $ python setup.py bdist 79 | -------------------------------------------------------------------------------- /src/Engine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "Utils.h" 9 | 10 | class CScript; 11 | 12 | typedef std::shared_ptr CScriptPtr; 13 | 14 | class CEngine 15 | { 16 | v8::Isolate *m_isolate; 17 | 18 | static uintptr_t CalcStackLimitSize(uintptr_t size); 19 | protected: 20 | CScriptPtr InternalCompile(v8::Handle src, v8::Handle name, int line, int col); 21 | 22 | static void TerminateAllThreads(void); 23 | 24 | static void ReportFatalError(const char* location, const char* message); 25 | static void ReportMessage(v8::Handle message, v8::Handle data); 26 | public: 27 | CEngine(v8::Isolate *isolate = NULL) : m_isolate(isolate ? isolate : v8::Isolate::GetCurrent()) {} 28 | 29 | CScriptPtr Compile(const std::string& src, const std::string name = std::string(), 30 | int line = -1, int col = -1) 31 | { 32 | v8::HandleScope scope(m_isolate); 33 | 34 | return InternalCompile(ToString(src), ToString(name), line, col); 35 | } 36 | 37 | CScriptPtr CompileW(const std::wstring& src, const std::wstring name = std::wstring(), 38 | int line = -1, int col = -1) 39 | { 40 | v8::HandleScope scope(m_isolate); 41 | 42 | return InternalCompile(ToString(src), ToString(name), line, col); 43 | } 44 | 45 | void RaiseError(v8::TryCatch& try_catch); 46 | public: 47 | static void Expose(void); 48 | 49 | static const std::string GetVersion(void) { 50 | return v8::V8::GetVersion(); 51 | } 52 | static bool SetMemoryLimit(int max_young_space_size, int max_old_space_size, int max_executable_size); 53 | static void SetStackLimit(uintptr_t stack_limit_size); 54 | 55 | py::object ExecuteScript(v8::Handle script); 56 | 57 | static void SetFlags(const std::string& flags) { 58 | v8::V8::SetFlagsFromString(flags.c_str(), flags.size()); 59 | } 60 | 61 | static void SetSerializeEnable(bool value); 62 | static bool IsSerializeEnabled(void); 63 | 64 | static py::object Serialize(void); 65 | static void Deserialize(py::object snapshot); 66 | static bool IsDead(void); 67 | }; 68 | 69 | class CScript 70 | { 71 | v8::Isolate *m_isolate; 72 | CEngine& m_engine; 73 | 74 | v8::Persistent m_source; 75 | v8::Persistent m_script; 76 | public: 77 | CScript(v8::Isolate *isolate, CEngine& engine, v8::Persistent& source, v8::Handle script) 78 | : m_isolate(isolate), m_engine(engine), m_source(m_isolate, source), m_script(m_isolate, script) 79 | { 80 | 81 | } 82 | 83 | CScript(const CScript& script) 84 | : m_isolate(script.m_isolate), m_engine(script.m_engine) 85 | { 86 | v8::HandleScope handle_scope(m_isolate); 87 | 88 | m_source.Reset(m_isolate, script.Source()); 89 | m_script.Reset(m_isolate, script.Script()); 90 | } 91 | 92 | ~CScript() 93 | { 94 | m_source.Reset(); 95 | m_script.Reset(); 96 | } 97 | 98 | v8::Handle Source() const { 99 | return v8::Local::New(m_isolate, m_source); 100 | } 101 | 102 | v8::Handle Script() const { 103 | return v8::Local::New(m_isolate, m_script); 104 | } 105 | 106 | const std::string GetSource(void) const; 107 | 108 | py::object Run(void); 109 | }; 110 | -------------------------------------------------------------------------------- /docs/source/api/engine.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: STPyV8 2 | :noindex: 3 | 4 | .. testsetup:: * 5 | 6 | from STPyV8 import * 7 | 8 | .. _engine: 9 | 10 | Javascript Engine 11 | ========================== 12 | 13 | Besides executing Javascript code with the method :py:meth:`JSContext.eval`, you could create a new :py:class:`JSEngine` 14 | instance and compile the Javascript code using the method :py:meth:`JSEngine.compile`. A :py:class:`JSScript` object is 15 | returned. This object allows code inspection and provides the method :py:meth:`JSScript.run` that allows to execute the 16 | compiled code. 17 | 18 | The class :py:class:`JSEngine` also provides the following static properties and methods 19 | 20 | ======================================= ======================================================= 21 | Property or Method Description 22 | ======================================= ======================================================= 23 | :py:attr:`JSEngine.version` Get the compiled v8 version 24 | :py:attr:`JSEngine.dead` Check if V8 is dead and therefore unusable 25 | :py:meth:`JSEngine.dispose` Releases any resources used by v8 26 | :py:meth:`JSEngine.terminateAllThreads` Forcefully terminate all the current JavaScript threads 27 | ======================================= ======================================================= 28 | 29 | 30 | Compile Script and Control Engine 31 | --------------------------------- 32 | 33 | When you use the method :py:meth:`JSEngine.compile` to compile a Javascript code, the V8 engine will parse the syntax and 34 | store the AST [#f1]_ in a :py:class:`JSScript` object. You could then execute it with the method :py:meth:`JSScript.run` or access 35 | the source code with the attribute :py:attr:`JSScript.source`. 36 | 37 | .. testcode:: 38 | 39 | with JSContext() as ctxt: 40 | with JSEngine() as engine: 41 | s = engine.compile("1+2") 42 | 43 | print(s.source) # "1+2" 44 | print(s.run()) # 3 45 | 46 | .. testoutput:: 47 | :hide: 48 | 49 | 1+2 50 | 3 51 | 52 | If you need reuse the script in different contexts, you could refer to the :ref:`jsext`. 53 | 54 | 55 | JSEngine - the backend Javascript engine 56 | ---------------------------------------- 57 | .. autoclass:: JSEngine 58 | :members: 59 | :inherited-members: 60 | :exclude-members: compile, precompile 61 | 62 | .. automethod:: compile(source, name = '', line = -1, col = -1) -> JSScript object 63 | 64 | Compile the Javascript code to a :py:class:`JSScript` object, which could be execute many times or visit it's AST. 65 | 66 | :param source: the Javascript code to be compiled 67 | :type source: str or unicode 68 | :param str name: the name of the Javascript code 69 | :param integer line: the start line number of the Javascript code 70 | :param integer col: the start column number of the Javascript code 71 | :rtype: a compiled :py:class:`JSScript` object 72 | 73 | .. automethod:: __enter__() -> JSEngine object 74 | 75 | .. automethod:: __exit__(exc_type, exc_value, traceback) -> None 76 | 77 | .. py:attribute:: version 78 | 79 | Get the V8 engine version 80 | 81 | .. py:attribute:: dead 82 | 83 | Check if V8 is dead and therefore unusable. 84 | 85 | 86 | JSScript - the compiled script 87 | ------------------------------ 88 | .. autoclass:: JSScript 89 | :members: 90 | :inherited-members: 91 | :exclude-members: run, visit 92 | 93 | .. automethod:: run() -> object 94 | 95 | .. toctree:: 96 | :maxdepth: 2 97 | 98 | .. rubric:: Footnotes 99 | 100 | .. [#f1] `Abstract Syntax Tree (AST) `_ is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code. The syntax is 'abstract' in the sense that it does not represent every detail that appears in the real syntax. For instance, grouping parentheses are implicit in the tree structure, and a syntactic construct such as an if-condition-then expression may be denoted by a single node with two branches. 101 | -------------------------------------------------------------------------------- /src/Utils.cpp: -------------------------------------------------------------------------------- 1 | #ifndef _WIN32 2 | #include "Python.h" 3 | #endif 4 | 5 | #include "Utils.h" 6 | 7 | #include 8 | #include 9 | 10 | #include "utf8.h" 11 | //#include "Locker.h" //TODO port me 12 | 13 | v8::Handle ToString(const std::string& str) 14 | { 15 | v8::EscapableHandleScope scope(v8::Isolate::GetCurrent()); 16 | 17 | return scope.Escape( 18 | v8::String::NewFromUtf8( 19 | v8::Isolate::GetCurrent(), 20 | str.c_str(), 21 | v8::NewStringType::kNormal, 22 | str.size()) 23 | .ToLocalChecked()); 24 | } 25 | 26 | v8::Handle ToString(const std::wstring& str) 27 | { 28 | v8::EscapableHandleScope scope(v8::Isolate::GetCurrent()); 29 | 30 | if (sizeof(wchar_t) == sizeof(uint16_t)) 31 | { 32 | return scope.Escape( 33 | v8::String::NewFromTwoByte( 34 | v8::Isolate::GetCurrent(), 35 | reinterpret_cast(str.c_str()), 36 | v8::NewStringType::kNormal, 37 | str.size()) 38 | .ToLocalChecked()); 39 | } 40 | 41 | std::vector data(str.size()+1); 42 | 43 | for (size_t i=0; i ToString(py::object str) 60 | { 61 | v8::EscapableHandleScope scope(v8::Isolate::GetCurrent()); 62 | 63 | if (PyBytes_CheckExact(str.ptr())) 64 | { 65 | return scope.Escape( 66 | v8::String::NewFromUtf8( 67 | v8::Isolate::GetCurrent(), 68 | PyBytes_AS_STRING(str.ptr()), 69 | v8::NewStringType::kNormal, 70 | PyBytes_GET_SIZE(str.ptr())) 71 | .ToLocalChecked()); 72 | } 73 | 74 | if (PyUnicode_CheckExact(str.ptr())) 75 | { 76 | int kind = PyUnicode_KIND(str.ptr()); 77 | void *dp = PyUnicode_DATA(str.ptr()); 78 | 79 | Py_ssize_t len = PyUnicode_GET_LENGTH(str.ptr()); 80 | std::vector data(len + 1); 81 | 82 | for (Py_ssize_t i = 0; i < len; i++) { 83 | data[i] = (uint16_t) PyUnicode_READ(kind, dp, i); 84 | } 85 | 86 | data[len] = 0; 87 | 88 | return scope.Escape( 89 | v8::String::NewFromTwoByte( 90 | v8::Isolate::GetCurrent(), 91 | &data[0], 92 | v8::NewStringType::kNormal, 93 | len) 94 | .ToLocalChecked()); 95 | } 96 | 97 | return ToString(py::object(py::handle<>(::PyObject_Str(str.ptr())))); 98 | } 99 | 100 | v8::Handle DecodeUtf8(const std::string& str) 101 | { 102 | v8::EscapableHandleScope scope(v8::Isolate::GetCurrent()); 103 | 104 | std::vector data; 105 | 106 | try 107 | { 108 | utf8::utf8to16(str.begin(), str.end(), std::back_inserter(data)); 109 | 110 | return scope.Escape(v8::String::NewFromTwoByte(v8::Isolate::GetCurrent(), &data[0], v8::NewStringType::kNormal, data.size()).ToLocalChecked()); 111 | } 112 | catch (const std::exception&) 113 | { 114 | return scope.Escape(v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), str.c_str(), v8::NewStringType::kNormal, str.size()).ToLocalChecked()); 115 | } 116 | } 117 | 118 | const std::string EncodeUtf8(const std::wstring& str) 119 | { 120 | std::vector data; 121 | 122 | if (sizeof(wchar_t) == sizeof(uint16_t)) 123 | { 124 | utf8::utf16to8(str.begin(), str.end(), std::back_inserter(data)); 125 | } 126 | else 127 | { 128 | utf8::utf32to8(str.begin(), str.end(), std::back_inserter(data)); 129 | } 130 | 131 | return std::string((const char *) &data[0], data.size()); 132 | } 133 | 134 | 135 | CPythonGIL::CPythonGIL() 136 | { 137 | m_state = ::PyGILState_Ensure(); 138 | } 139 | 140 | CPythonGIL::~CPythonGIL() 141 | { 142 | ::PyGILState_Release(m_state); 143 | } 144 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/STPyV8.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/STPyV8.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/STPyV8" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/STPyV8" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import platform 4 | 5 | STPYV8_HOME = os.path.dirname(os.path.realpath(__file__)) 6 | DEPOT_HOME = os.environ.get("DEPOT_HOME", os.path.join(STPYV8_HOME, "depot_tools")) 7 | V8_HOME = os.environ.get("V8_HOME", os.path.join(STPYV8_HOME, "v8")) 8 | 9 | V8_GIT_URL = "https://chromium.googlesource.com/v8/v8.git" 10 | V8_GIT_TAG_STABLE = "13.1.201.22" 11 | V8_GIT_TAG_MASTER = "master" 12 | V8_GIT_TAG = V8_GIT_TAG_STABLE 13 | DEPOT_GIT_URL = "https://chromium.googlesource.com/chromium/tools/depot_tools.git" 14 | 15 | STPYV8_VERSION = V8_GIT_TAG_STABLE 16 | 17 | v8_deps_linux = os.environ.get("V8_DEPS_LINUX", "1") in ("1",) 18 | 19 | os.environ["PATH"] = f"{os.environ.get('PATH', '')}:{DEPOT_HOME}" 20 | 21 | gn_args = { 22 | "dcheck_always_on": "false", 23 | "is_component_build": "false", 24 | "is_debug": "true" if os.environ.get("STPYV8_DEBUG") else "false", 25 | "treat_warnings_as_errors": "false", 26 | "use_custom_libcxx": "false", 27 | "v8_deprecation_warnings": "true", 28 | "v8_enable_disassembler": "false", 29 | "v8_enable_i18n_support": "true", 30 | "v8_enable_pointer_compression": "false", 31 | "v8_enable_31bit_smis_on_64bit_arch": "false", 32 | "v8_imminent_deprecation_warnings": "true", 33 | "v8_monolithic": "true", 34 | "v8_use_external_startup_data": "false", 35 | } 36 | 37 | source_files = [ 38 | "Exception.cpp", 39 | "Platform.cpp", 40 | "Isolate.cpp", 41 | "Context.cpp", 42 | "Engine.cpp", 43 | "Wrapper.cpp", 44 | "Locker.cpp", 45 | "Utils.cpp", 46 | "STPyV8.cpp", 47 | ] 48 | 49 | 50 | macros = [("BOOST_PYTHON_STATIC_LIB", None)] 51 | include_dirs = set() 52 | library_dirs = set() 53 | libraries = [] 54 | extra_compile_args = [] 55 | extra_link_args = [] 56 | 57 | include_dirs.add(os.path.join(V8_HOME, "include")) 58 | library_dirs.add( 59 | os.path.join(V8_HOME, os.path.join("out.gn", "x64.release.sample", "obj")) 60 | ) 61 | 62 | BOOST_PYTHON_LIB_SHORT = f"boost_python{sys.version_info.major}" 63 | BOOST_PYTHON_LIB_LONG = f"boost_python{sys.version_info.major}{sys.version_info.minor}" 64 | 65 | BOOST_PYTHON_UBUNTU_MATRIX = { 66 | "default": BOOST_PYTHON_LIB_LONG, 67 | "18.04": BOOST_PYTHON_LIB_SHORT, 68 | "20.04": f"{BOOST_PYTHON_LIB_SHORT}8", 69 | "22.04": f"{BOOST_PYTHON_LIB_SHORT}10", 70 | } 71 | 72 | 73 | def get_libboost_python_name(): 74 | if not os.path.exists("/etc/lsb-release"): 75 | return BOOST_PYTHON_UBUNTU_MATRIX["default"] 76 | 77 | platform_info = {} 78 | 79 | with open("/etc/lsb-release", encoding="utf-8", mode="r") as fd: 80 | for line in fd.readlines(): 81 | s = line.strip() 82 | p = s.split("=") 83 | 84 | if len(p) < 2: 85 | continue 86 | 87 | platform_info[p[0]] = p[1] 88 | 89 | if "DISTRIB_ID" not in platform_info: 90 | return BOOST_PYTHON_UBUNTU_MATRIX["default"] 91 | 92 | if platform_info["DISTRIB_ID"].lower() not in ("ubuntu",): 93 | return BOOST_PYTHON_UBUNTU_MATRIX["default"] 94 | 95 | release = platform_info["DISTRIB_RELEASE"] 96 | 97 | if release not in BOOST_PYTHON_UBUNTU_MATRIX: 98 | return BOOST_PYTHON_UBUNTU_MATRIX["default"] 99 | 100 | return BOOST_PYTHON_UBUNTU_MATRIX[release] 101 | 102 | 103 | STPYV8_BOOST_PYTHON = os.getenv( 104 | "STPYV8_BOOST_PYTHON", default=get_libboost_python_name() 105 | ) 106 | 107 | if os.name in ("nt",): 108 | include_dirs.add(os.path.join(V8_HOME, "include")) 109 | library_dirs.add(os.path.join(V8_HOME, "out.gn", "x64.release.sample", "obj")) 110 | 111 | if "BOOST_ROOT" in os.environ: 112 | include_dirs.add(os.environ.get("BOOST_ROOT")) 113 | library_dirs.add(os.path.join(os.environ["BOOST_ROOT"], "stage", "lib")) 114 | 115 | if "Python_ROOT_DIR" in os.environ: 116 | include_dirs.add(os.path.join(os.environ["Python_ROOT_DIR"], "include")) 117 | library_dirs.add(os.path.join(os.environ["Python_ROOT_DIR"], "libs")) 118 | 119 | libraries += ["winmm", "ws2_32", "Advapi32", "dbghelp", "v8_monolith"] 120 | extra_compile_args += ["/O2", "/GL", "/MT", "/EHsc", "/Gy", "/Zi", "/std:c++20", "/Zc:__cplusplus"] 121 | extra_link_args += ["/DLL", "/OPT:REF", "/OPT:ICF", "/MACHINE:X64"] 122 | macros += [ 123 | ("HAVE_SNPRINTF", None), 124 | ] 125 | 126 | os.environ["DEPOT_TOOLS_WIN_TOOLCHAIN"] = "0" 127 | 128 | elif os.name in ("posix",): 129 | libraries = [ 130 | "boost_system", 131 | "boost_iostreams", 132 | "boost_filesystem", 133 | "v8_monolith", 134 | STPYV8_BOOST_PYTHON.replace(".", ""), 135 | ] 136 | 137 | extra_compile_args.append("-Wno-strict-aliasing") 138 | extra_compile_args.append("-Wno-array-bounds") 139 | 140 | if platform.system() in ("Linux",): 141 | libraries.append("rt") 142 | extra_compile_args.append("-std=c++2a") 143 | else: 144 | extra_compile_args.append("-std=c++20") 145 | extra_link_args.append("-headerpad_max_install_names") 146 | 147 | 148 | GN_ARGS = " ".join(f"{key}={value}" for key, value in gn_args.items()) 149 | 150 | include_dirs = list(include_dirs) 151 | library_dirs = list(library_dirs) 152 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\STPyV8.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\STPyV8.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /tests/test_Context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | 6 | import STPyV8 7 | 8 | 9 | class TestContext(unittest.TestCase): 10 | def testEval(self): 11 | with STPyV8.JSContext() as context: 12 | self.assertEqual(2, context.eval("1+1")) 13 | self.assertEqual("Hello world", context.eval("'Hello ' + 'world'")) 14 | 15 | def testMultiNamespace(self): 16 | self.assertTrue(not bool(STPyV8.JSContext.inContext)) 17 | self.assertTrue(not bool(STPyV8.JSContext.entered)) 18 | 19 | class Global: 20 | name = "global" 21 | 22 | g = Global() 23 | 24 | with STPyV8.JSContext(g) as ctxt: 25 | self.assertTrue(ctxt) 26 | self.assertTrue(bool(STPyV8.JSContext.inContext)) 27 | self.assertEqual(g.name, str(STPyV8.JSContext.entered.locals.name)) 28 | 29 | class Local: 30 | name = "local" 31 | 32 | l = Local() 33 | 34 | with STPyV8.JSContext(l): 35 | self.assertTrue(bool(STPyV8.JSContext.inContext)) 36 | self.assertEqual(l.name, str(STPyV8.JSContext.entered.locals.name)) 37 | 38 | self.assertTrue(bool(STPyV8.JSContext.inContext)) 39 | self.assertEqual(g.name, str(STPyV8.JSContext.current.locals.name)) 40 | 41 | self.assertTrue(not bool(STPyV8.JSContext.entered)) 42 | self.assertTrue(not bool(STPyV8.JSContext.inContext)) 43 | 44 | def testMultiContext(self): 45 | with STPyV8.JSContext() as ctxt0: 46 | ctxt0.securityToken = "password" 47 | 48 | global0 = ctxt0.locals 49 | global0.custom = 1234 50 | 51 | self.assertEqual(1234, int(global0.custom)) 52 | 53 | with STPyV8.JSContext() as ctxt1: 54 | ctxt1.securityToken = ctxt0.securityToken 55 | 56 | global1 = ctxt1.locals 57 | global1.custom = 1234 58 | 59 | with ctxt0: 60 | self.assertEqual(1234, int(global0.custom)) 61 | 62 | self.assertEqual(1234, int(global1.custom)) 63 | 64 | def testPromiseResolved(self): 65 | with STPyV8.JSContext() as ctxt: 66 | ctxt.eval( 67 | """ 68 | var message; 69 | let done = true; 70 | 71 | const isItDoneYet = new Promise((resolve, reject) => { 72 | if (done) { 73 | const workDone = 'Here is the thing I built' 74 | resolve(workDone) 75 | } else { 76 | const why = 'Still working on something else' 77 | reject(why) 78 | } 79 | }) 80 | 81 | const checkIfItsDone = () => { 82 | isItDoneYet.then(ok => { 83 | message = ok; 84 | }).catch(err => { 85 | message = err; 86 | }) 87 | } 88 | 89 | checkIfItsDone() 90 | """ 91 | ) 92 | 93 | self.assertEqual("Here is the thing I built", ctxt.locals.message) 94 | 95 | def testPromiseRejected(self): 96 | with STPyV8.JSContext() as ctxt: 97 | ctxt.eval( 98 | """ 99 | var message; 100 | let done = false; 101 | 102 | const isItDoneYet = new Promise((resolve, reject) => { 103 | if (done) { 104 | const workDone = 'Here is the thing I built' 105 | resolve(workDone) 106 | } else { 107 | const why = 'Still working on something else' 108 | reject(why) 109 | } 110 | }) 111 | 112 | const checkIfItsDone = () => { 113 | isItDoneYet.then(ok => { 114 | message = ok; 115 | }).catch(err => { 116 | message = err; 117 | }) 118 | } 119 | 120 | checkIfItsDone() 121 | """ 122 | ) 123 | 124 | self.assertEqual("Still working on something else", ctxt.locals.message) 125 | 126 | def testSecurityChecks(self): 127 | with STPyV8.JSContext() as env1: 128 | env1.securityToken = "foo" 129 | 130 | # Create a function in env1. 131 | env1.eval("spy = function(){return spy;}") 132 | 133 | spy = env1.locals.spy 134 | 135 | self.assertTrue(isinstance(spy, STPyV8.JSFunction)) 136 | 137 | # Create another function accessing global objects. 138 | env1.eval("spy2 = function(){return 123;}") 139 | 140 | spy2 = env1.locals.spy2 141 | 142 | self.assertTrue(isinstance(spy2, STPyV8.JSFunction)) 143 | 144 | # Switch to env2 in the same domain and invoke spy on env2. 145 | env2 = STPyV8.JSContext() 146 | env2.securityToken = "foo" 147 | 148 | with env2: 149 | result = spy.apply(env2.locals) 150 | self.assertTrue(isinstance(result, STPyV8.JSFunction)) 151 | 152 | env2.securityToken = "bar" 153 | 154 | # FIXME 155 | # Call cross_domain_call, it should throw an exception 156 | # with env2: 157 | # self.assertRaises(STPyV8.JSError, spy2.apply, env2.locals) 158 | -------------------------------------------------------------------------------- /.github/workflows/osx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: MacOSX build/release workflow 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | branches: [master] 9 | jobs: 10 | build-v8: 11 | # If Google V8 is in the workflow cache, don't build it. 12 | # Cloning the repository is still necessary in any case 13 | # to calculate the hash for the cache key 14 | name: Build Google V8 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [macos-13] 19 | outputs: 20 | v8-hash: ${{ steps.build-v8.outputs.v8-hash }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Clone Google V8 25 | run: | 26 | python -m pip install wheel setuptools 27 | echo "::group::Clone Google V8" 28 | python setup.py checkout_v8 29 | echo "::endgroup::" 30 | - name: Restore Google V8 from cache 31 | id: restore-v8 32 | uses: actions/cache/restore@main 33 | with: 34 | path: | 35 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 36 | v8/out.gn/x64.release.sample/icudtl.dat 37 | v8/include 38 | key: ${{ matrix.os }}-build-v8-${{ hashFiles('v8/src/**') }} 39 | - name: Build Google V8 40 | id: build-v8 41 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 42 | continue-on-error: false 43 | run: | 44 | echo "v8-hash=${{ hashFiles('v8/src/**') }}" >> "$GITHUB_OUTPUT" 45 | python -m pip install wheel 46 | echo "::group::v8" 47 | python setup.py v8 48 | echo "::endgroup::" 49 | - name: Save Google V8 to cache 50 | uses: actions/cache/save@main 51 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 52 | with: 53 | # Save compiled binary and header files. This will save an 54 | # additional clone of Google V8 for the linker 55 | path: | 56 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 57 | v8/out.gn/x64.release.sample/icudtl.dat 58 | v8/include 59 | key: ${{ matrix.os }}-build-v8-${{ hashFiles('v8/src/**') }} 60 | build: 61 | name: Build OSX wheel (Python ${{ matrix.python-version }}) 62 | needs: build-v8 63 | runs-on: ${{ matrix.os }} 64 | strategy: 65 | matrix: 66 | os: [macos-13] 67 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 68 | boost-version: [1.87.0] 69 | boost-version-snake: ['1_87_0'] 70 | steps: 71 | - name: Checkout repository 72 | uses: actions/checkout@v4 73 | - name: Set up Python 74 | uses: actions/setup-python@v5 75 | with: 76 | python-version: ${{ matrix.python-version }} 77 | - name: Download Boost 78 | id: download-boost 79 | uses: suisei-cn/actions-download-file@v1.6.0 80 | with: 81 | url: https://archives.boost.io/release/${{ matrix.boost-version }}/source/boost_${{matrix.boost-version-snake }}.zip 82 | - name: Install Boost 83 | id: install-boost 84 | run: | 85 | unzip -q ${{ steps.download-boost.outputs.filename }} 86 | cd boost_${{ matrix.boost-version-snake }} 87 | ./bootstrap.sh 88 | sudo ./b2 install -j 8 --with-system --with-python --with-filesystem --with-iostreams --with-date_time --with-thread 89 | - name: Restore Google V8 from cache 90 | id: restore-v8 91 | uses: actions/cache/restore@main 92 | with: 93 | path: | 94 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 95 | v8/out.gn/x64.release.sample/icudtl.dat 96 | v8/include 97 | key: ${{ matrix.os }}-build-v8-${{ needs.build-v8.outputs.v8-hash }} 98 | - name: Install dependencies 99 | run: | 100 | pip install --upgrade pip setuptools delocate wheel pytest pytest-order 101 | - name: Build wheel 102 | run: | 103 | python setup.py sdist bdist_wheel --skip-build-v8 -d stpyv8-${{ matrix.os }}-${{ matrix.python-version }} 104 | env: 105 | ARCHFLAGS: -arch x86_64 106 | _PYTHON_HOST_PLATFORM: macosx-10.9-x86_64 107 | - name: Repair wheel 108 | run: | 109 | delocate-listdeps stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/stpyv8*.whl 110 | delocate-wheel --require-archs x86_64 stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/stpyv8*.whl 111 | - name: Install wheel 112 | run: | 113 | python -m pip install stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/*.whl 114 | - name: Test wheel 115 | run: | 116 | pytest -v 117 | - name: Upload wheel 118 | uses: actions/upload-artifact@v4 119 | with: 120 | name: stpyv8-${{ matrix.os }}-python${{ matrix.python-version }} 121 | path: stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/*.whl 122 | - name: Release 123 | uses: softprops/action-gh-release@v2 124 | if: ${{ github.event_name == 'release' }} 125 | with: 126 | files: stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/*.whl 127 | token: ${{ secrets.GITHUB_TOKEN }} 128 | pypi-publish: 129 | name: Upload release to PyPI 130 | if: ${{ github.event_name == 'release' }} 131 | needs: build 132 | runs-on: ubuntu-latest 133 | environment: 134 | name: pypi 135 | url: https://pypi.org/p/stpyv8 136 | permissions: 137 | id-token: write 138 | steps: 139 | - name: Download wheels 140 | uses: actions/download-artifact@v4 141 | with: 142 | path: stpyv8-macos-dist 143 | pattern: stpyv8-macos* 144 | merge-multiple: true 145 | - name: Publish wheels to PyPI 146 | uses: pypa/gh-action-pypi-publish@release/v1 147 | with: 148 | packages-dir: stpyv8-macos-dist 149 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linux build/release workflow 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | branches: [master] 9 | jobs: 10 | build-v8: 11 | # If Google V8 is in the workflow cache, don't build it. 12 | # Cloning the repository is still necessary in any case 13 | # to calculate the hash for the cache key 14 | name: Build Google V8 15 | runs-on: ubuntu-20.04 16 | outputs: 17 | v8-hash: ${{ steps.build-v8.outputs.v8-hash }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Clone Google V8 22 | run: | 23 | python -m pip install wheel 24 | echo "::group::Clone Google V8" 25 | python setup.py checkout_v8 26 | echo "::endgroup::" 27 | - name: Restore Google V8 from cache 28 | id: restore-v8 29 | uses: actions/cache/restore@main 30 | with: 31 | path: | 32 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 33 | v8/out.gn/x64.release.sample/icudtl.dat 34 | v8/include 35 | key: linux-build-v8-${{ hashFiles('v8/src/**') }} 36 | - name: Build Google V8 37 | id: build-v8 38 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 39 | continue-on-error: false 40 | run: | 41 | echo "v8-hash=${{ hashFiles('v8/src/**') }}" >> "$GITHUB_OUTPUT" 42 | python -m pip install wheel 43 | echo "::group::v8" 44 | python setup.py v8 45 | echo "::endgroup::" 46 | - name: Save Google V8 to cache 47 | uses: actions/cache/save@main 48 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 49 | with: 50 | # Save compiled binary and header files. This will save an 51 | # additional clone of Google V8 for the linker 52 | path: | 53 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 54 | v8/out.gn/x64.release.sample/icudtl.dat 55 | v8/include 56 | key: linux-build-v8-${{ hashFiles('v8/src/**') }} 57 | build: 58 | name: Build Linux wheel (Python ${{ matrix.python-version }}) 59 | needs: build-v8 60 | runs-on: ${{ matrix.os }} 61 | env: 62 | DIST_NAME: stpyv8-linux-py${{ matrix.python-version }} 63 | STPYV8_BOOST_PYTHON: boost_python${{ matrix.python-version }} 64 | strategy: 65 | matrix: 66 | os: [ubuntu-20.04] 67 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 68 | boost-version: [1.87.0] 69 | boost-version-snake: ['1_87_0'] 70 | steps: 71 | - name: Checkout repository 72 | uses: actions/checkout@v4 73 | - name: Set up Python 74 | uses: actions/setup-python@v5 75 | with: 76 | python-version: ${{ matrix.python-version }} 77 | - name: Download Boost 78 | id: download-boost 79 | uses: suisei-cn/actions-download-file@v1.6.0 80 | with: 81 | url: https://archives.boost.io/release/${{ matrix.boost-version }}/source/boost_${{matrix.boost-version-snake }}.zip 82 | - name: Install Boost 83 | id: install-boost 84 | run: | 85 | unzip -q ${{ steps.download-boost.outputs.filename }} 86 | cd boost_${{ matrix.boost-version-snake }} 87 | ./bootstrap.sh 88 | sudo ./b2 install -j 8 --with-system --with-python --with-filesystem --with-iostreams --with-date_time --with-thread 89 | - name: Restore Google V8 from cache 90 | id: restore-v8 91 | uses: actions/cache/restore@main 92 | with: 93 | path: | 94 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 95 | v8/out.gn/x64.release.sample/icudtl.dat 96 | v8/include 97 | key: linux-build-v8-${{ needs.build-v8.outputs.v8-hash }} 98 | - name: Install dependencies 99 | run: | 100 | pip install --upgrade pip setuptools wheel auditwheel patchelf pytest pytest-order 101 | - name: Build wheel 102 | run: | 103 | python setup.py sdist bdist_wheel --skip-build-v8 -d stpyv8-linux-wheelhouse-${{ matrix.python-version }} 104 | env: 105 | INCLUDE: ${{ env.INCLUDE }};${{ steps.install-python.outputs.python-path 106 | }}include 107 | V8_DEPS_LINUX: 0 108 | LDFLAGS: -L/usr/lib -L/usr/lib/x86_64-linux-gnu 109 | - name: Repair wheel 110 | run: | 111 | auditwheel repair --plat manylinux_2_31_x86_64 -w stpyv8-linux-${{ matrix.python-version }} stpyv8-linux-wheelhouse-${{ matrix.python-version }}/*.whl 112 | - name: Install wheel 113 | run: | 114 | python -m pip install stpyv8-linux-${{ matrix.python-version }}/*.whl 115 | - name: Test wheel 116 | run: | 117 | pytest -v 118 | - name: Upload wheel 119 | uses: actions/upload-artifact@v4 120 | with: 121 | name: stpyv8-linux-python${{ matrix.python-version }} 122 | path: stpyv8-linux-${{ matrix.python-version }}/*.whl 123 | - name: Release 124 | uses: softprops/action-gh-release@v2 125 | if: ${{ github.event_name == 'release' }} 126 | with: 127 | files: stpyv8-linux-${{ matrix.python-version }}/*.whl 128 | token: ${{ secrets.GITHUB_TOKEN }} 129 | pypi-publish: 130 | name: Upload release to PyPI 131 | if: ${{ github.event_name == 'release' }} 132 | needs: build 133 | runs-on: ubuntu-latest 134 | environment: 135 | name: pypi 136 | url: https://pypi.org/p/stpyv8 137 | permissions: 138 | id-token: write 139 | steps: 140 | - name: Download wheels 141 | uses: actions/download-artifact@v4 142 | with: 143 | path: stpyv8-linux-dist 144 | pattern: stpyv8-linux* 145 | merge-multiple: true 146 | - name: Publish wheels to PyPI 147 | uses: pypa/gh-action-pypi-publish@release/v1 148 | with: 149 | packages-dir: stpyv8-linux-dist 150 | -------------------------------------------------------------------------------- /.github/workflows/osx-arm64.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: MacOSX ARM64 build/release workflow 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | branches: [master] 9 | jobs: 10 | build-v8: 11 | # If Google V8 is in the workflow cache, don't build it. 12 | # Cloning the repository is still necessary in any case 13 | # to calculate the hash for the cache key 14 | name: Build Google V8 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [macos-14] 19 | outputs: 20 | v8-hash: ${{ steps.build-v8.outputs.v8-hash }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Clone Google V8 25 | run: | 26 | python3 -m pip install --break-system-packages wheel setuptools 27 | echo "::group::Clone Google V8" 28 | python3 setup.py checkout_v8 29 | echo "::endgroup::" 30 | - name: Restore Google V8 from cache 31 | id: restore-v8 32 | uses: actions/cache/restore@main 33 | with: 34 | path: | 35 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 36 | v8/out.gn/x64.release.sample/icudtl.dat 37 | v8/include 38 | key: ${{ matrix.os }}-arm64-build-v8-${{ hashFiles('v8/src/**') }} 39 | - name: Build Google V8 40 | id: build-v8 41 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 42 | continue-on-error: false 43 | run: | 44 | echo "v8-hash=${{ hashFiles('v8/src/**') }}" >> "$GITHUB_OUTPUT" 45 | python3 -m pip install --break-system-packages wheel 46 | echo "::group::v8" 47 | python3 setup.py v8 48 | echo "::endgroup::" 49 | - name: Save Google V8 to cache 50 | uses: actions/cache/save@main 51 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 52 | with: 53 | # Save compiled binary and header files. This will save an 54 | # additional clone of Google V8 for the linker 55 | path: | 56 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 57 | v8/out.gn/x64.release.sample/icudtl.dat 58 | v8/include 59 | key: ${{ matrix.os }}-arm64-build-v8-${{ hashFiles('v8/src/**') }} 60 | build: 61 | name: Build OSX ARM64 wheel (Python ${{ matrix.python-version }}) 62 | needs: build-v8 63 | runs-on: ${{ matrix.os }} 64 | strategy: 65 | matrix: 66 | os: [macos-14] 67 | python-version: ['3.10', '3.11', '3.12', '3.13'] 68 | boost-version: [1.87.0] 69 | boost-version-snake: ['1_87_0'] 70 | env: 71 | ARCH: arm64 72 | steps: 73 | - name: Checkout repository 74 | uses: actions/checkout@v4 75 | - name: Set up Python 76 | uses: actions/setup-python@v5 77 | with: 78 | python-version: ${{ matrix.python-version }} 79 | - name: Download Boost 80 | id: download-boost 81 | uses: suisei-cn/actions-download-file@v1.6.0 82 | with: 83 | url: https://archives.boost.io/release/${{ matrix.boost-version }}/source/boost_${{matrix.boost-version-snake }}.zip 84 | - name: Install Boost 85 | id: install-boost 86 | run: | 87 | unzip -q ${{ steps.download-boost.outputs.filename }} 88 | cd boost_${{ matrix.boost-version-snake }} 89 | ./bootstrap.sh 90 | sudo ./b2 install -j 8 --with-system --with-python --with-filesystem --with-iostreams --with-date_time --with-thread 91 | - name: Restore Google V8 from cache 92 | id: restore-v8 93 | uses: actions/cache/restore@main 94 | with: 95 | path: | 96 | v8/out.gn/x64.release.sample/obj/libv8_monolith.a 97 | v8/out.gn/x64.release.sample/icudtl.dat 98 | v8/include 99 | key: ${{ matrix.os }}-arm64-build-v8-${{ needs.build-v8.outputs.v8-hash 100 | }} 101 | - name: Install dependencies 102 | run: | 103 | pip install --upgrade --break-system-packages pip setuptools delocate wheel pytest pytest-order 104 | - name: Build wheel 105 | run: | 106 | python3 setup.py sdist bdist_wheel --skip-build-v8 -d stpyv8-${{ matrix.os }}-${{ matrix.python-version }} 107 | env: 108 | ARCHFLAGS: -arch arm64 109 | _PYTHON_HOST_PLATFORM: macosx-11.0-arm64 110 | - name: Repair wheel 111 | run: | 112 | delocate-listdeps stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/stpyv8*.whl 113 | delocate-wheel --require-archs arm64 stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/stpyv8*.whl 114 | - name: Install wheel 115 | run: | 116 | python3 -m pip install --break-system-packages stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/*.whl 117 | - name: Test wheel 118 | run: | 119 | pytest -v 120 | - name: Upload wheel 121 | uses: actions/upload-artifact@v4 122 | with: 123 | name: stpyv8-${{ matrix.os }}-python${{ matrix.python-version }} 124 | path: stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/*.whl 125 | - name: Release 126 | uses: softprops/action-gh-release@v2 127 | if: ${{ github.event_name == 'release' }} 128 | with: 129 | files: stpyv8-${{ matrix.os }}-${{ matrix.python-version }}/*.whl 130 | token: ${{ secrets.GITHUB_TOKEN }} 131 | pypi-publish: 132 | name: Upload release to PyPI 133 | if: ${{ github.event_name == 'release' }} 134 | needs: build 135 | runs-on: ubuntu-latest 136 | environment: 137 | name: pypi 138 | url: https://pypi.org/p/stpyv8 139 | permissions: 140 | id-token: write 141 | steps: 142 | - name: Download wheels 143 | uses: actions/download-artifact@v4 144 | with: 145 | path: stpyv8-macos-dist 146 | pattern: stpyv8-macos* 147 | merge-multiple: true 148 | - name: Publish wheels to PyPI 149 | uses: pypa/gh-action-pypi-publish@release/v1 150 | with: 151 | packages-dir: stpyv8-macos-dist 152 | -------------------------------------------------------------------------------- /tests/test_Engine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | 6 | import STPyV8 7 | 8 | 9 | class TestEngine(unittest.TestCase): 10 | def testClassProperties(self): 11 | with STPyV8.JSContext(): 12 | self.assertTrue(str(STPyV8.JSEngine.version).startswith("13.")) 13 | self.assertFalse(STPyV8.JSEngine.dead) 14 | 15 | def testCompile(self): 16 | with STPyV8.JSContext(): 17 | with STPyV8.JSEngine() as engine: 18 | s = engine.compile("1+2") 19 | 20 | self.assertTrue(isinstance(s, STPyV8.JSScript)) 21 | 22 | self.assertEqual("1+2", s.source) 23 | self.assertEqual(3, int(s.run())) 24 | 25 | self.assertRaises(SyntaxError, engine.compile, "1+") 26 | 27 | def testUnicodeSource(self): 28 | class Global(STPyV8.JSClass): 29 | var = "测试" 30 | 31 | def __getattr__(self, name): 32 | if name: 33 | return self.var 34 | 35 | return STPyV8.JSClass.__getattr__(self, name) 36 | 37 | g = Global() 38 | 39 | with STPyV8.JSContext(g) as ctxt: 40 | with STPyV8.JSEngine() as engine: 41 | src = """ 42 | function 函数() { return 变量.length; } 43 | 44 | 函数(); 45 | 46 | var func = function () {}; 47 | """ 48 | 49 | s = engine.compile(src) 50 | 51 | self.assertTrue(isinstance(s, STPyV8.JSScript)) 52 | 53 | self.assertEqual(src, s.source) 54 | self.assertEqual(2, s.run()) 55 | 56 | func_name = "函数" 57 | 58 | self.assertTrue(hasattr(ctxt.locals, func_name)) 59 | 60 | func = getattr(ctxt.locals, func_name) 61 | 62 | self.assertTrue(isinstance(func, STPyV8.JSFunction)) 63 | 64 | self.assertEqual(func_name, func.name) 65 | self.assertEqual("", func.resname) 66 | self.assertEqual(1, func.linenum) 67 | 68 | var_name = "变量" 69 | 70 | setattr(ctxt.locals, var_name, "测试长字符串") 71 | 72 | self.assertEqual(6, func()) 73 | 74 | self.assertEqual("func", ctxt.locals.func.inferredname) 75 | 76 | def testEval(self): 77 | with STPyV8.JSContext() as ctxt: 78 | self.assertEqual(3, int(ctxt.eval("1+2"))) 79 | 80 | def testGlobal(self): 81 | class Global(STPyV8.JSClass): 82 | version = "1.0" 83 | 84 | with STPyV8.JSContext(Global()) as ctxt: 85 | _vars = ctxt.locals 86 | 87 | # getter 88 | self.assertEqual(Global.version, str(_vars.version)) 89 | self.assertEqual(Global.version, str(ctxt.eval("version"))) 90 | 91 | self.assertRaises(ReferenceError, ctxt.eval, "nonexists") 92 | 93 | # setter 94 | self.assertEqual(2.0, float(ctxt.eval("version = 2.0"))) 95 | 96 | self.assertEqual(2.0, float(_vars.version)) 97 | 98 | def testThis(self): 99 | class Global(STPyV8.JSClass): 100 | version = 1.0 101 | 102 | with STPyV8.JSContext(Global()) as ctxt: 103 | self.assertEqual("[object Global]", str(ctxt.eval("this"))) 104 | self.assertEqual(1.0, float(ctxt.eval("this.version"))) 105 | 106 | def testObjectBuiltInMethods(self): 107 | class Global(STPyV8.JSClass): 108 | version = 1.0 109 | 110 | with STPyV8.JSContext(Global()) as ctxt: 111 | self.assertEqual("[object Global]", str(ctxt.eval("this.toString()"))) 112 | self.assertEqual("[object Global]", str(ctxt.eval("this.toLocaleString()"))) 113 | self.assertEqual(Global.version, float(ctxt.eval("this.valueOf()").version)) 114 | self.assertTrue(bool(ctxt.eval('this.hasOwnProperty("version")'))) 115 | self.assertFalse(ctxt.eval('this.hasOwnProperty("nonexistent")')) 116 | 117 | def testPythonWrapper(self): 118 | class Global(STPyV8.JSClass): 119 | s = [1, 2, 3] 120 | d = {"a": {"b": "c"}, "d": ["e", "f"]} 121 | 122 | g = Global() 123 | 124 | with STPyV8.JSContext(g) as ctxt: 125 | ctxt.eval( 126 | """ 127 | s[2] = s[1] + 2; 128 | s[0] = s[1]; 129 | delete s[1]; 130 | """ 131 | ) 132 | self.assertEqual([2, 4], g.s) 133 | self.assertEqual("c", ctxt.eval("d.a.b")) 134 | self.assertEqual(["e", "f"], ctxt.eval("d.d")) 135 | ctxt.eval( 136 | """ 137 | d.a.q = 4 138 | delete d.d 139 | """ 140 | ) 141 | self.assertEqual(4, g.d["a"]["q"]) 142 | self.assertEqual(None, ctxt.eval("d.d")) 143 | 144 | def _testMemoryAllocationCallback(self): 145 | alloc = {} 146 | 147 | def callback(space, action, size): 148 | alloc[(space, action)] = alloc.setdefault((space, action), 0) + size 149 | 150 | STPyV8.JSEngine.setMemoryAllocationCallback(callback) 151 | 152 | with STPyV8.JSContext() as ctxt: 153 | self.assertFalse( 154 | (STPyV8.JSObjectSpace.Code, STPyV8.JSAllocationAction.alloc) in alloc 155 | ) 156 | 157 | ctxt.eval("var o = new Array(1000);") 158 | 159 | self.assertTrue( 160 | (STPyV8.JSObjectSpace.Code, STPyV8.JSAllocationAction.alloc) in alloc 161 | ) 162 | 163 | STPyV8.JSEngine.setMemoryAllocationCallback(None) 164 | 165 | def _testOutOfMemory(self): 166 | with STPyV8.JSIsolate(): 167 | STPyV8.JSEngine.setMemoryLimit( 168 | max_young_space_size=16 * 1024, max_old_space_size=4 * 1024 * 1024 169 | ) 170 | 171 | with STPyV8.JSContext() as ctxt: 172 | STPyV8.JSEngine.ignoreOutOfMemoryException() 173 | 174 | ctxt.eval("var a = new Array(); while(true) a.push(a);") 175 | 176 | self.assertTrue(ctxt.hasOutOfMemoryException) 177 | 178 | STPyV8.JSEngine.setMemoryLimit() 179 | 180 | STPyV8.JSEngine.collect() 181 | 182 | def testStackLimit(self): 183 | with STPyV8.JSIsolate(): 184 | with STPyV8.JSContext() as ctxt: 185 | STPyV8.JSEngine.setStackLimit(256 * 1024) 186 | oldStackSize = ctxt.eval( 187 | "var maxStackSize = function(i){try{(function m(){++i&&m()}())}catch(e){return i}}(0); maxStackSize" 188 | ) 189 | 190 | with STPyV8.JSIsolate(): 191 | with STPyV8.JSContext() as ctxt: 192 | STPyV8.JSEngine.setStackLimit(512 * 1024) 193 | newStackSize = ctxt.eval( 194 | "var maxStackSize = function(i){try{(function m(){++i&&m()}())}catch(e){return i}}(0); maxStackSize" 195 | ) 196 | 197 | self.assertTrue(newStackSize > oldStackSize * 2) 198 | -------------------------------------------------------------------------------- /docs/source/api/context.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: STPyV8 2 | :noindex: 3 | 4 | .. testsetup:: * 5 | 6 | from STPyV8 import * 7 | 8 | .. _context: 9 | 10 | Javascript Context 11 | ================== 12 | 13 | .. sidebar:: Execution context 14 | 15 | When control is transferred to ECMAScript executable code [#f2]_, control is entering an execution context. 16 | 17 | -- ECMA-262 3rd Chapter 10 18 | 19 | According to the ECMAScript standard [#f1]_, an execution context has to be entered before executing any script code. 20 | 21 | :py:class:`JSContext` is a sandboxed execution context with its own set of built-in objects and functions. 22 | 23 | You could create a :py:class:`JSContext` instance, enter it with the :py:meth:`JSContext.enter` method, and use it to 24 | execute Javascript code with the :py:meth:`JSContext.eval` method. The best practice is to leave the context with the 25 | :py:meth:`JSContext.leave` when you do not need it anymore. 26 | 27 | .. doctest:: 28 | 29 | >>> ctxt = JSContext() # create a context with an implicit global object 30 | >>> ctxt.enter() # enter the context (also support with statement) 31 | >>> ctxt.eval("1+2") # evalute the javascript expression and return a native python int 32 | 3 33 | >>> ctxt.leave() # leave the context and release the related resources 34 | 35 | .. note:: 36 | 37 | To ensure the context is entered/left correctly, use the **with** statement 38 | 39 | .. testcode:: 40 | 41 | with JSContext() as ctxt: 42 | print(ctxt.eval("1+2")) # 3 43 | 44 | .. testoutput:: 45 | :hide: 46 | 47 | 3 48 | 49 | You could also check the current context using the :py:class:`JSContext` static properties. 50 | 51 | ============================== ============================================= 52 | Property Description 53 | ============================== ============================================= 54 | :py:attr:`JSContext.current` The context that is on the top of the stack. 55 | :py:attr:`JSContext.entered` The last entered context. 56 | :py:attr:`JSContext.calling` The context of the calling JavaScript code. 57 | :py:attr:`JSContext.inContext` Returns true if V8 has a current context. 58 | ============================== ============================================= 59 | 60 | .. _gobj: 61 | 62 | Global Object 63 | ------------- 64 | 65 | .. sidebar:: Global object 66 | 67 | There is a unique *global object* (15.1), which is created before control enters any execution context. Initially the global object has the following properties: 68 | 69 | * Built-in objects such as Math, String, Date, parseInt, etc. 70 | * Additional host defined properties. 71 | 72 | As control enters execution contexts, and as ECMAScript code is executed, additional properties may be added to the global object and the initial properties may be changed. 73 | 74 | -- ECMA-262 3rd Chapter 10.1.5 75 | 76 | The execution context has a global object which could be accessed both from the Python side with the :py:attr:`JSContext.locals` 77 | attribute and from the Javascript side using the global namespace. The Python and Javascript code could use such object to 78 | perform seamless interoperable logic while STPyV8 takes care of :ref:`typeconv`, :ref:`funcall` and :ref:`exctrans`. 79 | 80 | .. testcode:: 81 | 82 | with JSContext() as ctxt: 83 | ctxt.eval("a = 1") 84 | print(ctxt.locals.a) # 1 85 | 86 | ctxt.locals.a = 2 87 | print(ctxt.eval("a")) # 2 88 | 89 | .. testoutput:: 90 | :hide: 91 | 92 | 1 93 | 2 94 | 95 | Providing more complicated properties and methods to the Javascript code can be easily done by passing a customized global object 96 | instance when the :py:class:`JSContext` instance is created. 97 | 98 | .. testcode:: 99 | 100 | class Global(JSClass): 101 | version = "1.0" 102 | 103 | def hello(self, name): 104 | return "Hello " + name 105 | 106 | with JSContext(Global()) as ctxt: 107 | print(ctxt.eval("version"))) # 1.0 108 | print(ctxt.eval("hello('World')")) # Hello World 109 | print(ctxt.eval("hello.toString()")) # function () { [native code] } 110 | 111 | .. testoutput:: 112 | :hide: 113 | 114 | 1.0 115 | Hello World 116 | function () { [native code] } 117 | 118 | .. note:: 119 | 120 | If you want your global object to behave like a real Javascript object, you should inherit from the :py:class:`JSClass` class 121 | which provides a lot of helper methods such as :py:meth:`JSClass.toString`, :py:meth:`JSClass.watch` etc. 122 | 123 | .. _jsext: 124 | 125 | JSContext - the execution context. 126 | ---------------------------------- 127 | 128 | .. autoclass:: JSContext 129 | :members: 130 | :inherited-members: 131 | :exclude-members: eval 132 | 133 | JSContext is an execution context. 134 | 135 | .. automethod:: __init__(global = None) -> JSContext object 136 | 137 | :param object global: the global object 138 | :rtype: :py:class:`JSContext` instance 139 | 140 | .. automethod:: __init__(ctxt) -> JSContext object 141 | 142 | :param JSContext ctxt: an existing :py:class:`JSContext` instance 143 | :rtype: a cloned :py:class:`JSContext` instance 144 | 145 | .. automethod:: eval(source, name = '', line = -1, col = -1) -> object: 146 | 147 | Execute the Javascript code and return the result 148 | 149 | :param source: the Javascript code to be executed 150 | :type source: str or unicode 151 | :param str name: the name of the Javascript code 152 | :param integer line: the start line number of the Javascript code 153 | :param integer col: the start column number of the Javascript code 154 | :rtype: the result 155 | 156 | .. automethod:: __enter__() -> JSContext object 157 | 158 | .. automethod:: __exit__(exc_type, exc_value, traceback) -> None 159 | 160 | .. py:attribute:: current 161 | 162 | The context that is on the top of the stack. 163 | 164 | .. py:attribute:: entered 165 | 166 | The last entered context. 167 | 168 | .. py:attribute:: calling 169 | 170 | The context of the calling JavaScript code. 171 | 172 | .. py:attribute:: inContext 173 | 174 | Returns true if V8 has a current context. 175 | 176 | .. toctree:: 177 | :maxdepth: 2 178 | 179 | .. rubric:: Footnotes 180 | 181 | .. [#f1] `ECMAScript `_ is the scripting language standardized by Ecma International in the ECMA-262 specification and ISO/IEC 16262. The language is widely used for client-side scripting on the web, in the form of several well-known dialects such as JavaScript, JScript, and ActionScript. 182 | 183 | .. [#f2] There are three types of ECMAScript executable code: 184 | 185 | * *Global code* is source text that is treated as an ECMAScript *Program*. 186 | * *Eval code* is the source text supplied to the built-in **eval** function. 187 | * *Function code* is source text that is parsed as part of a *FunctionBody*. 188 | -------------------------------------------------------------------------------- /src/Exception.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Config.h" 8 | #include "Utils.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #define BEGIN_HANDLE_PYTHON_EXCEPTION try 15 | #define END_HANDLE_PYTHON_EXCEPTION \ 16 | catch (const std::exception& ex) { v8::Isolate::GetCurrent()->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), ex.what()).ToLocalChecked())); } \ 17 | catch (const py::error_already_set&) { CPythonObject::ThrowIf(v8::Isolate::GetCurrent()); } \ 18 | catch (...) { v8::Isolate::GetCurrent()->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), "unknown exception").ToLocalChecked())); } 19 | 20 | class CJavascriptException; 21 | 22 | struct ExceptionTranslator 23 | { 24 | static void Translate(CJavascriptException const& ex); 25 | 26 | static void *Convertible(PyObject* obj); 27 | static void Construct(PyObject* obj, py::converter::rvalue_from_python_stage1_data* data); 28 | }; 29 | 30 | class CJavascriptStackTrace; 31 | class CJavascriptStackFrame; 32 | 33 | typedef std::shared_ptr CJavascriptStackTracePtr; 34 | typedef std::shared_ptr CJavascriptStackFramePtr; 35 | 36 | class CJavascriptStackTrace 37 | { 38 | v8::Isolate *m_isolate; 39 | v8::Persistent m_st; 40 | public: 41 | CJavascriptStackTrace(v8::Isolate *isolate, v8::Handle st) 42 | : m_isolate(isolate), m_st(isolate, st) 43 | { 44 | 45 | } 46 | 47 | CJavascriptStackTrace(const CJavascriptStackTrace& st) 48 | : m_isolate(st.m_isolate) 49 | { 50 | v8::HandleScope handle_scope(m_isolate); 51 | 52 | m_st.Reset(m_isolate, st.Handle()); 53 | } 54 | 55 | v8::Handle Handle() const { 56 | return v8::Local::New(m_isolate, m_st); 57 | } 58 | 59 | int GetFrameCount() const { 60 | v8::HandleScope handle_scope(m_isolate); 61 | return Handle()->GetFrameCount(); 62 | } 63 | 64 | CJavascriptStackFramePtr GetFrame(size_t idx) const; 65 | 66 | static CJavascriptStackTracePtr GetCurrentStackTrace(v8::Isolate *isolate, int frame_limit, 67 | v8::StackTrace::StackTraceOptions options = v8::StackTrace::kOverview); 68 | 69 | void Dump(std::ostream& os) const; 70 | 71 | class FrameIterator 72 | : public boost::iterator_facade 73 | { 74 | CJavascriptStackTrace *m_st; 75 | size_t m_idx; 76 | public: 77 | FrameIterator(CJavascriptStackTrace *st, size_t idx) 78 | : m_st(st), m_idx(idx) 79 | { 80 | } 81 | 82 | void increment() { 83 | m_idx++; 84 | } 85 | 86 | bool equal(FrameIterator const& other) const { 87 | return m_st == other.m_st && m_idx == other.m_idx; 88 | } 89 | 90 | reference dereference() const { 91 | return m_st->GetFrame(m_idx); 92 | } 93 | }; 94 | 95 | FrameIterator begin(void) { 96 | return FrameIterator(this, 0); 97 | } 98 | 99 | FrameIterator end(void) { 100 | return FrameIterator(this, GetFrameCount()); 101 | } 102 | }; 103 | 104 | class CJavascriptStackFrame 105 | { 106 | v8::Isolate *m_isolate; 107 | v8::Persistent m_frame; 108 | public: 109 | CJavascriptStackFrame(v8::Isolate *isolate, v8::Handle frame) 110 | : m_isolate(isolate), m_frame(isolate, frame) 111 | { 112 | 113 | } 114 | 115 | CJavascriptStackFrame(const CJavascriptStackFrame& frame) 116 | : m_isolate(frame.m_isolate) 117 | { 118 | v8::HandleScope handle_scope(m_isolate); 119 | 120 | m_frame.Reset(m_isolate, frame.Handle()); 121 | } 122 | 123 | v8::Handle Handle() const { 124 | return v8::Local::New(m_isolate, m_frame); 125 | } 126 | 127 | int GetLineNumber() const { 128 | v8::HandleScope handle_scope(m_isolate); 129 | return Handle()->GetLineNumber(); 130 | } 131 | 132 | int GetColumn() const { 133 | v8::HandleScope handle_scope(m_isolate); 134 | return Handle()->GetColumn(); 135 | } 136 | 137 | const std::string GetScriptName() const; 138 | const std::string GetFunctionName() const; 139 | 140 | bool IsEval() const { 141 | v8::HandleScope handle_scope(m_isolate); 142 | return Handle()->IsEval(); 143 | } 144 | 145 | bool IsConstructor() const { 146 | v8::HandleScope handle_scope(m_isolate); 147 | return Handle()->IsConstructor(); 148 | } 149 | }; 150 | 151 | class CJavascriptException : public std::runtime_error 152 | { 153 | v8::Isolate *m_isolate; 154 | PyObject *m_type; 155 | 156 | v8::Persistent m_exc, m_stack; 157 | v8::Persistent m_msg; 158 | 159 | friend struct ExceptionTranslator; 160 | 161 | static const std::string Extract(v8::Isolate *isolate, v8::TryCatch& try_catch); 162 | protected: 163 | CJavascriptException(v8::Isolate *isolate, v8::TryCatch& try_catch, PyObject *type) 164 | : std::runtime_error(Extract(isolate, try_catch)), m_isolate(isolate), m_type(type) 165 | { 166 | v8::HandleScope handle_scope(m_isolate); 167 | 168 | m_exc.Reset(m_isolate, try_catch.Exception()); 169 | 170 | v8::MaybeLocal stack_trace = try_catch.StackTrace(v8::Isolate::GetCurrent()->GetCurrentContext()); 171 | if (!stack_trace.IsEmpty()) { 172 | m_stack.Reset(m_isolate, stack_trace.ToLocalChecked()); 173 | } 174 | 175 | m_msg.Reset(m_isolate, try_catch.Message()); 176 | } 177 | public: 178 | CJavascriptException(const std::string& msg, PyObject *type = NULL) 179 | : std::runtime_error(msg), m_isolate(v8::Isolate::GetCurrent()), m_type(type) 180 | { 181 | } 182 | 183 | CJavascriptException(const CJavascriptException& ex) 184 | : std::runtime_error(ex.what()), m_isolate(ex.m_isolate), m_type(ex.m_type) 185 | { 186 | v8::HandleScope handle_scope(m_isolate); 187 | 188 | m_exc.Reset(m_isolate, ex.Exception()); 189 | m_stack.Reset(m_isolate, ex.Stack()); 190 | m_msg.Reset(m_isolate, ex.Message()); 191 | } 192 | 193 | ~CJavascriptException() throw() 194 | { 195 | if (!m_exc.IsEmpty()) m_exc.Reset(); 196 | if (!m_msg.IsEmpty()) m_msg.Reset(); 197 | } 198 | 199 | v8::Handle Exception() const { 200 | return v8::Local::New(m_isolate, m_exc); 201 | } 202 | 203 | v8::Handle Stack() const { 204 | return v8::Local::New(m_isolate, m_stack); 205 | } 206 | 207 | v8::Handle Message() const { 208 | return v8::Local::New(m_isolate, m_msg); 209 | } 210 | 211 | const std::string GetName(void); 212 | const std::string GetMessage(void); 213 | const std::string GetScriptName(void); 214 | int GetLineNumber(void); 215 | int GetStartPosition(void); 216 | int GetEndPosition(void); 217 | int GetStartColumn(void); 218 | int GetEndColumn(void); 219 | const std::string GetSourceLine(void); 220 | const std::string GetStackTrace(void); 221 | 222 | void PrintCallStack(py::object file); 223 | 224 | static void ThrowIf(v8::Isolate *isolate, v8::TryCatch& try_catch); 225 | 226 | static void Expose(void); 227 | }; 228 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # STPyV8 documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Nov 27 12:08:50 2019. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', ] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'STPyV8' 44 | copyright = u'2019, Area 1 Security' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'sphinx_rtd_theme' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | html_sidebars = { 135 | '**': [ 136 | #'navigation.html', 137 | 'relations.html', 138 | 'searchbox.html', 139 | ] 140 | } 141 | 142 | # Additional templates that should be rendered to pages, maps page names to 143 | # template names. 144 | #html_additional_pages = {} 145 | 146 | # If false, no module index is generated. 147 | #html_domain_indices = True 148 | 149 | # If false, no index is generated. 150 | #html_use_index = True 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | #html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | #html_show_sourcelink = True 157 | 158 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 159 | #html_show_sphinx = True 160 | 161 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 162 | #html_show_copyright = True 163 | 164 | # If true, an OpenSearch description file will be output, and all pages will 165 | # contain a tag referring to it. The value of this option must be the 166 | # base URL from which the finished HTML is served. 167 | #html_use_opensearch = '' 168 | 169 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 170 | #html_file_suffix = None 171 | 172 | # Output file base name for HTML help builder. 173 | htmlhelp_basename = 'STPyV8doc' 174 | 175 | 176 | # -- Options for LaTeX output -------------------------------------------------- 177 | 178 | # The paper size ('letter' or 'a4'). 179 | #latex_paper_size = 'letter' 180 | 181 | # The font size ('10pt', '11pt' or '12pt'). 182 | #latex_font_size = '10pt' 183 | 184 | # Grouping the document tree into LaTeX files. List of tuples 185 | # (source start file, target name, title, author, documentclass [howto/manual]). 186 | latex_documents = [ 187 | ('index', 'STPyV8.tex', u'STPyV8 Documentation', 188 | u'Area 1 Security', 'manual'), 189 | ] 190 | 191 | # The name of an image file (relative to this directory) to place at the top of 192 | # the title page. 193 | #latex_logo = None 194 | 195 | # For "manual" documents, if this is true, then toplevel headings are parts, 196 | # not chapters. 197 | #latex_use_parts = False 198 | 199 | # If true, show page references after internal links. 200 | #latex_show_pagerefs = False 201 | 202 | # If true, show URL addresses after external links. 203 | #latex_show_urls = False 204 | 205 | # Additional stuff for the LaTeX preamble. 206 | #latex_preamble = '' 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'stpyv8', u'STPyV8 Documentation', 221 | [u'Area 1 Security'], 1) 222 | ] 223 | 224 | intersphinx_mapping = {'python': ('http://docs.python.org/3.7', None)} 225 | 226 | autodoc_member_order = 'groupwise' 227 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Windows build/release workflow 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | branches: [master] 9 | env: 10 | BOOST_ROOT: boost 11 | MSVC_TOOLSET_VERSION: 14.2 12 | jobs: 13 | build-v8: 14 | # If Google V8 is in the workflow cache, don't build it. 15 | # Cloning the repository is still necessary in any case 16 | # to calculate the hash for the cache key 17 | name: Build Google V8 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | # Needed for MSVC toolset 14.2 22 | os: [windows-latest] 23 | outputs: 24 | v8-hash: ${{ steps.build-v8.outputs.v8-hash }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Clone Google V8 29 | run: | 30 | python -m pip install wheel 31 | echo "::group::Clone v8" 32 | python setup.py checkout_v8 33 | echo "::endgroup::" 34 | - name: Restore Google V8 from cache 35 | id: restore-v8 36 | uses: actions/cache/restore@main 37 | with: 38 | path: | 39 | v8\out.gn\x64.release.sample\obj\v8_monolith.lib 40 | v8\out.gn\x64.release.sample\icudtl.dat 41 | v8\include 42 | key: windows-build-v8-${{ hashFiles('v8/src/**') }} 43 | - name: Initialize MSVC environment 44 | uses: ilammy/msvc-dev-cmd@v1 45 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 46 | with: 47 | toolset: 14.2 48 | - name: Build Google V8 49 | id: build-v8 50 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 51 | continue-on-error: false 52 | run: | 53 | echo "v8-hash=${{ hashFiles('v8/src/**') }}" >> "$GITHUB_OUTPUT" 54 | python -m pip install wheel 55 | echo "::group::v8" 56 | python setup.py v8 57 | echo "::endgroup::" 58 | - name: Save Google V8 to cache 59 | uses: actions/cache/save@main 60 | if: ${{ steps.restore-v8.outputs.cache-hit != 'true' }} 61 | with: 62 | # Save compiled binary and header files. This will save an 63 | # additional clone of Google V8 for the linker 64 | path: | 65 | v8\out.gn\x64.release.sample\obj\v8_monolith.lib 66 | v8\out.gn\x64.release.sample\icudtl.dat 67 | v8\include 68 | key: windows-build-v8-${{ hashFiles('v8/src/**') }} 69 | build: 70 | name: Build Windows wheel (Python ${{ matrix.python-version }}) 71 | needs: build-v8 72 | runs-on: ${{ matrix.os }} 73 | env: 74 | DIST_NAME: stpyv8-windows-py${{ matrix.python-version }} 75 | strategy: 76 | matrix: 77 | # Needed for MSVC toolset 14.2 78 | os: [windows-2019] 79 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 80 | boost-version: [1.84.0] 81 | boost-version-snake: ['1_84_0'] 82 | steps: 83 | - name: Checkout repository 84 | uses: actions/checkout@v4 85 | - name: Set up Python 86 | id: install-python 87 | uses: actions/setup-python@v5 88 | with: 89 | python-version: ${{ matrix.python-version }} 90 | - name: Restore Boost from cache 91 | id: restore-boost 92 | uses: actions/cache/restore@main 93 | with: 94 | path: ${{ env.BOOST_ROOT }}\stage\lib 95 | key: boost-${{ matrix.boost-version }}-python${{ matrix.python-version }}-vc142 96 | - name: Initialize MSVC environment 97 | uses: ilammy/msvc-dev-cmd@v1 98 | if: ${{ steps.restore-boost.outputs.cache-hit != 'true' }} 99 | with: 100 | toolset: ${{ env.MSVC_TOOLSET_VERSION }} 101 | - name: Download Boost 102 | if: ${{ steps.restore-boost.outputs.cache-hit != 'true' }} 103 | id: download-boost 104 | uses: suisei-cn/actions-download-file@v1.6.0 105 | with: 106 | url: https://archives.boost.io/release/${{ matrix.boost-version }}/source/boost_${{matrix.boost-version-snake }}.zip 107 | - name: Install Boost 108 | if: ${{ steps.restore-boost.outputs.cache-hit != 'true' }} 109 | id: install-boost 110 | run: | 111 | mkdir $env:BOOST_ROOT 112 | Expand-Archive ${{ steps.download-boost.outputs.filename }} -DestinationPath $env:BOOST_ROOT 113 | cd $env:BOOST_ROOT\* 114 | echo "BOOST_ROOT=$pwd" >> $env:GITHUB_OUTPUT 115 | echo "BOOST_ROOT=$pwd" >> $env:GITHUB_ENV 116 | echo "BOOST_LIBRARYDIR=$pwd\stage\lib" >> $env:GITHUB_ENV 117 | - name: Build Boost 118 | if: ${{ steps.restore-boost.outputs.cache-hit != 'true' }} 119 | working-directory: ${{ steps.install-boost.outputs.BOOST_ROOT }} 120 | run: | 121 | .\bootstrap.bat 122 | if (-not $?) { type bootstrap.log } 123 | 124 | # Set specific Python version 125 | $escapedPythonPath = "${{ steps.install-python.outputs.python-path }}" -Replace "\\","\\" 126 | echo "using python : : ""$escapedPythonPath"" ;" >> project-config.jam 127 | 128 | # Patch bug affecting compilation on Python 3.10 129 | # https://github.com/boostorg/python/commit/cbd2d9f033c61d29d0a1df14951f4ec91e7d05cd 130 | (Get-Content libs\python\src\exec.cpp).replace('_Py_fopen', 'fopen') | Set-Content libs\python\src\exec.cpp 131 | .\b2.exe stage -j 8 link=static runtime-link=static --with-python --with-filesystem --with-iostreams --with-date_time --with-thread 132 | ls stage\lib 133 | - name: Save Boost to cache 134 | uses: actions/cache/save@main 135 | if: ${{ steps.restore-boost.outputs.cache-hit != 'true' }} 136 | with: 137 | path: ${{ steps.install-boost.outputs.BOOST_ROOT }}\stage\lib 138 | key: boost-${{ matrix.boost-version }}-python${{ matrix.python-version }}-vc142 139 | - name: Restore Google V8 from cache 140 | id: restore-v8 141 | uses: actions/cache/restore@main 142 | with: 143 | path: | 144 | v8\out.gn\x64.release.sample\obj\v8_monolith.lib 145 | v8\out.gn\x64.release.sample\icudtl.dat 146 | v8\include 147 | key: windows-build-v8-${{ needs.build-v8.outputs.v8-hash }} 148 | - name: Build wheel 149 | env: 150 | # Set include and library files which will be picked up by setuptools 151 | INCLUDE: ${{ env.INCLUDE }};${{ steps.install-python.outputs.python-path 152 | }}include 153 | run: | 154 | python -m pip install setuptools wheel delvewheel importlib_resources pytest pytest-order 155 | 156 | # Google V8 build should already be supplied from cache hence no need to rebuild 157 | python setup.py sdist bdist_wheel --skip-build-v8 -d wheelhouse-${{ env.DIST_NAME }} 158 | if (-not $?) { 159 | echo "::error::Wheel build failed" 160 | exit 1 161 | } 162 | - name: Repair wheel 163 | run: | 164 | Get-ChildItem -Path wheelhouse-${{ env.DIST_NAME }} -Filter *.whl | Foreach-Object { 165 | delvewheel repair -vv -w ${{ env.DIST_NAME }} $_.FullName 166 | } 167 | - name: Install wheel 168 | run: | 169 | python -m pip install --find-links=${{ env.DIST_NAME }} stpyv8 170 | if (-not $?) { 171 | echo "::error::Wheel installation failed" 172 | exit 1 173 | } 174 | - name: Test wheel 175 | run: | 176 | pytest -v 177 | - name: Upload wheel 178 | uses: actions/upload-artifact@v4 179 | with: 180 | name: stpyv8-${{ matrix.os }}-python${{ matrix.python-version }} 181 | path: ${{ env.DIST_NAME }}/*.whl 182 | - name: Release 183 | uses: softprops/action-gh-release@v2 184 | if: ${{ github.event_name == 'release' }} 185 | with: 186 | files: ${{ env.DIST_NAME }}/*.whl 187 | token: ${{ secrets.GITHUB_TOKEN }} 188 | pypi-publish: 189 | name: Upload release to PyPI 190 | if: ${{ github.event_name == 'release' }} 191 | needs: build 192 | runs-on: ubuntu-latest 193 | environment: 194 | name: pypi 195 | url: https://pypi.org/p/stpyv8 196 | permissions: 197 | id-token: write 198 | steps: 199 | - name: Download wheels 200 | uses: actions/download-artifact@v4 201 | with: 202 | path: stpyv8-windows-dist 203 | pattern: stpyv8-windows* 204 | merge-multiple: true 205 | - name: Publish wheels to PyPI 206 | uses: pypa/gh-action-pypi-publish@release/v1 207 | with: 208 | packages-dir: stpyv8-windows-dist 209 | -------------------------------------------------------------------------------- /src/Context.cpp: -------------------------------------------------------------------------------- 1 | #include "Context.h" 2 | #include "Wrapper.h" 3 | #include "Engine.h" 4 | 5 | #include "libplatform/libplatform.h" 6 | 7 | void CContext::Expose(void) 8 | { 9 | py::class_("JSPlatform", "JSPlatform allows the V8 platform to be initialized", py::no_init) 10 | .def(py::init((py::arg("argv") = std::string()))) 11 | .def("init", &CPlatform::Init, "Initializes the platform") 12 | ; 13 | 14 | py::class_("JSIsolate", "JSIsolate is an isolated instance of the V8 engine.", py::no_init) 15 | .def(py::init((py::arg("owner") = false))) 16 | 17 | .add_static_property("current", &CIsolate::GetCurrent, 18 | "Returns the entered isolate for the current thread or NULL in case there is no current isolate.") 19 | 20 | .add_property("locked", &CIsolate::IsLocked) 21 | 22 | .def("GetCurrentStackTrace", &CIsolate::GetCurrentStackTrace) 23 | 24 | .def("enter", &CIsolate::Enter, 25 | "Sets this isolate as the entered one for the current thread. " 26 | "Saves the previously entered one (if any), so that it can be " 27 | "restored when exiting. Re-entering an isolate is allowed.") 28 | 29 | .def("leave", &CIsolate::Leave, 30 | "Exits this isolate by restoring the previously entered one in the current thread. " 31 | "The isolate may still stay the same, if it was entered more than once.") 32 | ; 33 | 34 | py::class_("JSContext", "JSContext is an execution context.", py::no_init) 35 | .def(py::init("Create a new context based on a existing context")) 36 | .def(py::init((py::arg("global") = py::object()), 37 | "Create a new context based on global object")) 38 | 39 | .add_property("securityToken", &CContext::GetSecurityToken, &CContext::SetSecurityToken) 40 | 41 | .add_property("locals", &CContext::GetGlobal, "Local variables within context") 42 | 43 | .add_static_property("entered", &CContext::GetEntered, 44 | "The last entered context.") 45 | .add_static_property("current", &CContext::GetCurrent, 46 | "The context that is on the top of the stack.") 47 | .add_static_property("calling", &CContext::GetCalling, 48 | "The context of the calling JavaScript code.") 49 | .add_static_property("inContext", &CContext::InContext, 50 | "Returns true if V8 has a current context.") 51 | 52 | .def("eval", &CContext::Evaluate, (py::arg("source"), 53 | py::arg("name") = std::string(), 54 | py::arg("line") = -1, 55 | py::arg("col") = -1)) 56 | .def("eval", &CContext::EvaluateW, (py::arg("source"), 57 | py::arg("name") = std::wstring(), 58 | py::arg("line") = -1, 59 | py::arg("col") = -1)) 60 | 61 | .def("enter", &CContext::Enter, "Enter this context. " 62 | "After entering a context, all code compiled and " 63 | "run is compiled and run in this context.") 64 | .def("leave", &CContext::Leave, "Exit this context. " 65 | "Exiting the current context restores the context " 66 | "that was in place when entering the current context.") 67 | 68 | .def("__bool__", &CContext::IsEntered, "the context has been entered.") 69 | ; 70 | 71 | py::objects::class_value_wrapper, 72 | py::objects::make_ptr_instance,CPlatform> > >(); 74 | 75 | py::objects::class_value_wrapper, 76 | py::objects::make_ptr_instance,CIsolate> > >(); 78 | 79 | py::objects::class_value_wrapper, 80 | py::objects::make_ptr_instance,CContext> > >(); 82 | } 83 | 84 | CContext::CContext(v8::Handle context) 85 | { 86 | v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); 87 | 88 | m_context.Reset(context->GetIsolate(), context); 89 | } 90 | 91 | CContext::CContext(const CContext& context) 92 | { 93 | v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); 94 | 95 | m_context.Reset(context.Handle()->GetIsolate(), context.Handle()); 96 | } 97 | 98 | CContext::CContext(py::object global) 99 | : m_global(global) 100 | { 101 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 102 | v8::HandleScope handle_scope(isolate); 103 | 104 | v8::Handle context = v8::Context::New(isolate); 105 | 106 | m_context.Reset(isolate, context); 107 | 108 | v8::Context::Scope context_scope(Handle()); 109 | 110 | if (!global.is_none()) 111 | { 112 | v8::Maybe retcode = 113 | Handle()->Global()->Set(context, 114 | v8::String::NewFromUtf8(isolate, "__proto__").ToLocalChecked(), 115 | CPythonObject::Wrap(global)); 116 | if(retcode.IsNothing()) { 117 | //TODO we need to do something if the set call failed 118 | } 119 | 120 | Py_DECREF(global.ptr()); 121 | } 122 | } 123 | 124 | py::object CContext::GetGlobal(void) 125 | { 126 | v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); 127 | 128 | return CJavascriptObject::Wrap(Handle()->Global()); 129 | } 130 | 131 | py::str CContext::GetSecurityToken(void) 132 | { 133 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 134 | v8::HandleScope handle_scope(isolate); 135 | 136 | v8::Handle token = Handle()->GetSecurityToken(); 137 | 138 | if (token.IsEmpty()) return py::str(); 139 | 140 | v8::String::Utf8Value str(isolate, token->ToString(m_context.Get(isolate)).ToLocalChecked()); 141 | 142 | return py::str(*str, str.length()); 143 | } 144 | 145 | void CContext::SetSecurityToken(py::str token) 146 | { 147 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 148 | v8::HandleScope handle_scope(isolate); 149 | 150 | if (token.is_none()) 151 | { 152 | Handle()->UseDefaultSecurityToken(); 153 | } 154 | else 155 | { 156 | Handle()->SetSecurityToken(v8::String::NewFromUtf8(isolate, 157 | py::extract(token)()).ToLocalChecked()); 158 | } 159 | } 160 | 161 | py::object CContext::GetEntered(void) 162 | { 163 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 164 | 165 | if (!isolate->InContext()) 166 | return py::object(); 167 | 168 | v8::HandleScope handle_scope(isolate); 169 | 170 | v8::Handle entered = isolate->GetEnteredOrMicrotaskContext(); 171 | 172 | return (entered.IsEmpty()) ? py::object() : 173 | py::object(py::handle<>(boost::python::converter::shared_ptr_to_python(CContextPtr(new CContext(entered))))); 174 | } 175 | 176 | py::object CContext::GetCurrent(void) 177 | { 178 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 179 | if (!isolate->InContext()) 180 | return py::object(); 181 | 182 | v8::HandleScope handle_scope(isolate); 183 | 184 | v8::Handle current = isolate->GetCurrentContext(); 185 | 186 | return (current.IsEmpty()) ? py::object() : 187 | py::object(py::handle<>(boost::python::converter::shared_ptr_to_python(CContextPtr(new CContext(current))))); 188 | } 189 | 190 | py::object CContext::GetCalling(void) 191 | { 192 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 193 | 194 | if (!isolate->InContext()) 195 | return py::object(); 196 | 197 | v8::HandleScope handle_scope(isolate); 198 | 199 | v8::Handle calling = isolate->GetCurrentContext(); 200 | 201 | return (calling.IsEmpty()) ? py::object() : 202 | py::object(py::handle<>(boost::python::converter::shared_ptr_to_python(CContextPtr(new CContext(calling))))); 203 | } 204 | 205 | py::object CContext::Evaluate(const std::string& src, 206 | const std::string name, 207 | int line, int col) 208 | { 209 | CEngine engine(v8::Isolate::GetCurrent()); 210 | 211 | CScriptPtr script = engine.Compile(src, name, line, col); 212 | 213 | return script->Run(); 214 | } 215 | 216 | py::object CContext::EvaluateW(const std::wstring& src, 217 | const std::wstring name, 218 | int line, int col) 219 | { 220 | CEngine engine(v8::Isolate::GetCurrent()); 221 | 222 | CScriptPtr script = engine.CompileW(src, name, line, col); 223 | 224 | return script->Run(); 225 | } 226 | -------------------------------------------------------------------------------- /src/Engine.cpp: -------------------------------------------------------------------------------- 1 | #include "Engine.h" 2 | #include "Exception.h" 3 | #include "Wrapper.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | void CEngine::Expose(void) 12 | { 13 | py::class_("JSEngine", "JSEngine is a backend Javascript engine.") 14 | .def(py::init<>("Create a new script engine instance.")) 15 | .add_static_property("version", &CEngine::GetVersion, 16 | "Get the V8 engine version.") 17 | 18 | .add_static_property("dead", &CEngine::IsDead, 19 | "Check if V8 is dead and therefore unusable.") 20 | 21 | .def("setFlags", &CEngine::SetFlags, "Sets V8 flags from a string.") 22 | .staticmethod("setFlags") 23 | 24 | .def("terminateAllThreads", &CEngine::TerminateAllThreads, 25 | "Forcefully terminate the current thread of JavaScript execution.") 26 | .staticmethod("terminateAllThreads") 27 | 28 | .def("dispose", &v8::V8::Dispose, 29 | "Releases any resources used by v8 and stops any utility threads " 30 | "that may be running. Note that disposing v8 is permanent, " 31 | "it cannot be reinitialized.") 32 | .staticmethod("dispose") 33 | 34 | .def("lowMemory", &v8::Isolate::LowMemoryNotification, 35 | "Optional notification that the system is running low on memory.") 36 | .staticmethod("lowMemory") 37 | 38 | /* 39 | .def("setMemoryLimit", &CEngine::SetMemoryLimit, (py::arg("max_young_space_size") = 0, 40 | py::arg("max_old_space_size") = 0, 41 | py::arg("max_executable_size") = 0), 42 | "Specifies the limits of the runtime's memory use." 43 | "You must set the heap size before initializing the VM" 44 | "the size cannot be adjusted after the VM is initialized.") 45 | .staticmethod("setMemoryLimit") 46 | */ 47 | 48 | .def("setStackLimit", &CEngine::SetStackLimit, (py::arg("stack_limit_size") = 0), 49 | "Uses the address of a local variable to determine the stack top now." 50 | "Given a size, returns an address that is that far from the current top of stack.") 51 | .staticmethod("setStackLimit") 52 | 53 | /* 54 | .def("setMemoryAllocationCallback", &MemoryAllocationManager::SetCallback, 55 | (py::arg("callback"), 56 | py::arg("space") = v8::kObjectSpaceAll, 57 | py::arg("action") = v8::kAllocationActionAll), 58 | "Enables the host application to provide a mechanism to be notified " 59 | "and perform custom logging when V8 Allocates Executable Memory.") 60 | .staticmethod("setMemoryAllocationCallback") 61 | */ 62 | 63 | .def("compile", &CEngine::Compile, (py::arg("source"), 64 | py::arg("name") = std::string(), 65 | py::arg("line") = -1, 66 | py::arg("col") = -1)) 67 | .def("compile", &CEngine::CompileW, (py::arg("source"), 68 | py::arg("name") = std::wstring(), 69 | py::arg("line") = -1, 70 | py::arg("col") = -1)) 71 | ; 72 | 73 | py::class_("JSScript", "JSScript is a compiled JavaScript script.", py::no_init) 74 | .add_property("source", &CScript::GetSource, "the source code") 75 | 76 | .def("run", &CScript::Run, "Execute the compiled code.") 77 | ; 78 | 79 | py::objects::class_value_wrapper, 80 | py::objects::make_ptr_instance, CScript> > >(); 82 | } 83 | 84 | bool CEngine::IsDead(void) 85 | { 86 | return v8::Isolate::GetCurrent()->IsDead(); 87 | } 88 | 89 | void CEngine::TerminateAllThreads(void) 90 | { 91 | v8::Isolate::GetCurrent()->TerminateExecution(); 92 | } 93 | 94 | void CEngine::ReportFatalError(const char* location, const char* message) 95 | { 96 | std::cerr << "<" << location << "> " << message << std::endl; 97 | } 98 | 99 | void CEngine::ReportMessage(v8::Handle message, v8::Handle data) 100 | { 101 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 102 | v8::Local context = isolate->GetCurrentContext(); 103 | 104 | v8::String::Utf8Value filename(isolate, message->GetScriptResourceName()); 105 | int lineno = message->GetLineNumber(context).ToChecked(); 106 | v8::String::Utf8Value sourceline(isolate, message->GetSourceLine(context).ToLocalChecked()); 107 | 108 | std::cerr << *filename << ":" << lineno << " -> " << *sourceline << std::endl; 109 | } 110 | 111 | bool CEngine::SetMemoryLimit(int max_young_space_size, int max_old_space_size, int max_executable_size) 112 | { 113 | v8::ResourceConstraints limit; 114 | 115 | if (max_young_space_size) limit.set_max_young_generation_size_in_bytes(max_young_space_size); 116 | if (max_old_space_size) limit.set_max_old_generation_size_in_bytes(max_old_space_size); 117 | //TODO should this be code range size instead? 118 | //if (max_executable_size) limit.set_max_executable_size(max_executable_size); 119 | 120 | //TODO - memory limits are now only settable on isolate creation 121 | //return v8::SetResourceConstraints(v8::Isolate::GetCurrent(), &limit); 122 | return false; 123 | } 124 | 125 | void CEngine::SetStackLimit(uintptr_t stack_limit_size) 126 | { 127 | // This function uses a local stack variable to determine the isolate's stack limit 128 | uint32_t here; 129 | uintptr_t stack_limit = reinterpret_cast(&here) - stack_limit_size; 130 | 131 | // If the size is very large and the stack is very near the bottom of 132 | // memory then the calculation above may wrap around and give an address 133 | // that is above the (downwards-growing) stack. In that case we use a 134 | // very low address. 135 | if (stack_limit > reinterpret_cast(&here)) 136 | stack_limit = stack_limit_size; 137 | 138 | v8::Isolate::GetCurrent()->SetStackLimit(stack_limit); 139 | } 140 | 141 | std::shared_ptr CEngine::InternalCompile(v8::Handle src, 142 | v8::Handle name, 143 | int line, int col) 144 | { 145 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 146 | v8::HandleScope handle_scope(isolate); 147 | v8::Local context = isolate->GetCurrentContext(); 148 | 149 | v8::TryCatch try_catch(isolate); 150 | 151 | v8::Persistent script_source(m_isolate, src); 152 | 153 | v8::MaybeLocal script; 154 | v8::Handle source = v8::Local::New(m_isolate, script_source); 155 | 156 | Py_BEGIN_ALLOW_THREADS 157 | 158 | if (line >= 0 && col >= 0) 159 | { 160 | v8::ScriptOrigin script_origin(name, line, col); 161 | script = v8::Script::Compile(context, source, &script_origin); 162 | } 163 | else 164 | { 165 | v8::ScriptOrigin script_origin(name); 166 | script = v8::Script::Compile(context, source, &script_origin); 167 | } 168 | 169 | Py_END_ALLOW_THREADS 170 | 171 | if (script.IsEmpty()) CJavascriptException::ThrowIf(m_isolate, try_catch); 172 | 173 | return std::shared_ptr(new CScript(m_isolate, *this, script_source, script.ToLocalChecked())); 174 | } 175 | 176 | py::object CEngine::ExecuteScript(v8::Handle script) 177 | { 178 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 179 | v8::HandleScope handle_scope(isolate); 180 | v8::Local context = isolate->GetCurrentContext(); 181 | 182 | v8::TryCatch try_catch(isolate); 183 | 184 | v8::MaybeLocal result; 185 | 186 | Py_BEGIN_ALLOW_THREADS 187 | 188 | result = script->Run(context); 189 | 190 | Py_END_ALLOW_THREADS 191 | 192 | if (result.IsEmpty()) 193 | { 194 | if (try_catch.HasCaught()) 195 | { 196 | if(!try_catch.CanContinue() && PyErr_Occurred()) 197 | { 198 | throw py::error_already_set(); 199 | } 200 | 201 | CJavascriptException::ThrowIf(m_isolate, try_catch); 202 | } 203 | 204 | result = v8::Null(m_isolate); 205 | } 206 | 207 | return CJavascriptObject::Wrap(result.ToLocalChecked()); 208 | } 209 | 210 | const std::string CScript::GetSource(void) const 211 | { 212 | v8::HandleScope handle_scope(m_isolate); 213 | 214 | v8::String::Utf8Value source(m_isolate, Source()); 215 | 216 | return std::string(*source, source.length()); 217 | } 218 | 219 | py::object CScript::Run(void) 220 | { 221 | v8::HandleScope handle_scope(m_isolate); 222 | 223 | return m_engine.ExecuteScript(Script()); 224 | } 225 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import shutil 5 | import subprocess 6 | 7 | from setuptools import setup, Extension 8 | from setuptools.command.build_ext import build_ext 9 | from setuptools.command.install import install 10 | from wheel.bdist_wheel import bdist_wheel 11 | 12 | from settings import * # pylint:disable=wildcard-import,unused-wildcard-import 13 | 14 | log = logging.getLogger() 15 | 16 | ICU_DATA_PACKAGE_FOLDER = os.path.join(os.pardir, os.pardir, "stpyv8-icu") 17 | ICU_DATA_V8_FILE_PATH = os.path.join("v8", "out.gn", "x64.release.sample", "icudtl.dat") 18 | 19 | 20 | def exec_cmd(cmdline, *args, **kwargs): 21 | msg = kwargs.get("msg") 22 | cwd = kwargs.get("cwd", ".") 23 | output = kwargs.get("output") 24 | 25 | if msg: 26 | print(msg) 27 | 28 | cmdline = " ".join([cmdline] + list(args)) 29 | 30 | proc = subprocess.Popen( 31 | cmdline, # pylint:disable=consider-using-with 32 | shell=kwargs.get("shell", True), 33 | cwd=cwd, 34 | env=kwargs.get("env"), 35 | stdout=subprocess.PIPE if output else None, 36 | stderr=subprocess.PIPE if output else None, 37 | ) 38 | 39 | stdout, stderr = proc.communicate() 40 | 41 | succeeded = proc.returncode == 0 42 | 43 | if not succeeded: 44 | log.error("%s failed: code = %d", msg or "Execute command", proc.returncode) 45 | 46 | if output: 47 | log.debug(stderr) 48 | 49 | return succeeded, stdout, stderr if output else succeeded 50 | 51 | 52 | def install_depot(): 53 | if not os.path.exists(DEPOT_HOME): 54 | exec_cmd( 55 | "git clone --depth 1", 56 | DEPOT_GIT_URL, 57 | DEPOT_HOME, 58 | cwd=os.path.dirname(DEPOT_HOME), 59 | msg="Cloning depot tools", 60 | ) 61 | 62 | return 63 | 64 | # depot_tools updates itself automatically when running gclient tool 65 | if os.path.isfile(os.path.join(DEPOT_HOME, "gclient")): 66 | success, stdout, __ = exec_cmd( 67 | os.path.join(DEPOT_HOME, "gclient"), # pylint:disable=unused-variable 68 | "--version", 69 | cwd=DEPOT_HOME, 70 | output=True, 71 | msg="Found depot tools", 72 | ) 73 | 74 | if not success: 75 | exit(1) 76 | 77 | 78 | def checkout_v8(): 79 | install_depot() 80 | 81 | if not os.path.exists(V8_HOME): 82 | success, _, __ = exec_cmd( 83 | os.path.join(DEPOT_HOME, "fetch"), 84 | "--no-history", 85 | "v8", 86 | cwd=os.path.dirname(V8_HOME), 87 | msg="Fetching Google V8 code", 88 | ) 89 | 90 | if not success: 91 | exit(1) 92 | 93 | success, _, __ = exec_cmd( 94 | "git fetch --tags --quiet", 95 | cwd=V8_HOME, 96 | msg="Fetching the release tag information", 97 | ) 98 | 99 | if not success: 100 | exit(1) 101 | 102 | success, _, __ = exec_cmd( 103 | "git checkout", V8_GIT_TAG, cwd=V8_HOME, msg=f"Checkout Google V8 v{V8_GIT_TAG}" 104 | ) 105 | 106 | if not success: 107 | exit(1) 108 | 109 | success, _, __ = exec_cmd( 110 | os.path.join(DEPOT_HOME, "gclient"), 111 | "sync", 112 | "-D", 113 | "-j8", 114 | cwd=os.path.dirname(V8_HOME), 115 | msg="Syncing Google V8 code", 116 | ) 117 | 118 | if not success: 119 | exit(1) 120 | 121 | # On Linux, install additional dependencies, per 122 | # https://v8.dev/docs/build step 4 123 | if ( 124 | sys.platform 125 | in ( 126 | "linux", 127 | "linux2", 128 | ) 129 | and v8_deps_linux 130 | ): 131 | success, _, __ = exec_cmd( 132 | "./v8/build/install-build-deps.sh", 133 | cwd=os.path.dirname(V8_HOME), 134 | msg="Installing additional linux dependencies", 135 | ) 136 | 137 | if not success: 138 | exit(1) 139 | 140 | 141 | def build_v8(): 142 | args = f"gen {os.path.join('out.gn', 'x64.release.sample')} --args=\"{GN_ARGS}\"" 143 | success, _, __ = exec_cmd( 144 | os.path.join(DEPOT_HOME, "gn"), 145 | args, 146 | cwd=V8_HOME, 147 | msg=f"Generate build scripts for V8 (v{V8_GIT_TAG})", 148 | ) 149 | 150 | if not success: 151 | exit(1) 152 | 153 | success, _, __ = exec_cmd( 154 | os.path.join(DEPOT_HOME, "ninja"), 155 | f"-C {os.path.join('out.gn', 'x64.release.sample')} v8_monolith", 156 | cwd=V8_HOME, 157 | msg="Build V8 with ninja", 158 | ) 159 | 160 | if not success: 161 | exit(1) 162 | 163 | 164 | def clean_stpyv8(): 165 | build_folder = os.path.join(STPYV8_HOME, "build") 166 | 167 | if os.path.exists(os.path.join(build_folder)): 168 | shutil.rmtree(build_folder) 169 | 170 | 171 | def prepare_v8(): 172 | try: 173 | checkout_v8() 174 | build_v8() 175 | # clean_stpyv8() 176 | except Exception as e: # pylint:disable=broad-except 177 | log.error("Fail to checkout and build v8, %s", str(e)) 178 | 179 | 180 | class stpyv8_bdist_wheel(bdist_wheel): 181 | user_options = bdist_wheel.user_options + [ 182 | ("skip-build-v8", None, "don't build v8") 183 | ] 184 | 185 | def initialize_options(self) -> None: 186 | bdist_wheel.initialize_options(self) 187 | 188 | self.skip_build_v8 = None 189 | 190 | def run(self): 191 | if not self.skip_build_v8: 192 | prepare_v8() 193 | 194 | bdist_wheel.run(self) 195 | 196 | 197 | class stpyv8_build(build_ext): 198 | def run(self): 199 | V8_GIT_TAG = V8_GIT_TAG_STABLE # pylint:disable=redefined-outer-name,unused-variable 200 | 201 | build_ext.run(self) 202 | 203 | 204 | class stpyv8_develop(build_ext): 205 | def run(self): 206 | V8_GIT_TAG = V8_GIT_TAG_MASTER # pylint:disable=redefined-outer-name,unused-variable 207 | prepare_v8() 208 | build_ext.run(self) 209 | 210 | 211 | class stpyv8_install_v8(build_ext): 212 | def run(self): 213 | V8_GIT_TAG = V8_GIT_TAG_MASTER # pylint:disable=redefined-outer-name,unused-variable 214 | prepare_v8() 215 | 216 | 217 | class stpyv8_build_no_v8(build_ext): 218 | def run(self): 219 | clean_stpyv8() 220 | build_ext.run(self) 221 | 222 | 223 | class stpyv8_install(install): 224 | def run(self): 225 | self.skip_build = True # pylint:disable=attribute-defined-outside-init 226 | install.run(self) 227 | 228 | 229 | class stpyv8_checkout_v8(build_ext): 230 | def run(self): 231 | checkout_v8() 232 | 233 | 234 | stpyv8 = Extension( 235 | name="_STPyV8", 236 | sources=[os.path.join("src", source) for source in source_files], 237 | define_macros=macros, 238 | include_dirs=include_dirs, 239 | library_dirs=library_dirs, 240 | libraries=libraries, 241 | extra_compile_args=extra_compile_args, 242 | extra_link_args=extra_link_args, 243 | ) 244 | 245 | setup( 246 | name="stpyv8", 247 | version=STPYV8_VERSION, 248 | description="Python Wrapper for Google V8 Engine", 249 | long_description=open("README.md").read(), 250 | platforms=["Linux", "MacOS", "Windows"], 251 | author="Philip Syme, Angelo Dell'Aera", 252 | url="https://github.com/cloudflare/stpyv8", 253 | license="Apache License 2.0", 254 | py_modules=["STPyV8"], 255 | ext_modules=[stpyv8], 256 | install_requires=["importlib_resources; python_version < '3.10'"], 257 | setup_requires=["wheel"], 258 | data_files=[ 259 | (ICU_DATA_PACKAGE_FOLDER, [ICU_DATA_V8_FILE_PATH]), 260 | ], 261 | classifiers=[ 262 | "Development Status :: 5 - Production/Stable", 263 | "Environment :: Plugins", 264 | "Intended Audience :: Developers", 265 | "Intended Audience :: System Administrators", 266 | "License :: OSI Approved :: Apache Software License", 267 | "Natural Language :: English", 268 | "Programming Language :: C++", 269 | "Programming Language :: Python", 270 | "Topic :: Internet", 271 | "Topic :: Internet :: WWW/HTTP", 272 | "Topic :: Software Development", 273 | "Topic :: Software Development :: Libraries :: Python Modules", 274 | "Topic :: Utilities", 275 | "Operating System :: POSIX :: Linux", 276 | "Operating System :: MacOS :: MacOS X", 277 | "Operating System :: Microsoft :: Windows", 278 | "Programming Language :: Python :: 3", 279 | "Programming Language :: Python :: 3.9", 280 | "Programming Language :: Python :: 3.10", 281 | "Programming Language :: Python :: 3.11", 282 | "Programming Language :: Python :: 3.12", 283 | ], 284 | long_description_content_type="text/markdown", 285 | cmdclass=dict( 286 | bdist_wheel=stpyv8_bdist_wheel, 287 | build_ext=stpyv8_build, 288 | develop=stpyv8_develop, 289 | v8=stpyv8_install_v8, 290 | stpyv8=stpyv8_build_no_v8, 291 | install=stpyv8_install, 292 | checkout_v8=stpyv8_checkout_v8, 293 | ), 294 | ) 295 | -------------------------------------------------------------------------------- /src/Wrapper.h: -------------------------------------------------------------------------------- 1 | //TODO make this an ifdef guard 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Exception.h" 10 | 11 | #include 12 | 13 | class CJavascriptObject; 14 | class CJavascriptFunction; 15 | 16 | typedef std::shared_ptr CJavascriptObjectPtr; 17 | typedef std::shared_ptr CJavascriptFunctionPtr; 18 | 19 | class CJavascriptObject; 20 | 21 | struct CWrapper 22 | { 23 | static void Expose(void); 24 | }; 25 | 26 | class CPythonObject 27 | { 28 | private: 29 | CPythonObject(); 30 | virtual ~CPythonObject(); 31 | 32 | public: 33 | static v8::Intercepted NamedGetter(v8::Local prop, const v8::PropertyCallbackInfo& info); 34 | static v8::Intercepted NamedSetter(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info); 35 | static v8::Intercepted NamedQuery(v8::Local prop, const v8::PropertyCallbackInfo& info); 36 | static v8::Intercepted NamedDeleter(v8::Local prop, const v8::PropertyCallbackInfo& info); 37 | static void NamedEnumerator(const v8::PropertyCallbackInfo& info); 38 | 39 | static v8::Intercepted IndexedGetter(uint32_t index, const v8::PropertyCallbackInfo& info); 40 | static v8::Intercepted IndexedSetter(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& info); 41 | static v8::Intercepted IndexedQuery(uint32_t index, const v8::PropertyCallbackInfo& info); 42 | static v8::Intercepted IndexedDeleter(uint32_t index, const v8::PropertyCallbackInfo& info); 43 | static void IndexedEnumerator(const v8::PropertyCallbackInfo& info); 44 | 45 | static void Caller(const v8::FunctionCallbackInfo& info); 46 | 47 | #ifdef SUPPORT_TRACE_LIFECYCLE 48 | static void DisposeCallback(v8::Persistent object, void* parameter); 49 | #endif 50 | 51 | static void SetupObjectTemplate(v8::Isolate *isolate, v8::Handle clazz); 52 | static v8::Handle CreateObjectTemplate(v8::Isolate *isolate); 53 | 54 | static v8::Handle WrapInternal(py::object obj); 55 | 56 | static bool IsWrapped(v8::Handle obj); 57 | static v8::Handle Wrap(py::object obj); 58 | static py::object Unwrap(v8::Handle obj); 59 | static void Dispose(v8::Handle value); 60 | 61 | static void ThrowIf(v8::Isolate* isolate); 62 | }; 63 | 64 | struct ILazyObject 65 | { 66 | virtual void LazyConstructor(void) = 0; 67 | }; 68 | 69 | class CJavascriptObject : public CWrapper 70 | { 71 | protected: 72 | v8::Persistent m_obj; 73 | 74 | void CheckAttr(v8::Handle name) const; 75 | 76 | CJavascriptObject() 77 | { 78 | } 79 | public: 80 | CJavascriptObject(v8::Handle obj) 81 | : m_obj(v8::Isolate::GetCurrent(), obj) 82 | { 83 | } 84 | 85 | virtual ~CJavascriptObject() 86 | { 87 | m_obj.Reset(); 88 | } 89 | 90 | v8::Local Object(void) const { 91 | return v8::Local::New(v8::Isolate::GetCurrent(), m_obj); 92 | } 93 | 94 | py::object GetAttr(const std::string& name); 95 | void SetAttr(const std::string& name, py::object value); 96 | void DelAttr(const std::string& name); 97 | 98 | py::list GetAttrList(void); 99 | int GetIdentityHash(void); 100 | CJavascriptObjectPtr Clone(void); 101 | 102 | bool Contains(const std::string& name); 103 | 104 | operator long() const; 105 | operator double() const; 106 | operator bool() const; 107 | 108 | bool Equals(CJavascriptObjectPtr other) const; 109 | bool Unequals(CJavascriptObjectPtr other) const { 110 | return !Equals(other); 111 | } 112 | 113 | void Dump(std::ostream& os) const; 114 | 115 | static py::object Wrap(CJavascriptObject *obj); 116 | static py::object Wrap(v8::Handle value, 117 | v8::Handle self = v8::Handle()); 118 | static py::object Wrap(v8::Handle obj, 119 | v8::Handle self = v8::Handle()); 120 | }; 121 | 122 | class CJavascriptNull : public CJavascriptObject 123 | { 124 | public: 125 | bool nonzero(void) const { 126 | return false; 127 | } 128 | const std::string str(void) const { 129 | return "null"; 130 | } 131 | }; 132 | 133 | class CJavascriptUndefined : public CJavascriptObject 134 | { 135 | public: 136 | bool nonzero(void) const { 137 | return false; 138 | } 139 | const std::string str(void) const { 140 | return "undefined"; 141 | } 142 | }; 143 | 144 | class CJavascriptArray : public CJavascriptObject, public ILazyObject 145 | { 146 | py::object m_items; 147 | size_t m_size; 148 | public: 149 | class ArrayIterator 150 | : public boost::iterator_facade 151 | { 152 | CJavascriptArray *m_array; 153 | size_t m_idx; 154 | public: 155 | ArrayIterator(CJavascriptArray *array, size_t idx) 156 | : m_array(array), m_idx(idx) 157 | { 158 | } 159 | 160 | void increment() { 161 | m_idx++; 162 | } 163 | 164 | bool equal(ArrayIterator const& other) const { 165 | return m_array == other.m_array && m_idx == other.m_idx; 166 | } 167 | 168 | reference dereference() const { 169 | return m_array->GetItem(py::long_(m_idx)); 170 | } 171 | }; 172 | 173 | CJavascriptArray(v8::Handle array) 174 | : CJavascriptObject(array), m_size(array->Length()) 175 | { 176 | } 177 | 178 | CJavascriptArray(py::object items) 179 | : m_items(items), m_size(0) 180 | { 181 | } 182 | 183 | size_t Length(void); 184 | 185 | py::object GetItem(py::object key); 186 | py::object SetItem(py::object key, py::object value); 187 | py::object DelItem(py::object key); 188 | bool Contains(py::object item); 189 | 190 | ArrayIterator begin(void) { 191 | return ArrayIterator(this, 0); 192 | } 193 | ArrayIterator end(void) { 194 | return ArrayIterator(this, Length()); 195 | } 196 | 197 | // ILazyObject 198 | virtual void LazyConstructor(void); 199 | }; 200 | 201 | class CJavascriptFunction : public CJavascriptObject 202 | { 203 | v8::Persistent m_self; 204 | 205 | py::object Call(v8::Handle self, py::list args, py::dict kwds); 206 | public: 207 | CJavascriptFunction(v8::Handle self, v8::Handle func) 208 | : CJavascriptObject(func), m_self(v8::Isolate::GetCurrent(), self) 209 | { 210 | } 211 | 212 | ~CJavascriptFunction() 213 | { 214 | m_self.Reset(); 215 | } 216 | 217 | v8::Handle Self(void) const { 218 | return v8::Local::New(v8::Isolate::GetCurrent(), m_self); 219 | } 220 | 221 | static py::object CallWithArgs(py::tuple args, py::dict kwds); 222 | static py::object CreateWithArgs(CJavascriptFunctionPtr proto, py::tuple args, py::dict kwds); 223 | 224 | py::object ApplyJavascript(CJavascriptObjectPtr self, py::list args, py::dict kwds); 225 | py::object ApplyPython(py::object self, py::list args, py::dict kwds); 226 | py::object Invoke(py::list args, py::dict kwds); 227 | 228 | const std::string GetName(void) const; 229 | void SetName(const std::string& name); 230 | 231 | int GetLineNumber(void) const; 232 | int GetColumnNumber(void) const; 233 | const std::string GetResourceName(void) const; 234 | const std::string GetInferredName(void) const; 235 | int GetLineOffset(void) const; 236 | int GetColumnOffset(void) const; 237 | 238 | py::object GetOwner(void) const; 239 | }; 240 | 241 | #ifdef SUPPORT_TRACE_LIFECYCLE 242 | 243 | class ObjectTracer; 244 | 245 | typedef std::map LivingMap; 246 | 247 | class ObjectTracer 248 | { 249 | v8::Persistent m_handle; 250 | std::unique_ptr m_object; 251 | 252 | LivingMap *m_living; 253 | 254 | void Trace(void); 255 | 256 | static void WeakCallback(const v8::WeakCallbackInfo& info); 257 | 258 | static LivingMap *GetLivingMapping(void); 259 | public: 260 | ObjectTracer(v8::Handle handle, py::object *object); 261 | ~ObjectTracer(void); 262 | 263 | const v8::Persistent& Handle(void) const { 264 | return m_handle; 265 | } 266 | py::object *Object(void) const { 267 | return m_object.get(); 268 | } 269 | 270 | void Dispose(void); 271 | 272 | static ObjectTracer& Trace(v8::Handle handle, py::object *object); 273 | 274 | static v8::Handle FindCache(py::object obj); 275 | }; 276 | 277 | class ContextTracer 278 | { 279 | v8::Persistent m_ctxt; 280 | std::unique_ptr m_living; 281 | 282 | void Trace(void); 283 | 284 | static void WeakCallback(const v8::WeakCallbackInfo& info); 285 | public: 286 | ContextTracer(v8::Handle ctxt, LivingMap *living); 287 | ~ContextTracer(void); 288 | 289 | v8::Handle Context(void) const { 290 | return v8::Local::New(v8::Isolate::GetCurrent(), m_ctxt); 291 | } 292 | 293 | static void Trace(v8::Handle ctxt, LivingMap *living); 294 | }; 295 | 296 | #endif 297 | -------------------------------------------------------------------------------- /src/utf8/unchecked.h: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Nemanja Trifunovic 2 | 3 | /* 4 | Permission is hereby granted, free of charge, to any person or organization 5 | obtaining a copy of the software and accompanying documentation covered by 6 | this license (the "Software") to use, reproduce, display, distribute, 7 | execute, and transmit the Software, and to prepare derivative works of the 8 | Software, and to permit third-parties to whom the Software is furnished to 9 | do so, all subject to the following: 10 | 11 | The copyright notices in the Software and this entire statement, including 12 | the above license grant, this restriction and the following disclaimer, 13 | must be included in all copies of the Software, in whole or in part, and 14 | all derivative works of the Software, unless such copies or derivative 15 | works are solely in the form of machine-executable object code generated by 16 | a source language processor. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 21 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 22 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | 28 | #ifndef UTF8_FOR_CPP_UNCHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 29 | #define UTF8_FOR_CPP_UNCHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 30 | 31 | #include "core.h" 32 | 33 | namespace utf8 34 | { 35 | namespace unchecked 36 | { 37 | template 38 | octet_iterator append(uint32_t cp, octet_iterator result) 39 | { 40 | if (cp < 0x80) // one octet 41 | *(result++) = static_cast(cp); 42 | else if (cp < 0x800) { // two octets 43 | *(result++) = static_cast((cp >> 6) | 0xc0); 44 | *(result++) = static_cast((cp & 0x3f) | 0x80); 45 | } 46 | else if (cp < 0x10000) { // three octets 47 | *(result++) = static_cast((cp >> 12) | 0xe0); 48 | *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); 49 | *(result++) = static_cast((cp & 0x3f) | 0x80); 50 | } 51 | else { // four octets 52 | *(result++) = static_cast((cp >> 18) | 0xf0); 53 | *(result++) = static_cast(((cp >> 12) & 0x3f)| 0x80); 54 | *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); 55 | *(result++) = static_cast((cp & 0x3f) | 0x80); 56 | } 57 | return result; 58 | } 59 | 60 | template 61 | uint32_t next(octet_iterator& it) 62 | { 63 | uint32_t cp = internal::mask8(*it); 64 | typename std::iterator_traits::difference_type length = utf8::internal::sequence_length(it); 65 | switch (length) { 66 | case 1: 67 | break; 68 | case 2: 69 | it++; 70 | cp = ((cp << 6) & 0x7ff) + ((*it) & 0x3f); 71 | break; 72 | case 3: 73 | ++it; 74 | cp = ((cp << 12) & 0xffff) + ((internal::mask8(*it) << 6) & 0xfff); 75 | ++it; 76 | cp += (*it) & 0x3f; 77 | break; 78 | case 4: 79 | ++it; 80 | cp = ((cp << 18) & 0x1fffff) + ((internal::mask8(*it) << 12) & 0x3ffff); 81 | ++it; 82 | cp += (internal::mask8(*it) << 6) & 0xfff; 83 | ++it; 84 | cp += (*it) & 0x3f; 85 | break; 86 | } 87 | ++it; 88 | return cp; 89 | } 90 | 91 | template 92 | uint32_t peek_next(octet_iterator it) 93 | { 94 | return next(it); 95 | } 96 | 97 | template 98 | uint32_t prior(octet_iterator& it) 99 | { 100 | while (internal::is_trail(*(--it))) ; 101 | octet_iterator temp = it; 102 | return next(temp); 103 | } 104 | 105 | // Deprecated in versions that include prior, but only for the sake of consistency (see utf8::previous) 106 | template 107 | inline uint32_t previous(octet_iterator& it) 108 | { 109 | return prior(it); 110 | } 111 | 112 | template 113 | void advance (octet_iterator& it, distance_type n) 114 | { 115 | for (distance_type i = 0; i < n; ++i) 116 | next(it); 117 | } 118 | 119 | template 120 | typename std::iterator_traits::difference_type 121 | distance (octet_iterator first, octet_iterator last) 122 | { 123 | typename std::iterator_traits::difference_type dist; 124 | for (dist = 0; first < last; ++dist) 125 | next(first); 126 | return dist; 127 | } 128 | 129 | template 130 | octet_iterator utf16to8 (u16bit_iterator start, u16bit_iterator end, octet_iterator result) 131 | { 132 | while (start != end) { 133 | uint32_t cp = internal::mask16(*start++); 134 | // Take care of surrogate pairs first 135 | if (internal::is_lead_surrogate(cp)) { 136 | uint32_t trail_surrogate = internal::mask16(*start++); 137 | cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; 138 | } 139 | result = append(cp, result); 140 | } 141 | return result; 142 | } 143 | 144 | template 145 | u16bit_iterator utf8to16 (octet_iterator start, octet_iterator end, u16bit_iterator result) 146 | { 147 | while (start < end) { 148 | uint32_t cp = next(start); 149 | if (cp > 0xffff) { //make a surrogate pair 150 | *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); 151 | *result++ = static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); 152 | } 153 | else 154 | *result++ = static_cast(cp); 155 | } 156 | return result; 157 | } 158 | 159 | template 160 | octet_iterator utf32to8 (u32bit_iterator start, u32bit_iterator end, octet_iterator result) 161 | { 162 | while (start != end) 163 | result = append(*(start++), result); 164 | 165 | return result; 166 | } 167 | 168 | template 169 | u32bit_iterator utf8to32 (octet_iterator start, octet_iterator end, u32bit_iterator result) 170 | { 171 | while (start < end) 172 | (*result++) = next(start); 173 | 174 | return result; 175 | } 176 | 177 | // The iterator class 178 | template 179 | // class iterator : public std::iterator { 180 | class iterator { 181 | octet_iterator it; 182 | public: 183 | using iterator_category = std::bidirectional_iterator_tag; 184 | using value_type = uint32_t; 185 | 186 | iterator () {}; 187 | explicit iterator (const octet_iterator& octet_it): it(octet_it) {} 188 | // the default "big three" are OK 189 | octet_iterator base () const { return it; } 190 | uint32_t operator * () const 191 | { 192 | octet_iterator temp = it; 193 | return next(temp); 194 | } 195 | bool operator == (const iterator& rhs) const 196 | { 197 | return (it == rhs.it); 198 | } 199 | bool operator != (const iterator& rhs) const 200 | { 201 | return !(operator == (rhs)); 202 | } 203 | iterator& operator ++ () 204 | { 205 | std::advance(it, internal::sequence_length(it)); 206 | return *this; 207 | } 208 | iterator operator ++ (int) 209 | { 210 | iterator temp = *this; 211 | std::advance(it, internal::sequence_length(it)); 212 | return temp; 213 | } 214 | iterator& operator -- () 215 | { 216 | prior(it); 217 | return *this; 218 | } 219 | iterator operator -- (int) 220 | { 221 | iterator temp = *this; 222 | prior(it); 223 | return temp; 224 | } 225 | }; // class iterator 226 | 227 | } // namespace utf8::unchecked 228 | } // namespace utf8 229 | 230 | 231 | #endif // header guard 232 | 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STPyV8 2 | 3 | STPyV8 allows interoperability between Python 3 and JavaScript running Google V8 engine. 4 | Use STPyV8 to embed JavaScript code directly into your Python project, or to call Python 5 | code from JavaScript. 6 | 7 | STPyV8 is a fork of the original [PyV8](https://code.google.com/archive/p/pyv8/) project, 8 | with code changed to work with the latest Google V8 engine and Python 3. STPyV8 links with 9 | Google V8 built as a static library. 10 | 11 | Currently the library builds on Linux, MacOS and Windows. 12 | 13 | # Usage Examples 14 | 15 | Wrapping a JavaScript function in a Python function: 16 | 17 | ```Python 18 | # simple.py 19 | import STPyV8 20 | 21 | with STPyV8.JSContext() as ctxt: 22 | upcase = ctxt.eval(""" 23 | ( (lowerString) => { 24 | return lowerString.toUpperCase(); 25 | }) 26 | """) 27 | print(upcase("hello world!")) 28 | ``` 29 | 30 | ```Shell 31 | $ python simple.py 32 | HELLO WORLD! 33 | ``` 34 | 35 | ## Using Python in V8 36 | 37 | STPyV8 allows you to use Python functions, classes, and objects from within V8. 38 | 39 | Exporting a Python class into V8 and using it from JavaScript: 40 | 41 | ```Python 42 | # meaning.py 43 | import STPyV8 44 | 45 | class MyClass(STPyV8.JSClass): 46 | def reallyComplexFunction(self, addme): 47 | return 10 * 3 + addme 48 | 49 | my_class = MyClass() 50 | 51 | with STPyV8.JSContext(my_class) as ctxt: 52 | meaning = ctxt.eval("this.reallyComplexFunction(2) + 10;") 53 | print("The meaning of life: " + str(meaning)) 54 | ``` 55 | 56 | ```Shell 57 | $ python meaning.py 58 | The meaning of life: 42 59 | ``` 60 | 61 | ## Using JavaScript in Python 62 | 63 | STPyV8 allows you to use JavaScript functions, classes, object from Python. 64 | 65 | Calling methods on a JavaScript class from Python code: 66 | 67 | ```Python 68 | # circle.py 69 | import STPyV8 70 | 71 | with STPyV8.JSContext() as ctxt: 72 | ctxt.eval(""" 73 | class Circle { 74 | constructor(radius) { 75 | this.radius = radius; 76 | } 77 | get area() { 78 | return this.calcArea() 79 | } 80 | calcArea() { 81 | return 3.14 * this.radius * this.radius; 82 | } 83 | } 84 | """) 85 | circle = ctxt.eval("new Circle(10)") 86 | print("Area of the circle: " + str(circle.area)) 87 | ``` 88 | 89 | ```Shell 90 | $ python cicle.py 91 | Area of the circle: 314 92 | ``` 93 | 94 | Find more in the [tests](tests) directory. 95 | 96 | # Installing 97 | 98 | STPyV8 is avaliable on PyPI (starting from release v12.0.267.16) and officially supports 99 | Python 3.9+ 100 | 101 | ```Shell 102 | $ pip install stpyv8 103 | ``` 104 | 105 | Be aware that, starting from STPyV8 v12.0.267.14, installing boost-python and some other 106 | boost dependencies is not required anymore while it is still required if you're installing 107 | older versions (see later for details). Most Linux distributions and MacOS provide easy to 108 | install Boost packages and this is the suggested way to install the library in case you 109 | need it. 110 | 111 | If you are planning to install a version older than v12.0.267.16 you should use one of 112 | the Python wheels provided at [Releases](https://github.com/cloudflare/stpyv8/releases). 113 | The wheels are automatically generated using Github Actions and multiple platforms and 114 | Python versions are supported. In such case, you need to download the zip file for the 115 | proper platform and Python version. Each zip file contains the ICU data file icudtl.dat 116 | and the wheel itself. First of all you should copy icudtl.data to the STPyV8 ICU data 117 | folder (Linux: /usr/share/stpyv8, MacOS: /Library/Application Support/STPyV8/) and then 118 | install/upgrade STPyV8 using pip. 119 | 120 | Installing on MacOS 121 | 122 | ```Shell 123 | $ unzip stpyv8-macos-10.15-python-3.9.zip 124 | Archive: stpyv8-macos-10.15-python-3.9.zip 125 | inflating: stpyv8-macos-10.15-3.9/icudtl.dat 126 | inflating: stpyv8-macos-10.15-3.9/stpyv8-9.9.115.8-cp39-cp39-macosx_10_15_x86_64.whl 127 | $ cd stpyv8-macos-10.15-3.9 128 | $ sudo mv icudtl.dat /Library/Application\ Support/STPyV8 129 | $ pip install --upgrade stpyv8-9.9.115.8-cp39-cp39-macosx_10_15_x86_64.whl 130 | Processing ./stpyv8-9.9.115.8-cp39-cp39-macosx_10_15_x86_64.whl 131 | Installing collected packages: stpyv8 132 | Successfully installed stpyv8-9.9.115.8 133 | ``` 134 | 135 | If no wheels are provided for your platform and Python version you are required to build 136 | STPyV8. 137 | 138 | # Building 139 | 140 | GCC/clang or equivalent and Python3 headers are needed to build the main STPyV8 source 141 | code, as well as boost-python and some other boost dependencies. 142 | 143 | ## Build Examples 144 | 145 | ### Ubuntu/Debian 146 | Building on Ubuntu and Debian distros: 147 | 148 | ```Shell 149 | $ sudo apt install python3 python3-dev build-essential libboost-dev libboost-system-dev libboost-python-dev libboost-iostreams-dev 150 | $ python setup.py build 151 | $ sudo python setup.py install 152 | ``` 153 | 154 | Building on other Linux distributions requires appropriate use of their package managers 155 | for these external dependencies, and some gymnastics for the V8 build dependencies. 156 | 157 | ### MacOS 158 | 159 | Building on MacOS requires full [XCode](https://developer.apple.com/xcode/) (not just the 160 | command line tools) to compile Google V8. The command line tools bundled with XCode are 161 | required (rather than the stand-alone command line tools, sometimes requiring 162 | [drastic measures](https://bugs.chromium.org/p/chromium/issues/detail?id=729990#c1) .) 163 | 164 | Using [HomeBrew](https://brew.sh) makes the boost-python and related dependencies easier for 165 | STPyV8: 166 | 167 | ```Shell 168 | $ brew install boost-python3 169 | $ python setup.py build 170 | $ sudo python setup.py install 171 | ``` 172 | 173 | More detailed build instructions are in the [docs](docs/source/build.rst) folder. 174 | 175 | ### Windows 176 | 177 | Please note that building STPyV8 on Windows may take a while and can be a pretty involved process. You're encouraged to download a precompiled binary from this repository. 178 | 179 | Here are the prerequisites for building on Windows: 180 | 181 | * MSVC 14.20 (packaged with Visual Studio 2019) 182 | * [Boost](https://boostorg.jfrog.io/ui/repos/tree/General/main/release) 183 | 184 | The following environment variables must be set: 185 | * `BOOST_ROOT` - Boost installation directory (e.g. `C:\local\boost_1_83_0`) 186 | * `Python_ROOT_DIR` - Python installation directory (e.g. `C:\python311`) 187 | 188 | #### Boost installation with precompiled binaries 189 | 190 | If your Boost installation comes with precompiled binaries you'll have to make sure they can be used to build this project. 191 | 192 | The binaries required are statically-linkable LIB files compiled with MSVC 14.20 and may look something like this (in a Boost 1.82 installation): 193 | 194 | boost_python310-vc142-mt-s-x64-1_82.lib 195 | 196 | If you were able to locate a similar file for your Python version you might not need to build Boost. 197 | 198 | If the LIB file you found is not located in the directory `$env:BOOST_DIR\stage\lib` then you must add its containing directory path to the `LIB` environment variable. 199 | 200 | For example, if you installed Boost through an official installer, the LIB file might be in the `lib64-msvc-14.2` directory. In this case: `$env:LIB = "$env:LIB;$env:BOOST_ROOT\lib64-msvc-14.2"`. 201 | 202 | If you weren't able to located the correct file, or you encountered linking errors further down the build process, you'll have to build Boost. Here is an example of one such linking error: 203 | 204 | LINK : fatal error LNK1104: cannot open file 'libboost_python310-vc142-mt-s-x32-1_74.lib' 205 | 206 | #### Building Boost 207 | 208 | To build the Boost.Python component of Boost with Powershell Developer Console: 209 | 210 | ```powershell 211 | cd $env:BOOST_ROOT 212 | .\bootstrap.bat 213 | ``` 214 | 215 | Before building you must tell Boost which Python version you're building for. To do this, add the following line to the end of `project-config.jam`: 216 | 217 | using python : : "C:\\python311" ; 218 | 219 | NOTE: Use the actual path to your Python installation and ensure backslases are escaped. This directory should include `python.exe`, an `include` directory and a `libs` directory. 220 | 221 | Back to Powershell: 222 | 223 | ```powershell 224 | .\b2.exe stage -j 8 link=static runtime-link=static --with-python --with-iostreams --with-date_time --with-thread 225 | ``` 226 | 227 | The boost binaries will be generated to `$env:BOOST_ROOT\stage\lib`. 228 | 229 | #### Building STPyV8 230 | 231 | Once you've got your Boost binaries you're ready to build STPyV8. 232 | 233 | From Powershell, `cd` into the project root: 234 | 235 | ```powershell 236 | python -m pip install wheel 237 | python setup.py bdist_wheel 238 | ``` 239 | 240 | Once the second command is done (may take quite a while) you'll have a wheel file ready to be installed. 241 | 242 | # How does this work? 243 | STPyV8 is a Python [C++ Extension Module](https://docs.python.org/3/c-api/index.html) that 244 | links to an [embedded V8](https://v8.dev/docs/embed) library. Since PyV8 used the 245 | [Boost.Python C++](https://www.boost.org/doc/libs/1_70_0/libs/python/doc/html/index.html) 246 | library (as wells as some others) we kept it, but future work may include just using the C 247 | API exposed by Python and eliminating boost. Think of this as an Oreo cookie - Python and 248 | Google V8 crackers with C++ icing in the middle gluing them together. 249 | 250 | ## Is STPyV8 fast? 251 | STPyV8 needs to translate Python arguments (and JavaScript arguments) back and forth between 252 | function and method calls in the two languages. It does the minimum amount of work using native 253 | code, but if you are interested in the height of performance, make your interface between 254 | Python and JavaScript "chunky" ... i.e., make the minimum number of transitions between the two. 255 | 256 | # What can I use this for? 257 | We use STPyV8 to simulate a [browser](https://github.com/buffer/thug), and then execute sketchy 258 | JavaScript in an instrumented container. Other kinds of JavaScript sandboxing (simulating and 259 | monitoring the external world to JavaScript code) are a natural fit. 260 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /STPyV8.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import with_statement 5 | from __future__ import print_function 6 | 7 | import os 8 | import sys 9 | import re 10 | import collections.abc 11 | 12 | import _STPyV8 13 | 14 | __version__ = _STPyV8.JSEngine.version 15 | 16 | __all__ = [ 17 | "ReadOnly", 18 | "DontEnum", 19 | "DontDelete", 20 | "Internal", 21 | "JSError", 22 | "JSObject", 23 | "JSNull", 24 | "JSUndefined", 25 | "JSArray", 26 | "JSFunction", 27 | "JSClass", 28 | "JSEngine", 29 | "JSContext", 30 | "JSIsolate", 31 | "JSStackTrace", 32 | "JSStackFrame", 33 | "JSScript", 34 | "JSLocker", 35 | "JSUnlocker", 36 | "JSPlatform", 37 | ] 38 | 39 | 40 | # ICU 41 | ICU_DATA_FOLDERS_UNIX = ( 42 | "/usr/share/stpyv8", 43 | os.path.expanduser("~/.local/share/stpyv8"), 44 | ) 45 | ICU_DATA_FOLDERS_OSX = ( 46 | "/Library/Application Support/STPyV8", 47 | os.path.expanduser("~/Library/Application Support/STPyV8"), 48 | ) 49 | ICU_DATA_FOLDERS_WINDOWS = ( 50 | os.path.join(os.environ["PROGRAMDATA"], "STPyV8") 51 | if "PROGRAMDATA" in os.environ 52 | else None, 53 | os.path.join(os.environ["APPDATA"], "STPyV8") if "APPDATA" in os.environ else None, 54 | ) 55 | 56 | icu_data_folders = None 57 | if os.name in ("posix",): 58 | icu_data_folders = ( 59 | ICU_DATA_FOLDERS_OSX if sys.platform in ("darwin",) else ICU_DATA_FOLDERS_UNIX 60 | ) 61 | else: 62 | icu_data_folders = ICU_DATA_FOLDERS_WINDOWS 63 | 64 | 65 | class JSAttribute: 66 | def __init__(self, name): 67 | self.name = name 68 | 69 | def __call__(self, func): 70 | setattr(func, f"__{self.name}__", True) 71 | 72 | return func 73 | 74 | 75 | ReadOnly = JSAttribute(name="readonly") 76 | DontEnum = JSAttribute(name="dontenum") 77 | DontDelete = JSAttribute(name="dontdel") 78 | Internal = JSAttribute(name="internal") 79 | 80 | 81 | class JSError(Exception): 82 | def __init__(self, impl): 83 | Exception.__init__(self) 84 | self._impl = impl 85 | 86 | def __str__(self): 87 | return str(self._impl) 88 | 89 | def __getattribute__(self, attr): 90 | impl = super().__getattribute__("_impl") 91 | 92 | try: 93 | return getattr(impl, attr) 94 | except AttributeError: 95 | return super().__getattribute__(attr) 96 | 97 | RE_FRAME = re.compile( 98 | r"\s+at\s(?:new\s)?(?P.+)\s\((?P[^:]+):?(?P\d+)?:?(?P\d+)?\)" 99 | ) 100 | RE_FUNC = re.compile(r"\s+at\s(?:new\s)?(?P.+)\s\((?P[^\)]+)\)") 101 | RE_FILE = re.compile(r"\s+at\s(?P[^:]+):?(?P\d+)?:?(?P\d+)?") 102 | 103 | @staticmethod 104 | def parse_stack(value): 105 | stack = [] 106 | 107 | def int_or_nul(value): 108 | return int(value) if value else None 109 | 110 | for line in value.split("\n")[1:]: 111 | m = JSError.RE_FRAME.match(line) 112 | 113 | if m: 114 | stack.append( 115 | ( 116 | m.group("func"), 117 | m.group("file"), 118 | int_or_nul(m.group("row")), 119 | int_or_nul(m.group("col")), 120 | ) 121 | ) 122 | continue 123 | 124 | m = JSError.RE_FUNC.match(line) 125 | 126 | if m: 127 | stack.append((m.group("func"), m.group("file"), None, None)) 128 | continue 129 | 130 | m = JSError.RE_FILE.match(line) 131 | 132 | if m: 133 | stack.append( 134 | ( 135 | None, 136 | m.group("file"), 137 | int_or_nul(m.group("row")), 138 | int_or_nul(m.group("col")), 139 | ) 140 | ) 141 | continue 142 | 143 | assert line 144 | 145 | return stack 146 | 147 | @property 148 | def frames(self): 149 | return self.parse_stack(self.stackTrace) 150 | 151 | 152 | _STPyV8._JSError._jsclass = JSError # pylint:disable=protected-access 153 | 154 | JSObject = _STPyV8.JSObject 155 | JSNull = _STPyV8.JSNull 156 | JSUndefined = _STPyV8.JSUndefined 157 | JSArray = _STPyV8.JSArray 158 | JSFunction = _STPyV8.JSFunction 159 | JSPlatform = _STPyV8.JSPlatform 160 | 161 | 162 | class JSLocker(_STPyV8.JSLocker): 163 | def __enter__(self): 164 | self.enter() 165 | 166 | if JSContext.entered: 167 | self.leave() 168 | raise RuntimeError("Lock should be acquired before entering the context") 169 | 170 | return self 171 | 172 | def __exit__(self, exc_type, exc_value, traceback): 173 | if JSContext.entered: 174 | self.leave() 175 | raise RuntimeError("Lock should be released after leaving the context") 176 | 177 | self.leave() 178 | 179 | def __bool__(self): 180 | return self.entered() 181 | 182 | 183 | class JSUnlocker(_STPyV8.JSUnlocker): 184 | def __enter__(self): 185 | self.enter() 186 | return self 187 | 188 | def __exit__(self, exc_type, exc_value, traceback): 189 | self.leave() 190 | 191 | def __bool__(self): 192 | return self.entered() 193 | 194 | 195 | class JSClass: 196 | __properties__ = {} 197 | __watchpoints__ = {} 198 | 199 | def __getattr__(self, name): 200 | if name == "constructor": 201 | return JSClassConstructor(self.__class__) 202 | 203 | if name == "prototype": 204 | return JSClassPrototype(self.__class__) 205 | 206 | prop = self.__dict__.setdefault("__properties__", {}).get(name, None) 207 | 208 | if prop and isinstance(prop[0], collections.abc.Callable): 209 | return prop[0]() 210 | 211 | raise AttributeError(name) 212 | 213 | def __setattr__(self, name, value): 214 | prop = self.__dict__.setdefault("__properties__", {}).get(name, None) 215 | 216 | if prop and isinstance(prop[1], collections.abc.Callable): 217 | return prop[1](value) 218 | 219 | return object.__setattr__(self, name, value) 220 | 221 | def toString(self): 222 | """ 223 | Return the string representation of the object 224 | """ 225 | return f"[object {self.__class__.__name__}]" 226 | 227 | def toLocaleString(self): 228 | """ 229 | Return the string representation of the object as a string value 230 | appropriate to the environment current locale 231 | """ 232 | return self.toString() 233 | 234 | def valueOf(self): 235 | """ 236 | Return the primitive value of the object 237 | """ 238 | return self 239 | 240 | def hasOwnProperty(self, name): 241 | """ 242 | Return a boolean value indicating whether the object has a property 243 | with the specified name 244 | """ 245 | return hasattr(self, name) 246 | 247 | def isPrototypeOf(self, obj): 248 | """ 249 | Return a boolean value indicating whether the object exists in the 250 | prototype chain of another object 251 | """ 252 | raise NotImplementedError() 253 | 254 | def __defineGetter__(self, name, getter): 255 | """ 256 | Bind the object property to a function to be called when that property 257 | is looked up 258 | """ 259 | self.__properties__[name] = (getter, self.__lookupSetter__(name)) 260 | 261 | def __lookupGetter__(self, name): 262 | """ 263 | Return the function bound as a getter to the specified property 264 | """ 265 | return self.__properties__.get(name, (None, None))[0] 266 | 267 | def __defineSetter__(self, name, setter): 268 | """ 269 | Bind the object property to a function to be called when an attempt 270 | is made to set that property 271 | """ 272 | self.__properties__[name] = (self.__lookupGetter__(name), setter) 273 | 274 | def __lookupSetter__(self, name): 275 | """ 276 | Return the function bound as setter to the specified property 277 | """ 278 | return self.__properties__.get(name, (None, None))[1] 279 | 280 | def watch(self, prop, handler): 281 | """ 282 | Watch for a property to be assigned a value and runs a function when 283 | such assignment occurs 284 | """ 285 | self.__watchpoints__[prop] = handler 286 | 287 | def unwatch(self, prop): 288 | """ 289 | Remove a watchpoint set with the watch method 290 | """ 291 | del self.__watchpoints__[prop] 292 | 293 | 294 | class JSClassConstructor(JSClass): # pylint:disable=abstract-method 295 | def __init__(self, cls): 296 | self.cls = cls 297 | 298 | @property 299 | def name(self): 300 | return self.cls.__name__ 301 | 302 | def toString(self): 303 | return f"function {self.name}() {{\n [native code]\n}}" 304 | 305 | def __call__(self, *args, **kwds): 306 | return self.cls(*args, **kwds) 307 | 308 | 309 | class JSClassPrototype(JSClass): # pylint:disable=abstract-method 310 | def __init__(self, cls): 311 | self.cls = cls 312 | 313 | @property 314 | def constructor(self): 315 | return JSClassConstructor(self.cls) 316 | 317 | @property 318 | def name(self): 319 | return self.cls.__name__ 320 | 321 | 322 | class JSEngine(_STPyV8.JSEngine): 323 | def __init__(self): 324 | _STPyV8.JSEngine.__init__(self) 325 | 326 | def __enter__(self): 327 | return self 328 | 329 | def __exit__(self, exc_type, exc_value, traceback): 330 | del self 331 | 332 | 333 | JSScript = _STPyV8.JSScript 334 | JSStackTrace = _STPyV8.JSStackTrace 335 | JSStackTrace.Options = _STPyV8.JSStackTraceOptions 336 | JSStackTrace.GetCurrentStackTrace = staticmethod( 337 | lambda frame_limit, # pylint:disable=unnecessary-lambda 338 | options: _STPyV8.JSIsolate.current.GetCurrentStackTrace(frame_limit, options) 339 | ) 340 | JSStackFrame = _STPyV8.JSStackFrame 341 | 342 | 343 | class JSIsolate(_STPyV8.JSIsolate): 344 | def __enter__(self): 345 | self.enter() 346 | return self 347 | 348 | def __exit__(self, exc_type, exc_value, traceback): 349 | self.leave() 350 | del self 351 | 352 | 353 | class JSContext(_STPyV8.JSContext): 354 | def __init__(self, obj=None, ctxt=None): 355 | self.lock = JSLocker() 356 | self.lock.enter() 357 | 358 | if ctxt: 359 | _STPyV8.JSContext.__init__(self, ctxt) 360 | else: 361 | _STPyV8.JSContext.__init__(self, obj) 362 | 363 | def __enter__(self): 364 | self.enter() 365 | return self 366 | 367 | def __exit__(self, exc_type, exc_value, traceback): 368 | self.leave() 369 | 370 | if hasattr(JSLocker, "lock"): 371 | self.lock.leave() 372 | self.lock = None 373 | 374 | del self 375 | 376 | 377 | def icu_sync(): 378 | if sys.version_info < (3, 10): 379 | from importlib_resources import files 380 | else: 381 | from importlib.resources import files 382 | 383 | for folder in icu_data_folders: 384 | if not folder or not os.path.exists(folder): 385 | continue 386 | 387 | version_file = os.path.join(folder, "stpyv8-version.txt") 388 | if not os.path.exists(version_file): 389 | continue 390 | 391 | with open(version_file, encoding="utf-8", mode="r") as fd: 392 | version = fd.read() 393 | 394 | if version.strip() in (__version__,): 395 | return 396 | 397 | try: 398 | stpyv8_icu_files = files("stpyv8-icu") 399 | except ModuleNotFoundError: 400 | return 401 | 402 | for f in stpyv8_icu_files.iterdir(): 403 | if f.name not in ("icudtl.dat",): 404 | continue 405 | 406 | data = f.read_bytes() 407 | 408 | for folder in icu_data_folders: 409 | try: 410 | os.makedirs(folder, exist_ok=True) 411 | with open(os.path.join(folder, "icudtl.dat"), mode="wb") as fd: 412 | fd.write(data) 413 | 414 | version_file = os.path.join(folder, "stpyv8-version.txt") 415 | with open(version_file, encoding="utf-8", mode="w") as fd: 416 | fd.write(__version__) 417 | except PermissionError: 418 | pass 419 | 420 | 421 | icu_sync() 422 | 423 | v8_default_platform = JSPlatform() 424 | v8_default_platform.init() 425 | 426 | v8_default_isolate = JSIsolate() 427 | v8_default_isolate.enter() 428 | -------------------------------------------------------------------------------- /src/utf8/checked.h: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Nemanja Trifunovic 2 | 3 | /* 4 | Permission is hereby granted, free of charge, to any person or organization 5 | obtaining a copy of the software and accompanying documentation covered by 6 | this license (the "Software") to use, reproduce, display, distribute, 7 | execute, and transmit the Software, and to prepare derivative works of the 8 | Software, and to permit third-parties to whom the Software is furnished to 9 | do so, all subject to the following: 10 | 11 | The copyright notices in the Software and this entire statement, including 12 | the above license grant, this restriction and the following disclaimer, 13 | must be included in all copies of the Software, in whole or in part, and 14 | all derivative works of the Software, unless such copies or derivative 15 | works are solely in the form of machine-executable object code generated by 16 | a source language processor. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 21 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 22 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | 28 | #ifndef UTF8_FOR_CPP_CHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 29 | #define UTF8_FOR_CPP_CHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 30 | 31 | #include "core.h" 32 | #include 33 | 34 | namespace utf8 35 | { 36 | // Base for the exceptions that may be thrown from the library 37 | class exception : public std::exception { 38 | }; 39 | 40 | // Exceptions that may be thrown from the library functions. 41 | class invalid_code_point : public exception { 42 | uint32_t cp; 43 | public: 44 | invalid_code_point(uint32_t cp) : cp(cp) {} 45 | virtual const char* what() const throw() { return "Invalid code point"; } 46 | uint32_t code_point() const {return cp;} 47 | }; 48 | 49 | class invalid_utf8 : public exception { 50 | uint8_t u8; 51 | public: 52 | invalid_utf8 (uint8_t u) : u8(u) {} 53 | virtual const char* what() const throw() { return "Invalid UTF-8"; } 54 | uint8_t utf8_octet() const {return u8;} 55 | }; 56 | 57 | class invalid_utf16 : public exception { 58 | uint16_t u16; 59 | public: 60 | invalid_utf16 (uint16_t u) : u16(u) {} 61 | virtual const char* what() const throw() { return "Invalid UTF-16"; } 62 | uint16_t utf16_word() const {return u16;} 63 | }; 64 | 65 | class not_enough_room : public exception { 66 | public: 67 | virtual const char* what() const throw() { return "Not enough space"; } 68 | }; 69 | 70 | /// The library API - functions intended to be called by the users 71 | 72 | template 73 | output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out, uint32_t replacement) 74 | { 75 | while (start != end) { 76 | octet_iterator sequence_start = start; 77 | internal::utf_error err_code = internal::validate_next(start, end); 78 | switch (err_code) { 79 | case internal::UTF8_OK : 80 | for (octet_iterator it = sequence_start; it != start; ++it) 81 | *out++ = *it; 82 | break; 83 | case internal::NOT_ENOUGH_ROOM: 84 | throw not_enough_room(); 85 | case internal::INVALID_LEAD: 86 | append (replacement, out); 87 | ++start; 88 | break; 89 | case internal::INCOMPLETE_SEQUENCE: 90 | case internal::OVERLONG_SEQUENCE: 91 | case internal::INVALID_CODE_POINT: 92 | append (replacement, out); 93 | ++start; 94 | // just one replacement mark for the sequence 95 | while (internal::is_trail(*start) && start != end) 96 | ++start; 97 | break; 98 | } 99 | } 100 | return out; 101 | } 102 | 103 | template 104 | inline output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out) 105 | { 106 | static const uint32_t replacement_marker = internal::mask16(0xfffd); 107 | return replace_invalid(start, end, out, replacement_marker); 108 | } 109 | 110 | template 111 | octet_iterator append(uint32_t cp, octet_iterator result) 112 | { 113 | if (!internal::is_code_point_valid(cp)) 114 | throw invalid_code_point(cp); 115 | 116 | if (cp < 0x80) // one octet 117 | *(result++) = static_cast(cp); 118 | else if (cp < 0x800) { // two octets 119 | *(result++) = static_cast((cp >> 6) | 0xc0); 120 | *(result++) = static_cast((cp & 0x3f) | 0x80); 121 | } 122 | else if (cp < 0x10000) { // three octets 123 | *(result++) = static_cast((cp >> 12) | 0xe0); 124 | *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); 125 | *(result++) = static_cast((cp & 0x3f) | 0x80); 126 | } 127 | else { // four octets 128 | *(result++) = static_cast((cp >> 18) | 0xf0); 129 | *(result++) = static_cast(((cp >> 12) & 0x3f) | 0x80); 130 | *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); 131 | *(result++) = static_cast((cp & 0x3f) | 0x80); 132 | } 133 | return result; 134 | } 135 | 136 | template 137 | uint32_t next(octet_iterator& it, octet_iterator end) 138 | { 139 | uint32_t cp = 0; 140 | internal::utf_error err_code = internal::validate_next(it, end, &cp); 141 | switch (err_code) { 142 | case internal::UTF8_OK : 143 | break; 144 | case internal::NOT_ENOUGH_ROOM : 145 | throw not_enough_room(); 146 | case internal::INVALID_LEAD : 147 | case internal::INCOMPLETE_SEQUENCE : 148 | case internal::OVERLONG_SEQUENCE : 149 | throw invalid_utf8(*it); 150 | case internal::INVALID_CODE_POINT : 151 | throw invalid_code_point(cp); 152 | } 153 | return cp; 154 | } 155 | 156 | template 157 | uint32_t peek_next(octet_iterator it, octet_iterator end) 158 | { 159 | return next(it, end); 160 | } 161 | 162 | template 163 | uint32_t prior(octet_iterator& it, octet_iterator start) 164 | { 165 | octet_iterator end = it; 166 | while (internal::is_trail(*(--it))) 167 | if (it < start) 168 | throw invalid_utf8(*it); // error - no lead byte in the sequence 169 | octet_iterator temp = it; 170 | return next(temp, end); 171 | } 172 | 173 | /// Deprecated in versions that include "prior" 174 | template 175 | uint32_t previous(octet_iterator& it, octet_iterator pass_start) 176 | { 177 | octet_iterator end = it; 178 | while (internal::is_trail(*(--it))) 179 | if (it == pass_start) 180 | throw invalid_utf8(*it); // error - no lead byte in the sequence 181 | octet_iterator temp = it; 182 | return next(temp, end); 183 | } 184 | 185 | template 186 | void advance (octet_iterator& it, distance_type n, octet_iterator end) 187 | { 188 | for (distance_type i = 0; i < n; ++i) 189 | next(it, end); 190 | } 191 | 192 | template 193 | typename std::iterator_traits::difference_type 194 | distance (octet_iterator first, octet_iterator last) 195 | { 196 | typename std::iterator_traits::difference_type dist; 197 | for (dist = 0; first < last; ++dist) 198 | next(first, last); 199 | return dist; 200 | } 201 | 202 | template 203 | octet_iterator utf16to8 (u16bit_iterator start, u16bit_iterator end, octet_iterator result) 204 | { 205 | while (start != end) { 206 | uint32_t cp = internal::mask16(*start++); 207 | // Take care of surrogate pairs first 208 | if (internal::is_lead_surrogate(cp)) { 209 | if (start != end) { 210 | uint32_t trail_surrogate = internal::mask16(*start++); 211 | if (internal::is_trail_surrogate(trail_surrogate)) 212 | cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; 213 | else 214 | throw invalid_utf16(static_cast(trail_surrogate)); 215 | } 216 | else 217 | throw invalid_utf16(static_cast(cp)); 218 | 219 | } 220 | // Lone trail surrogate 221 | else if (internal::is_trail_surrogate(cp)) 222 | throw invalid_utf16(static_cast(cp)); 223 | 224 | result = append(cp, result); 225 | } 226 | return result; 227 | } 228 | 229 | template 230 | u16bit_iterator utf8to16 (octet_iterator start, octet_iterator end, u16bit_iterator result) 231 | { 232 | while (start != end) { 233 | uint32_t cp = next(start, end); 234 | if (cp > 0xffff) { //make a surrogate pair 235 | *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); 236 | *result++ = static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); 237 | } 238 | else 239 | *result++ = static_cast(cp); 240 | } 241 | return result; 242 | } 243 | 244 | template 245 | octet_iterator utf32to8 (u32bit_iterator start, u32bit_iterator end, octet_iterator result) 246 | { 247 | while (start != end) 248 | result = append(*(start++), result); 249 | 250 | return result; 251 | } 252 | 253 | template 254 | u32bit_iterator utf8to32 (octet_iterator start, octet_iterator end, u32bit_iterator result) 255 | { 256 | while (start != end) 257 | (*result++) = next(start, end); 258 | 259 | return result; 260 | } 261 | 262 | // The iterator class 263 | template 264 | // class iterator : public std::iterator { 265 | class iterator { 266 | octet_iterator it; 267 | octet_iterator range_start; 268 | octet_iterator range_end; 269 | public: 270 | using iterator_category = std::bidirectional_iterator_tag; 271 | using value_type = uint32_t; 272 | 273 | iterator () {}; 274 | explicit iterator (const octet_iterator& octet_it, 275 | const octet_iterator& range_start, 276 | const octet_iterator& range_end) : 277 | it(octet_it), range_start(range_start), range_end(range_end) 278 | { 279 | if (it < range_start || it > range_end) 280 | throw std::out_of_range("Invalid utf-8 iterator position"); 281 | } 282 | // the default "big three" are OK 283 | octet_iterator base () const { return it; } 284 | uint32_t operator * () const 285 | { 286 | octet_iterator temp = it; 287 | return next(temp, range_end); 288 | } 289 | bool operator == (const iterator& rhs) const 290 | { 291 | if (range_start != rhs.range_start || range_end != rhs.range_end) 292 | throw std::logic_error("Comparing utf-8 iterators defined with different ranges"); 293 | return (it == rhs.it); 294 | } 295 | bool operator != (const iterator& rhs) const 296 | { 297 | return !(operator == (rhs)); 298 | } 299 | iterator& operator ++ () 300 | { 301 | next(it, range_end); 302 | return *this; 303 | } 304 | iterator operator ++ (int) 305 | { 306 | iterator temp = *this; 307 | next(it, range_end); 308 | return temp; 309 | } 310 | iterator& operator -- () 311 | { 312 | prior(it, range_start); 313 | return *this; 314 | } 315 | iterator operator -- (int) 316 | { 317 | iterator temp = *this; 318 | prior(it, range_start); 319 | return temp; 320 | } 321 | }; // class iterator 322 | 323 | } // namespace utf8 324 | 325 | #endif //header guard 326 | --------------------------------------------------------------------------------