├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── DESIGN.md ├── Dockerfile ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── benchmark ├── babel │ ├── Gemfile │ ├── bench.rb │ ├── bench_node.js │ └── helper_files │ │ ├── babel.js │ │ └── composer.js.es6 └── exec_js_uglify │ ├── Gemfile │ ├── bench.rb │ └── helper_files │ ├── discourse_app.js │ └── discourse_app_minified.js ├── bin ├── console └── setup ├── examples └── source-map-support │ ├── .gitignore │ ├── error-causing-component.jsx │ ├── index.jsx │ ├── package.json │ ├── readme.md │ ├── renderer.rb │ └── webpack.config.js ├── ext ├── mini_racer_extension │ ├── extconf.rb │ ├── mini_racer_extension.c │ ├── mini_racer_v8.cc │ ├── mini_racer_v8.h │ └── serde.c └── mini_racer_loader │ ├── extconf.rb │ └── mini_racer_loader.c ├── lib ├── mini_racer.rb └── mini_racer │ ├── shared.rb │ ├── truffleruby.rb │ └── version.rb ├── mini_racer.gemspec └── test ├── file.js ├── function_test.rb ├── mini_racer_test.rb ├── smoke └── minimal.rb ├── support └── add.wasm ├── test_crash.rb ├── test_forking.rb ├── test_helper.rb ├── test_leak.rb └── test_multithread.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | lib/mini_racer_extension.so 11 | lib/mini_racer_loader.so 12 | *.bundle 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Look for updates to Github Action modules once a week 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test-truffleruby: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: 15 | - "macos-13" 16 | - "macos-14" # arm64 17 | - "macos-15" # arm64 18 | - "ubuntu-20.04" 19 | ruby: 20 | - "truffleruby+graalvm" 21 | 22 | name: ${{ matrix.os }} - ${{ matrix.ruby }} 23 | runs-on: ${{ matrix.os }} 24 | 25 | env: 26 | TRUFFLERUBYOPT: "--jvm --polyglot" 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{ matrix.ruby }} 33 | bundler: latest # to get this fix: https://github.com/rubygems/rubygems/issues/6165 34 | bundler-cache: true 35 | - name: Install GraalVM JS component 36 | run: truffleruby-polyglot-get js 37 | - name: Compile 38 | run: bundle exec rake compile 39 | - name: Test 40 | run: bundle exec rake test 41 | - name: Build & Install gem 42 | run: | 43 | bundle exec rake build 44 | gem uninstall --all --force mini_racer 45 | gem install pkg/*.gem 46 | - name: Smoke Test installed gem 47 | run: ruby test/smoke/minimal.rb 48 | 49 | test-darwin: 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | os: 54 | - "macos-13" 55 | - "macos-14" # arm64 56 | - "macos-15" 57 | ruby: 58 | - "ruby-3.1" 59 | - "ruby-3.2" 60 | - "ruby-3.3" 61 | - "ruby-3.4" 62 | 63 | name: ${{ matrix.os }} - ${{ matrix.ruby }} 64 | runs-on: ${{ matrix.os }} 65 | 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: ruby/setup-ruby@v1 69 | with: 70 | ruby-version: ${{ matrix.ruby }} 71 | bundler-cache: true 72 | - name: Compile 73 | run: bundle exec rake compile 74 | - name: Test 75 | run: bundle exec rake test 76 | - name: Build & Install gem 77 | run: | 78 | bundle exec rake build 79 | gem uninstall --all --force mini_racer 80 | gem install pkg/*.gem 81 | - name: Smoke Test installed gem 82 | run: ruby test/smoke/minimal.rb 83 | 84 | test-linux: 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | ruby: 89 | - "3.1" 90 | - "3.2" 91 | - "3.3" 92 | - "3.4" 93 | runner: 94 | - "ubuntu-24.04" 95 | - "ubuntu-24.04-arm" 96 | libc: 97 | - "gnu" 98 | - "musl" 99 | 100 | name: linux-${{ matrix.runner }} - ruby-${{ matrix.ruby }} - ${{ matrix.libc }} 101 | runs-on: ${{ matrix.runner }} 102 | 103 | steps: 104 | - name: Start container 105 | id: container 106 | run: | 107 | case ${{ matrix.libc }} in 108 | gnu) 109 | echo 'ruby:${{ matrix.ruby }}' 110 | ;; 111 | musl) 112 | echo 'ruby:${{ matrix.ruby }}-alpine' 113 | ;; 114 | esac > container_image 115 | echo "image=$(cat container_image)" >> $GITHUB_OUTPUT 116 | docker run --rm -d -v "${PWD}":"${PWD}" -w "${PWD}" $(cat container_image) /bin/sleep 64d | tee container_id 117 | docker exec -w "${PWD}" $(cat container_id) uname -a 118 | echo "container_id=$(cat container_id)" >> $GITHUB_OUTPUT 119 | - name: Install Alpine system dependencies 120 | if: ${{ matrix.libc == 'musl' }} 121 | run: docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} apk add --no-cache build-base bash git 122 | - name: Checkout 123 | uses: actions/checkout@v4 124 | - name: Update Rubygems 125 | run: docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} gem update --system 126 | - name: Bundle 127 | run: docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} bundle install 128 | - name: Compile 129 | run: docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} bundle exec rake compile 130 | - name: Test 131 | run: docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} bundle exec rake test 132 | - name: Build & Install gem 133 | run: | 134 | docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} bundle exec rake build 135 | docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} gem uninstall --all --force mini_racer 136 | docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} gem install pkg/*.gem 137 | - name: Smoke Test installed gem 138 | run: docker exec -w "${PWD}" ${{ steps.container.outputs.container_id }} ruby test/smoke/minimal.rb 139 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | lib/mini_racer_extension.so 11 | lib/mini_racer_loader.so 12 | *.bundle 13 | .vscode/ 14 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | - 0.18.1 - 03-04-2025 2 | - Convert round doubles to fixnum for very big floats - this has better parity with JavaScript - Ben Noorhuis 3 | 4 | - 0.18.0 - 05-03-2025 5 | - Time for a major release 6 | - Handle ActiveSupport TimeWithZone objects during serialization - Sam Saffron 7 | 8 | - 0.18.0.pre1 - 06-02-2025 9 | - Updated to node 23.6.1.0 10 | 11 | - 0.17.0.pre13 - 04-02-2025 12 | - Only issue idle GC once post dispatch - reduces CPU usage for auto cleanup - Sam Saffron 13 | 14 | - 0.17.0.pre12 - 23-01-2025 15 | - Corrected off-by-one error with object serialization - Ben Noordhuis 16 | 17 | - 0.17.0.pre11 - 21-01-2025 18 | - Corrected encoding bug with deserialization of strings - Ben Noordhuis 19 | 20 | - 0.17.0.pre10 - 20-01-2025 21 | - Added back support for partially deserialized objects (objects that do not translate across boundaries are returned as Error properties) - Ben Noordhuis 22 | 23 | - 0.17.0.pre9 - 13-01-2025 24 | - For backwards compatibility convert v8 return values to UTF-8 (invalidly encoded string still get returned using V8 encoding) 25 | 26 | - 0.17.0.pre8 - 11-01-2025 27 | - Fix handling of UTF 32 LE and Ascii encoding strings - Ben Noordhuis 28 | - Handle rare edge case in V8 serialization - Ben Noordhuis 29 | 30 | - 0.17.0.pre7 - 10-01-2025 31 | 32 | - Objects containing non serializable properties will return an Error object vs raising an exception. Ben Noordhuis 33 | - Truffle support was added back Eregon 34 | 35 | - 0.17.0.pre6 - 08-01-2025 36 | 37 | - Moved all mini_racer interaction with v8 to a dedicated native thread to avoid cross VM stack contamination. Ben Noordhuis 38 | 39 | - 0.17.0.pre5 - 30-09-2024 40 | 41 | - Handle segfault from JSON.stringify 42 | - Fix segfaults around symbol conversion 43 | - Fix crash on invalid date object 44 | 45 | 46 | - 0.17.0.pre4 - 18-09-2024 47 | 48 | - Attempt to change compilation flags to disable strict aliasing to see if it resolves stability issues 49 | 50 | - 0.17.0.pre3 - 15-09-2024 51 | 52 | - Text clang based Linux v8 build, in case there is an edge case with GCC compilation 53 | 54 | - 0.17.0.pre2 - 09-09-2024 55 | 56 | - Test build to see if disabling Maglev optimisations resolves segfault issues 57 | 58 | - 0.17.0.pre - 05-09-2024 59 | 60 | - Test build to see if disabling concurrent GC marking resolves segfault 61 | 62 | - 0.16.0 - 05-09-2024 63 | 64 | - Sadly still seeing segfaults, reverted back 18.19.0.0 65 | 66 | - 0.15.0 - 05-09-2024 67 | 68 | - Use libv8-node 22.7.0 - this corrects issues with multithreaded behavior and forking in single threaded mode. 69 | 70 | - 0.14.1 - 14-08-2024 71 | 72 | - No longer use mini_racer_loader if LD_PRELOAD is defined and adds a malloc provider. This resolves segfaults when people LD_PRELOAD jemalloc or tcmalloc. 73 | 74 | - 0.14.0 - 06-08-2024 75 | 76 | - Node 22.5.1.0 is not stable in production, reverting to last known good build 77 | 78 | - 0.13.0 - 29-07-2024 79 | 80 | - Target Node to 22.5.1.0 0 - corrects segfault in earlier release 81 | - Remove Ruby 3.0 which is EOL (use ealier version of gem if needed) 82 | 83 | - 0.9.0 - 25-03-2024 84 | 85 | - Target Node to 18.19.0.0 86 | 87 | - 0.8.0 - 29-05-2023 88 | 89 | - Target Node to 18.16.0.0 90 | - Drop supporting EOL'd Ruby 2.7 91 | 92 | - 0.7.0 - 26-05-2023 93 | 94 | - Target Node to 17.9.1.0 95 | 96 | - 0.6.4 - 25-05-2022 97 | 98 | - Target Node 16.19.0.0 99 | 100 | - 0.6.3 - 16-08-2022 101 | 102 | - Truffle ruby support! Thanks to Brandon Fish and the truffle team 103 | - Hide libv8 symbols on ELF targets 104 | - Slightly shrunk binary size 105 | - Simplified timeout implementation 106 | - Some stability fixes 107 | 108 | - 17-01-2022 - 0.6.2 109 | 110 | - Fix support for compilation on 2.6, llvm compiles 111 | - Stability patches to handle rare memory leaks 112 | - Corrected re-raising of exceptions to support strings 113 | - During early termination of context under certain conditions MiniRacer could crash 114 | 115 | - 0.6.1 - 31-12-2021 116 | 117 | - Added support for single threaded platform: `MiniRacer::Platform.set_flags! :single_threaded` 118 | must be called prior to booting ANY MiniRacer::Context 119 | 120 | - 0.6.0 - 11-04-2021 121 | 122 | - Ruby 3.1 support 123 | - Fixes memory leak in heap snapshotting 124 | - Improved compilation ergonomics in clang 125 | - Migrated internal storage in c extension to TypedData 126 | 127 | 128 | - 0.5.0 129 | 130 | - Fixes issues on aarch (Apple M1) 131 | - Update to use libv8-node 16.x (#210) [Loic Nageleisen] 132 | - FEATURE: Configurable max marshal stack depth (#202) [seanmakesgames] 133 | - Ruby 2.3 and 2.4 are EOL, we no longer support them 134 | 135 | - 0.4.0 - 08-04-2021 136 | 137 | - FEATURE: upgrade to libv8 node 15.14.0 (v8 8.6.395.17) 138 | - Promote 0.4.0.beta1 to release, using libv8-node release path 139 | 140 | 141 | - 0.4.0.beta1 - 23-07-2020 142 | 143 | - FIX: on downgrade mkmf was picking the wrong version of libv8, this fix will correct future issues 144 | - FEATURE: upgraded libv8 to use node libv8 build which supports M1 and various ARM builds v8 moved to (8.6.395.17) 145 | 146 | 147 | - 0.3.1 - 22-07-2020 148 | 149 | - FIX: specify that libv8 must be larger than 8.4.255 but smaller than 8.5, this avoids issues going forward 150 | 151 | - 0.3.0 - 29-06-2020 152 | 153 | - FEATURE: upgraded to libv8 version 8.4.255.0 154 | 155 | 156 | - 0.2.15 - 15-05-2020 157 | 158 | - FEATURE: basic wasm support via pump_message_loop 159 | 160 | 161 | - 0.2.14 - 15-05-2020 162 | 163 | - FIX: ensure_gc_after_idle should take in milliseconds like the rest of the APIs not seconds 164 | - FEATURE: strict params on MiniRacer::Context.new 165 | 166 | 167 | - 0.2.13 - 15-05-2020 168 | 169 | - FIX: edge case around ensure_gc_after_idle possibly firing when context is not idle 170 | 171 | 172 | - 0.2.12 - 14-05-2020 173 | 174 | - FEATURE: isolate.low_memory_notification which can force a full GC 175 | - FEATURE: MiniRacer::Context.new(ensure_gc_after_idle: 2) - to force full GC 2 seconds after context is idle, this allows you to conserve memory on isolates 176 | 177 | 178 | - 0.2.11 179 | 180 | - FIX: dumping heap snapshots was not flushing the file leading to corrupt snapshots 181 | - FIX: a use-after-free shutdown crash 182 | 183 | - 0.2.10 - 22-04-2020 184 | - FEATURE: memory softlimit support for nogvl_context_call 185 | 186 | - 0.2.9 - 09-01-2020 187 | 188 | - FIX: correct segfault when JS returns a Symbol and properly cast to ruby symbol 189 | 190 | - 0.2.8 - 11-11-2019 191 | 192 | 193 | - FIX: ensure thread live cycle is properly accounter for following file descriptor fix 194 | 195 | - 0.2.7 - 11-11-2019 196 | 197 | - FIX: release the file descriptor for timeout pipe earlier (this avoids holding too many files open in Ruby 2.7) 198 | 199 | - 0.2.6 - 14-05-2019 200 | 201 | - FEATURE: add support for write_heap_snapshot which helps you analyze memory 202 | 203 | - 0.2.5 - 25-04-2019 204 | 205 | - FIX: Compatibility fixes for V8 7 and above @ignisf 206 | - FIX: Memory leak in gc_callback @messense 207 | - IMPROVEMENT: Added example of sourcemap support @ianks 208 | - URGENT: you will need this release for latest version of libv8 to work 209 | 210 | - 0.2.4 - 02-11-2018 211 | 212 | - FIX: deadlock releasing context when shared isolates are used 213 | - FEATURE: include js backtrace when snapshots do not compile 214 | 215 | - 0.2.3 - 28-09-2018 216 | 217 | - Drop all conditional logic from Mini Racer compilation for clang, always 218 | rely on MacOS being High Sierra or up 219 | 220 | - 0.2.2 - 26-09-2018 221 | 222 | - WORKAROUND: RUBY_PLATFORM is hardcoded on Ruby compile and can not be 223 | trusted for feature detection, use a different technique when checking for 224 | macOS Mojave 225 | 226 | - 0.2.1 - 25-09-2018 227 | 228 | - FEATURE: Mojave macOS support 229 | 230 | - 0.2.0 - 06-07-2018 231 | 232 | - FEATURE: context#call to allow for cheaper invocation of functions 233 | - FIX: rare memory leak when terminating a long running attached function 234 | - FIX: rare segfault when terminating a long running attached function 235 | - FIX: Reimplement Isolate#idle_notification using idle_notification_deadline, API remains the same @ignisf 236 | - Account for changes in the upstream V8 API @ignisf 237 | - Support for libv8 6.7 238 | 239 | - 0.1.15 - 23-08-2017 240 | 241 | - bump dependency of libv8 to 6.3 242 | 243 | - 0.1.14 - 23-08-2017 244 | 245 | - libv8 erroneously bumped to beta, reverted change 246 | 247 | - 0.1.13 - 23-08-2017 248 | 249 | - Fix: amend array buffer allocator to use v8 6.0 compatible allocator @ignisf 250 | 251 | - 0.1.12 - 18-07-2017 252 | 253 | - Feature: upgrade libv8 to 5.9 254 | - Fix: warning when running with ruby warnings enabled (missed @disposed initialize) 255 | 256 | - 0.1.11 - 18-07-2017 257 | 258 | - Feature: upgrade libv8 to 5.7 259 | 260 | 261 | - 0.1.10 - 13-07-2017 262 | 263 | - Fix leak: memory leak when disposing a context (20 bytes per context) 264 | - Feature: added #heap_stats so you can get visibility from context to actual memory usage of isolate 265 | - Feature: added #dispose so you reclaim all v8 memory right away as opposed to waiting for GC 266 | - Feature: you can now specify filename in an eval eg: eval('a = 1', filename: 'my_awesome.js') 267 | 268 | 269 | - 0.1.9 - 09-03-2017 270 | 271 | - Perf: speed up ruby/node boundary performance when moving large objects 272 | 273 | - 0.1.8 - 06-02-2017 274 | 275 | - Fix: Include math.h to fix use of undeclared identifier floor with rbx. See #51 276 | 277 | - 0.1.7 - 02-11-2016 278 | 279 | - Fix: if for some reason an isolate was forked don't free it and raise a warning instead to avoid hanging process 280 | 281 | - 0.1.6 - 25-10-2016 282 | 283 | - Fix: timeout behavior was incorrect, in some cases stop could be called on already stopped contexts 284 | 285 | - 0.1.5 - 10-10-2016 286 | 287 | - Support for snapshots, shared isolates, runtime flags thanks to @wk8 288 | - Fix timeout behavior when it occurs in an attached Ruby method 289 | 290 | - 0.1.4 - 19-05-2016 291 | 292 | - Set upper bound for libv8 inclusion @ignisf 293 | - Support conversion of Date, Time and DateTime from Ruby to JS @seanmakesgames 294 | - Support conversion of large numbers back from Ruby to JS @seanmakesgames 295 | 296 | - 0.1.3 - 17-05-2016 297 | 298 | - Support more conversions from Ruby back to JS (Hash, Symbol, Array) 299 | - Support attaching nested objects 300 | 301 | 302 | - 0.1.2 - 17-05-2016 303 | 304 | - Gemspec specifies minimal version of Ruby (2.0) 305 | - Implement #load on Context to load files 306 | 307 | 308 | - 0.1.1 309 | 310 | - Added unblock function so SIGINT does not lead to a crash 311 | 312 | 313 | - 0.1.1.beta.1 - 14-05-2016 314 | 315 | - First release 316 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at sam.saffron@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | rationale 2 | ========= 3 | 4 | Before the commit that added this document, Ruby and V8 shared the same system 5 | stack but it's been observed that they don't always co-exist peacefully there. 6 | 7 | Symptoms range from unexpected JS stack overflow exceptions and hitting debug 8 | checks in V8, to outright segmentation faults. 9 | 10 | To mitigate that, V8 runs on separate threads now. 11 | 12 | implementation 13 | ============== 14 | 15 | Each `MiniRacer::Context` is paired with a native system thread that runs V8. 16 | 17 | Multiple Ruby threads can concurrently access the `MiniRacer::Context`. 18 | MiniRacer ensures mutual exclusion. Ruby threads won't trample each other. 19 | 20 | Ruby threads communicate with the V8 thread through a mutex-and-condition-variable 21 | protected request/response memory buffer. 22 | 23 | The wire format is V8's native (de)serialization format. An encoder/decoder 24 | has been added to MiniRacer. 25 | 26 | Requests and (some) responses are prefixed with a single character 27 | that indicates the desired action: `'C'` is `context.call(...)`, 28 | `'E'` is `context.eval(...)`, and so on. 29 | 30 | A response from the V8 thread either starts with: 31 | 32 | - `'\xff'`, indicating a normal response that should be deserialized as-is 33 | 34 | - `'c'`, signaling an in-band request (not a response!) to call a Ruby function 35 | registered with `context.attach(...)`. In turn, the Ruby thread replies with 36 | a `'c'` response containing the return value from the Ruby function. 37 | 38 | Special care has been taken to ensure Ruby and JS functions can call each other 39 | recursively without deadlocking. The Ruby thread uses a recursive mutex that 40 | excludes other Ruby threads but still allows reentrancy from the same thread. 41 | 42 | The exact request and response payloads are documented in the source code but 43 | they are almost universally: 44 | 45 | - either a single value (e.g. `true` or `false`), or 46 | 47 | - a two or three element array (ex. `[filename, source]` for `context.eval(...)`), or 48 | 49 | - for responses, an errback-style `[response, error]` array, where `error` 50 | is a multi-line string that contains the error message on the first line, 51 | and, optionally, the stack trace. If not empty, the error string is turned 52 | into a Ruby exception and raised. 53 | 54 | deliberate changes & known bugs 55 | =============================== 56 | 57 | - `MiniRacer::Platform.set_flags! :single_threaded` still runs everything on 58 | the same thread but is prone to crashes in Ruby < 3.4.0 due to a Ruby runtime 59 | bug that clobbers thread-local variables. 60 | 61 | - The `Isolate` class is gone. Maintaining a one-to-many relationship between 62 | isolates and contexts in a multi-threaded environment had a bad cost/benefit 63 | ratio. `Isolate` methods like `isolate.low_memory_notification` have been 64 | moved to `Context`, ex., `context.low_memory_notification`. 65 | 66 | - The `marshal_stack_depth` argument is still accepted but ignored; it's no 67 | longer necessary. 68 | 69 | - The `ensure_gc_after_idle` argument is a no-op in `:single_threaded` mode. 70 | 71 | - The `timeout` argument no longer interrupts long-running Ruby code. Killing 72 | or interrupting a Ruby thread executing arbitrary code is fraught with peril. 73 | 74 | - Returning an invalid JS `Date` object (think `new Date(NaN)`) now raises a 75 | `RangeError` instead of silently returning a bogus `Time` object. 76 | 77 | - Not all JS objects map 1-to-1 to Ruby objects. Typed arrays and arraybuffers 78 | are currently mapped to `Encoding::ASCII_8BIT`strings as the closest Ruby 79 | equivalent to a byte buffer. 80 | 81 | - Not all JS objects are serializable/cloneable. Where possible, such objects 82 | are substituted with a cloneable representation, else a `MiniRacer::RuntimeError` 83 | is raised. 84 | 85 | Promises, argument objects, map and set iterators, etc., are substituted, 86 | either with an empty object (promises, argument objects), or by turning them 87 | into arrays (map/set iterators.) 88 | 89 | Function objects are substituted with a marker so they can be represented 90 | as `MiniRacer::JavaScriptFunction` objects on the Ruby side. 91 | 92 | SharedArrayBuffers are not cloneable by design but aren't really usable in 93 | `mini_racer` in the first place (no way to share them between isolates.) 94 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION=3.2 2 | FROM ruby:${RUBY_VERSION} 3 | 4 | RUN test ! -f /etc/alpine-release || apk add --no-cache build-base git 5 | 6 | COPY Gemfile mini_racer.gemspec /code/ 7 | COPY lib/mini_racer/version.rb /code/lib/mini_racer/version.rb 8 | WORKDIR /code 9 | RUN bundle install 10 | 11 | COPY Rakefile /code/ 12 | COPY ext /code/ext/ 13 | RUN bundle exec rake compile 14 | 15 | COPY . /code/ 16 | CMD bundle exec irb -rmini_racer 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in mini_racer.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2019, the mini_racer project authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniRacer 2 | 3 | [![Test](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml/badge.svg)](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml) ![Gem](https://img.shields.io/gem/v/mini_racer) 4 | 5 | Minimal, modern embedded V8 for Ruby. 6 | 7 | MiniRacer provides a minimal two way bridge between the V8 JavaScript engine and Ruby. 8 | 9 | It was created as an alternative to the excellent [therubyracer](https://github.com/cowboyd/therubyracer), which is [no longer maintained](https://github.com/rubyjs/therubyracer/issues/462). Unlike therubyracer, mini_racer only implements a minimal bridge. This reduces the surface area making upgrading v8 much simpler and exhaustive testing simpler. 10 | 11 | MiniRacer has an adapter for [execjs](https://github.com/rails/execjs) so it can be used directly with Rails projects to minify assets, run babel or compile CoffeeScript. 12 | 13 | ## Supported Ruby Versions & Troubleshooting 14 | 15 | MiniRacer only supports non-EOL versions of Ruby. See [Ruby Maintenance Branches](https://www.ruby-lang.org/en/downloads/branches/) for the list of non-EOL Rubies. If you require support for older versions of Ruby install an older version of the gem. [TruffleRuby](https://github.com/oracle/truffleruby) is also supported. 16 | 17 | MiniRacer **does not support** 18 | 19 | * [Ruby built on MinGW](https://github.com/rubyjs/mini_racer/issues/252#issuecomment-1201172236), "pure windows" no Cygwin, no WSL2 (see https://github.com/rubyjs/libv8-node/issues/9) 20 | * [JRuby](https://www.jruby.org) 21 | 22 | If you have a problem installing MiniRacer, please consider the following steps: 23 | 24 | * make sure you try the latest released version of `mini_racer` 25 | * make sure you have Rubygems >= 3.2.13 and bundler >= 2.2.13 installed: `gem update --system` 26 | * if you are using bundler 27 | * make sure it is actually using the latest bundler version: [`bundle update --bundler`](https://bundler.io/v2.4/man/bundle-update.1.html) 28 | * make sure to have `PLATFORMS` set correctly in `Gemfile.lock` via [`bundle lock --add-platform`](https://bundler.io/v2.4/man/bundle-lock.1.html#SUPPORTING-OTHER-PLATFORMS) 29 | * make sure to recompile/reinstall `mini_racer` and `libv8-node` after OS upgrades (for example via `gem uninstall --all mini_racer libv8-node`) 30 | * make sure you are on the latest patch/teeny version of a supported Ruby branch 31 | 32 | ## Features 33 | 34 | ### Simple eval for JavaScript 35 | 36 | You can simply eval one or many JavaScript snippets in a shared context 37 | 38 | ```ruby 39 | context = MiniRacer::Context.new 40 | context.eval("var adder = (a,b)=>a+b;") 41 | puts context.eval("adder(20,22)") 42 | # => 42 43 | ``` 44 | 45 | ### Attach global Ruby functions to your JavaScript context 46 | 47 | You can attach one or many ruby proc that can be accessed via JavaScript 48 | 49 | ```ruby 50 | context = MiniRacer::Context.new 51 | context.attach("math.adder", proc{|a,b| a+b}) 52 | puts context.eval("math.adder(20,22)") 53 | # => 42 54 | ``` 55 | 56 | ```ruby 57 | context = MiniRacer::Context.new 58 | context.attach("array_and_hash", proc{{a: 1, b: [1, {a: 1}]}}) 59 | puts context.eval("array_and_hash()") 60 | # => {"a" => 1, "b" => [1, {"a" => 1}]} 61 | ``` 62 | 63 | ### GIL free JavaScript execution 64 | 65 | The Ruby Global interpreter lock is released when scripts are executing: 66 | 67 | ```ruby 68 | context = MiniRacer::Context.new 69 | Thread.new do 70 | sleep 1 71 | context.stop 72 | end 73 | context.eval("while(true){}") 74 | # => exception is raised 75 | ``` 76 | 77 | This allows you to execute multiple scripts in parallel. 78 | 79 | ### Timeout Support 80 | 81 | Contexts can specify a default timeout for scripts 82 | 83 | ```ruby 84 | context = MiniRacer::Context.new(timeout: 1000) 85 | context.eval("while(true){}") 86 | # => exception is raised after 1 second (1000 ms) 87 | ``` 88 | 89 | ### Memory softlimit Support 90 | 91 | Contexts can specify a memory softlimit for scripts 92 | 93 | ```ruby 94 | # terminates script if heap usage exceeds 200mb after V8 garbage collection has run 95 | context = MiniRacer::Context.new(max_memory: 200_000_000) 96 | context.eval("var a = new Array(10000); while(true) {a = a.concat(new Array(10000)) }") 97 | # => V8OutOfMemoryError is raised 98 | ``` 99 | 100 | ### Rich Debugging with File Name in Stack Trace Support 101 | 102 | You can provide `filename:` to `#eval` which will be used in stack traces produced by V8: 103 | 104 | ```ruby 105 | context = MiniRacer::Context.new 106 | context.eval("var foo = function() {bar();}", filename: "a/foo.js") 107 | context.eval("bar()", filename: "a/bar.js") 108 | 109 | # JavaScript at a/bar.js:1:1: ReferenceError: bar is not defined (MiniRacer::RuntimeError) 110 | # … 111 | ``` 112 | 113 | ### Fork Safety 114 | 115 | Some Ruby web servers employ forking (for example unicorn or puma in clustered mode). V8 is not fork safe by default and sadly Ruby does not have support for fork notifications per [#5446](https://bugs.ruby-lang.org/issues/5446). 116 | 117 | Since 0.6.1 mini_racer does support V8 single threaded platform mode which should remove most forking related issues. To enable run this before using `MiniRacer::Context`, for example in a Rails initializer: 118 | 119 | ```ruby 120 | MiniRacer::Platform.set_flags!(:single_threaded) 121 | ``` 122 | 123 | If you want to ensure your application does not leak memory after fork either: 124 | 125 | 1. Ensure no `MiniRacer::Context` objects are created in the master process; or 126 | 2. Dispose manually of all `MiniRacer::Context` objects prior to forking 127 | 128 | ```ruby 129 | # before fork 130 | 131 | require "objspace" 132 | ObjectSpace.each_object(MiniRacer::Context){|c| c.dispose} 133 | 134 | # fork here 135 | ``` 136 | 137 | ### Threadsafe 138 | 139 | Context usage is threadsafe 140 | 141 | ```ruby 142 | context = MiniRacer::Context.new 143 | context.eval("counter=0; plus=()=>counter++;") 144 | 145 | (1..10).map do 146 | Thread.new { 147 | context.eval("plus()") 148 | } 149 | end.each(&:join) 150 | 151 | puts context.eval("counter") 152 | # => 10 153 | ``` 154 | 155 | ### Snapshots 156 | 157 | Contexts can be created with pre-loaded snapshots: 158 | 159 | ```ruby 160 | snapshot = MiniRacer::Snapshot.new("function hello() { return 'world!'; }") 161 | 162 | context = MiniRacer::Context.new(snapshot: snapshot) 163 | 164 | context.eval("hello()") 165 | # => "world!" 166 | ``` 167 | 168 | Snapshots can come in handy for example if you want your contexts to be pre-loaded for efficiency. It uses [V8 snapshots](http://v8project.blogspot.com/2015/09/custom-startup-snapshots.html) under the hood; see [this link](http://v8project.blogspot.com/2015/09/custom-startup-snapshots.html) for caveats using these, in particular: 169 | 170 | > There is an important limitation to snapshots: they can only capture V8’s 171 | > heap. Any interaction from V8 with the outside is off-limits when creating the 172 | > snapshot. Such interactions include: 173 | > 174 | > * defining and calling API callbacks (i.e. functions created via v8::FunctionTemplate) 175 | > * creating typed arrays, since the backing store may be allocated outside of V8 176 | > 177 | > And of course, values derived from sources such as `Math.random` or `Date.now` 178 | > are fixed once the snapshot has been captured. They are no longer really random 179 | > nor reflect the current time. 180 | 181 | Also note that snapshots can be warmed up, using the `warmup!` method, which allows you to call functions which are otherwise lazily compiled to get them to compile right away; any side effect of your warm up code being then dismissed. [More details on warming up here](https://github.com/electron/electron/issues/169#issuecomment-76783481), and a small example: 182 | 183 | ```ruby 184 | snapshot = MiniRacer::Snapshot.new("var counter = 0; function hello() { counter++; return 'world! '; }") 185 | 186 | snapshot.warmup!("hello()") 187 | 188 | context = MiniRacer::Context.new(snapshot: snapshot) 189 | 190 | context.eval("hello()") 191 | # => "world! 1" 192 | context.eval("counter") 193 | # => 1 194 | ``` 195 | 196 | ### Garbage collection 197 | 198 | You can make the garbage collector more aggressive by defining the context with `MiniRacer::Context.new(ensure_gc_after_idle: 1000)`. Using this will ensure V8 will run a full GC using `context.low_memory_notification` 1 second after the last eval on the context. Low memory notifications ensure long living contexts use minimal amounts of memory. 199 | 200 | ### V8 Runtime flags 201 | 202 | It is possible to set V8 Runtime flags: 203 | 204 | ```ruby 205 | MiniRacer::Platform.set_flags! :noconcurrent_recompilation, max_inlining_levels: 10 206 | ``` 207 | 208 | This can come in handy if you want to use MiniRacer with Unicorn, which doesn't seem to always appreciate V8's liberal use of threading: 209 | 210 | ```ruby 211 | MiniRacer::Platform.set_flags! :noconcurrent_recompilation, :noconcurrent_sweeping 212 | ``` 213 | 214 | Or else to unlock experimental features in V8, for example tail recursion optimization: 215 | 216 | ```ruby 217 | MiniRacer::Platform.set_flags! :harmony 218 | 219 | js = <<-JS 220 | 'use strict'; 221 | var f = function f(n){ 222 | if (n <= 0) { 223 | return 'foo'; 224 | } 225 | return f(n - 1); 226 | } 227 | 228 | f(1e6); 229 | JS 230 | 231 | context = MiniRacer::Context.new 232 | 233 | context.eval js 234 | # => "foo" 235 | ``` 236 | 237 | The same code without the harmony runtime flag results in a `MiniRacer::RuntimeError: RangeError: Maximum call stack size exceeded` exception. 238 | Please refer to http://node.green/ as a reference on other harmony features. 239 | 240 | A list of all V8 runtime flags can be found using `node --v8-options`, or else by perusing [the V8 source code for flags (make sure to use the right version of V8)](https://github.com/v8/v8/blob/master/src/flags/flag-definitions.h). 241 | 242 | Note that runtime flags must be set before any other operation (e.g. creating a context or a snapshot), otherwise an exception will be thrown. 243 | 244 | Flags: 245 | 246 | * `:expose_gc`: Will expose `gc()` which you can run in JavaScript to issue a GC run. 247 | * `:max_old_space_size`: defaults to 1400 (megs) on 64 bit, you can restrict memory usage by limiting this. 248 | 249 | **NOTE TO READER** our documentation could be awesome we could be properly documenting all the flags, they are hugely useful, if you feel like documenting a few more, PLEASE DO, PRs are welcome. 250 | 251 | ## Controlling memory 252 | 253 | When hosting v8 you may want to keep track of memory usage, use `#heap_stats` to get memory usage: 254 | 255 | ```ruby 256 | context = MiniRacer::Context.new 257 | # use context 258 | p context.heap_stats 259 | # {:total_physical_size=>1280640, 260 | # :total_heap_size_executable=>4194304, 261 | # :total_heap_size=>3100672, 262 | # :used_heap_size=>1205376, 263 | # :heap_size_limit=>1501560832} 264 | ``` 265 | 266 | If you wish to dispose of a context before waiting on the GC use `#dispose`: 267 | 268 | ```ruby 269 | context = MiniRacer::Context.new 270 | context.eval("let a='testing';") 271 | context.dispose 272 | context.eval("a = 2") 273 | # MiniRacer::ContextDisposedError 274 | 275 | # nothing works on the context from now on, it's a shell waiting to be disposed 276 | ``` 277 | 278 | A MiniRacer context can also be dumped in a heapsnapshot file using `#write_heap_snapshot(file_or_io)` 279 | 280 | ```ruby 281 | context = MiniRacer::Context.new 282 | # use context 283 | context.write_heap_snapshot("test.heapsnapshot") 284 | ``` 285 | 286 | This file can then be loaded in the "memory" tab of the [Chrome DevTools](https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/#view_snapshots). 287 | 288 | ### Function call 289 | 290 | This calls the function passed as first argument: 291 | 292 | ```ruby 293 | context = MiniRacer::Context.new 294 | context.eval("function hello(name) { return `Hello, ${name}!` }") 295 | context.call("hello", "George") 296 | # "Hello, George!" 297 | ``` 298 | 299 | Performance is slightly better than running `context.eval("hello('George')")` since: 300 | 301 | * compilation of eval'd string is avoided 302 | * function arguments don't need to be converted to JSON 303 | 304 | ## Performance 305 | 306 | The `bench` folder contains benchmark. 307 | 308 | ### Benchmark minification of Discourse application.js (both minified and non-minified) 309 | 310 | MiniRacer outperforms node when minifying assets via execjs. 311 | 312 | * MiniRacer version 0.1.9 313 | * node version 6.10 314 | * therubyracer version 0.12.2 315 | 316 | ```terminal 317 | $ bundle exec ruby bench.rb mini_racer 318 | Benching with mini_racer 319 | mini_racer minify discourse_app.js 9292.72063ms 320 | mini_racer minify discourse_app_minified.js 11799.850171ms 321 | mini_racer minify discourse_app.js twice (2 threads) 10269.570797ms 322 | 323 | sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb node 324 | Benching with node 325 | node minify discourse_app.js 13302.715484ms 326 | node minify discourse_app_minified.js 18100.761243ms 327 | node minify discourse_app.js twice (2 threads) 14383.600207000001ms 328 | 329 | sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb therubyracer 330 | Benching with therubyracer 331 | therubyracer minify discourse_app.js 171683.01867700001ms 332 | therubyracer minify discourse_app_minified.js 143138.88492ms 333 | therubyracer minify discourse_app.js twice (2 threads) NEVER FINISH 334 | 335 | Killed: 9 336 | ``` 337 | 338 | The huge performance disparity (MiniRacer is 10x faster) is due to MiniRacer running latest version of V8. In July 2016 there is a queued upgrade to therubyracer which should bring some of the perf inline. 339 | 340 | Note how the global interpreter lock release leads to 2 threads doing the same work taking the same wall time as 1 thread. 341 | 342 | As a rule MiniRacer strives to always support and depend on the latest stable version of libv8. 343 | 344 | ## Source Maps 345 | 346 | MiniRacer can fully support source maps but must be configured correctly to do so. [Check out this example](./examples/source-map-support/) for a working implementation. 347 | 348 | ## Installation 349 | 350 | Add this line to your application's Gemfile: 351 | 352 | ```ruby 353 | gem "mini_racer" 354 | ``` 355 | 356 | And then execute: 357 | 358 | ```terminal 359 | $ bundle 360 | 361 | Or install it yourself as: 362 | 363 | ```terminal 364 | $ gem install mini_racer 365 | ``` 366 | 367 | **Note** using v8.h and compiling MiniRacer requires a C++20 capable compiler. 368 | gcc >= 12.2 and Xcode >= 13 are, at the time of writing, known to work. 369 | 370 | ## Similar Projects 371 | 372 | ### therubyracer 373 | 374 | * https://github.com/cowboyd/therubyracer 375 | * Most comprehensive bridge available 376 | * Provides the ability to "eval" JavaScript 377 | * Provides the ability to invoke Ruby code from JavaScript 378 | * Hold references to JavaScript objects and methods in your Ruby code 379 | * Hold references to Ruby objects and methods in JavaScript code 380 | * Uses libv8, so installation is fast 381 | * Supports timeouts for JavaScript execution 382 | * Does not release global interpreter lock, so performance is constrained to a single thread 383 | * Currently (May 2016) only supports v8 version 3.16.14 (Released approx November 2013), plans to upgrade by July 2016 384 | * Supports execjs 385 | 386 | ### v8eval 387 | 388 | * https://github.com/sony/v8eval 389 | * Provides the ability to "eval" JavaScript using the latest V8 engine 390 | * Does not depend on the [libv8](https://github.com/cowboyd/libv8) gem, installation can take 10-20 mins as V8 needs to be downloaded and compiled. 391 | * Does not release global interpreter lock when executing JavaScript 392 | * Does not allow you to invoke Ruby code from JavaScript 393 | * Multi runtime support due to SWIG based bindings 394 | * Supports a JavaScript debugger 395 | * Does not support timeouts for JavaScript execution 396 | * No support for execjs (can not be used with Rails uglifier and coffeescript gems) 397 | 398 | ### therubyrhino 399 | 400 | * https://github.com/cowboyd/therubyrhino 401 | * API compatible with therubyracer 402 | * Uses Mozilla's Rhino engine https://github.com/mozilla/rhino 403 | * Requires JRuby 404 | * Support for timeouts for JavaScript execution 405 | * Concurrent cause .... JRuby 406 | * Supports execjs 407 | 408 | ## Contributing 409 | 410 | Bug reports and pull requests are welcome on GitHub at https://github.com/rubyjs/mini_racer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 411 | 412 | ## License 413 | 414 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 415 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "minitest/test_task" 3 | 4 | CLEAN.add("{ext,lib}/**/*.{o,so,bundle}", "pkg") 5 | CLOBBER.add("Gemfile.lock") 6 | 7 | Minitest::TestTask.create(:test) do |t| 8 | t.libs << "test" 9 | t.libs << "lib" 10 | t.test_globs = FileList['test/**/*_test.rb'] 11 | end 12 | 13 | task :default => [:compile, :test] 14 | 15 | gem = Gem::Specification.load( File.dirname(__FILE__) + '/mini_racer.gemspec' ) 16 | 17 | if RUBY_ENGINE == "truffleruby" 18 | task :compile do 19 | # noop 20 | end 21 | 22 | task :clean do 23 | # noop 24 | end 25 | else 26 | require 'rake/extensiontask' 27 | Rake::ExtensionTask.new( 'mini_racer_loader', gem ) 28 | Rake::ExtensionTask.new( 'mini_racer_extension', gem ) 29 | end 30 | 31 | 32 | # via http://blog.flavorjon.es/2009/06/easily-valgrind-gdb-your-ruby-c.html 33 | namespace :test do 34 | desc "run test suite with Address Sanitizer" 35 | task :asan do 36 | ENV["CONFIGURE_ARGS"] = [ENV["CONFIGURE_ARGS"], '--enable-asan'].compact.join(' ') 37 | Rake::Task['compile'].invoke 38 | 39 | asan_path = `ldconfig -N -p |grep libasan | grep -v 32 | sed 's/.* => \\(.*\\)$/\\1/'`.chomp.split("\n")[-1] 40 | 41 | 42 | cmdline = "env LD_PRELOAD=\"#{asan_path}\" ruby test/test_leak.rb" 43 | puts cmdline 44 | system cmdline 45 | 46 | cmdline = "env LD_PRELOAD=\"#{asan_path}\" rake test" 47 | puts cmdline 48 | system cmdline 49 | end 50 | # partial-loads-ok and undef-value-errors necessary to ignore 51 | # spurious (and eminently ignorable) warnings from the ruby 52 | # interpreter 53 | VALGRIND_BASIC_OPTS = "--num-callers=50 --error-limit=no \ 54 | --partial-loads-ok=yes --undef-value-errors=no" 55 | 56 | desc "run test suite under valgrind with basic ruby options" 57 | task :valgrind => :compile do 58 | cmdline = "valgrind #{VALGRIND_BASIC_OPTS} ruby test/test_leak.rb" 59 | puts cmdline 60 | system cmdline 61 | end 62 | 63 | desc "run test suite under valgrind with leak-check=full" 64 | task :valgrind_leak_check => :compile do 65 | cmdline = "valgrind #{VALGRIND_BASIC_OPTS} --leak-check=full ruby test/test_leak.rb" 66 | puts cmdline 67 | require 'open3' 68 | _, stderr = Open3.capture3(cmdline) 69 | 70 | section = "" 71 | stderr.split("\n").each do |line| 72 | 73 | if line =~ /==.*==\s*$/ 74 | if (section =~ /mini_racer|SUMMARY/) 75 | puts 76 | puts section 77 | puts 78 | end 79 | section = "" 80 | else 81 | section << line << "\n" 82 | end 83 | end 84 | end 85 | end 86 | 87 | desc 'run clang-tidy linter on mini_racer_extension.cc' 88 | task :lint do 89 | require 'mkmf' 90 | require 'libv8' 91 | 92 | Libv8.configure_makefile 93 | 94 | conf = RbConfig::CONFIG.merge('hdrdir' => $hdrdir.quote, 'srcdir' => $srcdir.quote, 95 | 'arch_hdrdir' => $arch_hdrdir.quote, 96 | 'top_srcdir' => $top_srcdir.quote) 97 | if $universal and (arch_flag = conf['ARCH_FLAG']) and !arch_flag.empty? 98 | conf['ARCH_FLAG'] = arch_flag.gsub(/(?:\G|\s)-arch\s+\S+/, '') 99 | end 100 | 101 | checks = %W(bugprone-* 102 | cert-* 103 | cppcoreguidelines-* 104 | clang-analyzer-* 105 | performance-* 106 | portability-* 107 | readability-*).join(',') 108 | 109 | sh RbConfig::expand("clang-tidy -checks='#{checks}' ext/mini_racer_extension/mini_racer_extension.cc -- #$INCFLAGS #$CXXFLAGS", conf) 110 | end 111 | -------------------------------------------------------------------------------- /benchmark/babel/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | if ENV['RR'] 4 | gem 'therubyracer' 5 | else 6 | gem 'mini_racer', path: '../../' 7 | end 8 | -------------------------------------------------------------------------------- /benchmark/babel/bench.rb: -------------------------------------------------------------------------------- 1 | unless defined? Bundler 2 | system 'bundle' 3 | exec 'bundle exec ruby bench.rb' 4 | end 5 | 6 | require 'mini_racer' 7 | 8 | 9 | ctx2 = MiniRacer::Context.new 10 | 11 | start = Time.now 12 | ctx2.eval(File.read('./helper_files/babel.js')) 13 | puts "#{(Time.now - start) * 1000} load babel" 14 | 15 | composer = File.read('./helper_files/composer.js.es6').inspect 16 | str = "babel.transform(#{composer}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring', 'es6.spread', 'es6.parameters', 'es6.templateLiterals', 'es6.regex.unicode', 'es7.decorators', 'es6.classes']})['code']" 17 | 18 | 19 | 10.times do 20 | 21 | start = Time.now 22 | ctx2.eval str 23 | 24 | puts "mini racer #{(Time.now - start) * 1000} transform" 25 | 26 | end 27 | 28 | str = "for(var i=0;i<10;i++){#{str}}" 29 | 30 | 10.times do 31 | 32 | start = Time.now 33 | ctx2.eval str 34 | 35 | puts "mini racer 10x #{(Time.now - start) * 1000} transform" 36 | 37 | end 38 | 39 | -------------------------------------------------------------------------------- /benchmark/babel/bench_node.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var start = new Date(); 4 | var babel = require ('./helper_files/babel.js'); 5 | console.log("Running V8 version " + process.versions.v8); 6 | console.log(new Date() - start + " requiring babel"); 7 | 8 | for (var i=0; i < 10; i++) { 9 | start = new Date(); 10 | 11 | babel.transform(fs.readFileSync('./helper_files/composer.js.es6', 'utf8'), {ast: false, 12 | whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 13 | 'es6.blockScoping', 'es6.destructuring', 'es6.spread', 'es6.parameters', 14 | 'es6.templateLiterals', 'es6.regex.unicode', 'es7.decorators', 'es6.classes']})['code'] 15 | 16 | console.log(new Date() - start + " transpile") 17 | } 18 | 19 | 20 | 21 | for (var i=0; i < 10; i++) { 22 | start = new Date(); 23 | 24 | for (var j=0; j < 10; j++) { 25 | babel.transform(fs.readFileSync('./helper_files/composer.js.es6', 'utf8'), {ast: false, 26 | whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 27 | 'es6.blockScoping', 'es6.destructuring', 'es6.spread', 'es6.parameters', 28 | 'es6.templateLiterals', 'es6.regex.unicode', 'es7.decorators', 'es6.classes']})['code'] 29 | } 30 | 31 | console.log(new Date() - start + " transpile 10 times") 32 | } 33 | -------------------------------------------------------------------------------- /benchmark/babel/helper_files/composer.js.es6: -------------------------------------------------------------------------------- 1 | import RestModel from 'discourse/models/rest'; 2 | import Topic from 'discourse/models/topic'; 3 | import { throwAjaxError } from 'discourse/lib/ajax-error'; 4 | import Quote from 'discourse/lib/quote'; 5 | import Draft from 'discourse/models/draft'; 6 | import computed from 'ember-addons/ember-computed-decorators'; 7 | 8 | const CLOSED = 'closed', 9 | SAVING = 'saving', 10 | OPEN = 'open', 11 | DRAFT = 'draft', 12 | 13 | // The actions the composer can take 14 | CREATE_TOPIC = 'createTopic', 15 | PRIVATE_MESSAGE = 'privateMessage', 16 | REPLY = 'reply', 17 | EDIT = 'edit', 18 | REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic", 19 | 20 | // When creating, these fields are moved into the post model from the composer model 21 | _create_serializer = { 22 | raw: 'reply', 23 | title: 'title', 24 | category: 'categoryId', 25 | topic_id: 'topic.id', 26 | is_warning: 'isWarning', 27 | whisper: 'whisper', 28 | archetype: 'archetypeId', 29 | target_usernames: 'targetUsernames', 30 | typing_duration_msecs: 'typingTime', 31 | composer_open_duration_msecs: 'composerTime', 32 | tags: 'tags' 33 | }, 34 | 35 | _edit_topic_serializer = { 36 | title: 'topic.title', 37 | categoryId: 'topic.category.id', 38 | tags: 'topic.tags' 39 | }; 40 | 41 | const Composer = RestModel.extend({ 42 | _categoryId: null, 43 | 44 | archetypes: function() { 45 | return this.site.get('archetypes'); 46 | }.property(), 47 | 48 | 49 | @computed 50 | categoryId: { 51 | get() { return this._categoryId; }, 52 | 53 | // We wrap categoryId this way so we can fire `applyTopicTemplate` with 54 | // the previous value as well as the new value 55 | set(categoryId) { 56 | const oldCategoryId = this._categoryId; 57 | 58 | if (Ember.isEmpty(categoryId)) { categoryId = null; } 59 | this._categoryId = categoryId; 60 | 61 | if (oldCategoryId !== categoryId) { 62 | this.applyTopicTemplate(oldCategoryId, categoryId); 63 | } 64 | return categoryId; 65 | } 66 | }, 67 | 68 | creatingTopic: Em.computed.equal('action', CREATE_TOPIC), 69 | creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE), 70 | notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'), 71 | 72 | @computed("privateMessage", "archetype.hasOptions") 73 | showCategoryChooser(isPrivateMessage, hasOptions) { 74 | const manyCategories = Discourse.Category.list().length > 1; 75 | return !isPrivateMessage && (hasOptions || manyCategories); 76 | }, 77 | 78 | @computed("creatingPrivateMessage", "topic") 79 | privateMessage(creatingPrivateMessage, topic) { 80 | return creatingPrivateMessage || (topic && topic.get('archetype') === 'private_message'); 81 | }, 82 | 83 | topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'), 84 | 85 | editingPost: Em.computed.equal('action', EDIT), 86 | replyingToTopic: Em.computed.equal('action', REPLY), 87 | 88 | viewOpen: Em.computed.equal('composeState', OPEN), 89 | viewDraft: Em.computed.equal('composeState', DRAFT), 90 | 91 | 92 | composeStateChanged: function() { 93 | var oldOpen = this.get('composerOpened'); 94 | 95 | if (this.get('composeState') === OPEN) { 96 | this.set('composerOpened', oldOpen || new Date()); 97 | } else { 98 | if (oldOpen) { 99 | var oldTotal = this.get('composerTotalOpened') || 0; 100 | this.set('composerTotalOpened', oldTotal + (new Date() - oldOpen)); 101 | } 102 | this.set('composerOpened', null); 103 | } 104 | }.observes('composeState'), 105 | 106 | composerTime: function() { 107 | var total = this.get('composerTotalOpened') || 0; 108 | 109 | var oldOpen = this.get('composerOpened'); 110 | if (oldOpen) { 111 | total += (new Date() - oldOpen); 112 | } 113 | 114 | return total; 115 | }.property().volatile(), 116 | 117 | archetype: function() { 118 | return this.get('archetypes').findProperty('id', this.get('archetypeId')); 119 | }.property('archetypeId'), 120 | 121 | archetypeChanged: function() { 122 | return this.set('metaData', Em.Object.create()); 123 | }.observes('archetype'), 124 | 125 | // view detected user is typing 126 | typing: _.throttle(function(){ 127 | var typingTime = this.get("typingTime") || 0; 128 | this.set("typingTime", typingTime + 100); 129 | }, 100, {leading: false, trailing: true}), 130 | 131 | editingFirstPost: Em.computed.and('editingPost', 'post.firstPost'), 132 | canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'), 133 | canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'), 134 | 135 | // Determine the appropriate title for this action 136 | actionTitle: function() { 137 | const topic = this.get('topic'); 138 | 139 | let postLink, topicLink, usernameLink; 140 | if (topic) { 141 | const postNumber = this.get('post.post_number'); 142 | postLink = "" + 143 | I18n.t("post.post_number", { number: postNumber }) + ""; 144 | topicLink = " " + Discourse.Utilities.escapeExpression(topic.get('title')) + ""; 145 | usernameLink = "" + this.get('post.username') + ""; 146 | } 147 | 148 | let postDescription; 149 | const post = this.get('post'); 150 | 151 | if (post) { 152 | postDescription = I18n.t('post.' + this.get('action'), { 153 | link: postLink, 154 | replyAvatar: Discourse.Utilities.tinyAvatar(post.get('avatar_template')), 155 | username: this.get('post.username'), 156 | usernameLink 157 | }); 158 | 159 | if (!this.site.mobileView) { 160 | const replyUsername = post.get('reply_to_user.username'); 161 | const replyAvatarTemplate = post.get('reply_to_user.avatar_template'); 162 | if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) { 163 | postDescription += " " + Discourse.Utilities.tinyAvatar(replyAvatarTemplate) + " " + replyUsername; 164 | } 165 | } 166 | } 167 | 168 | switch (this.get('action')) { 169 | case PRIVATE_MESSAGE: return I18n.t('topic.private_message'); 170 | case CREATE_TOPIC: return I18n.t('topic.create_long'); 171 | case REPLY: 172 | case EDIT: 173 | if (postDescription) return postDescription; 174 | if (topic) return I18n.t('post.reply_topic', { link: topicLink }); 175 | } 176 | 177 | }.property('action', 'post', 'topic', 'topic.title'), 178 | 179 | 180 | // whether to disable the post button 181 | cantSubmitPost: function() { 182 | 183 | // can't submit while loading 184 | if (this.get('loading')) return true; 185 | 186 | // title is required when 187 | // - creating a new topic/private message 188 | // - editing the 1st post 189 | if (this.get('canEditTitle') && !this.get('titleLengthValid')) return true; 190 | 191 | // reply is always required 192 | if (this.get('missingReplyCharacters') > 0) return true; 193 | 194 | if (this.get("privateMessage")) { 195 | // need at least one user when sending a PM 196 | return this.get('targetUsernames') && (this.get('targetUsernames').trim() + ',').indexOf(',') === 0; 197 | } else { 198 | // has a category? (when needed) 199 | return this.get('canCategorize') && 200 | !this.siteSettings.allow_uncategorized_topics && 201 | !this.get('categoryId') && 202 | !this.user.get('admin'); 203 | } 204 | }.property('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters'), 205 | 206 | titleLengthValid: function() { 207 | if (this.user.get('admin') && this.get('post.static_doc') && this.get('titleLength') > 0) return true; 208 | if (this.get('titleLength') < this.get('minimumTitleLength')) return false; 209 | return (this.get('titleLength') <= this.siteSettings.max_topic_title_length); 210 | }.property('minimumTitleLength', 'titleLength', 'post.static_doc'), 211 | 212 | // The icon for the save button 213 | saveIcon: function () { 214 | switch (this.get('action')) { 215 | case EDIT: return ''; 216 | case REPLY: return ''; 217 | case CREATE_TOPIC: return ''; 218 | case PRIVATE_MESSAGE: return ''; 219 | } 220 | }.property('action'), 221 | 222 | // The text for the save button 223 | saveText: function() { 224 | switch (this.get('action')) { 225 | case EDIT: return I18n.t('composer.save_edit'); 226 | case REPLY: return I18n.t('composer.reply'); 227 | case CREATE_TOPIC: return I18n.t('composer.create_topic'); 228 | case PRIVATE_MESSAGE: return I18n.t('composer.create_pm'); 229 | } 230 | }.property('action'), 231 | 232 | hasMetaData: function() { 233 | const metaData = this.get('metaData'); 234 | return metaData ? Em.isEmpty(Em.keys(this.get('metaData'))) : false; 235 | }.property('metaData'), 236 | 237 | /** 238 | Did the user make changes to the reply? 239 | 240 | @property replyDirty 241 | **/ 242 | replyDirty: function() { 243 | return this.get('reply') !== this.get('originalText'); 244 | }.property('reply', 'originalText'), 245 | 246 | /** 247 | Number of missing characters in the title until valid. 248 | 249 | @property missingTitleCharacters 250 | **/ 251 | missingTitleCharacters: function() { 252 | return this.get('minimumTitleLength') - this.get('titleLength'); 253 | }.property('minimumTitleLength', 'titleLength'), 254 | 255 | /** 256 | Minimum number of characters for a title to be valid. 257 | 258 | @property minimumTitleLength 259 | **/ 260 | minimumTitleLength: function() { 261 | if (this.get('privateMessage')) { 262 | return this.siteSettings.min_private_message_title_length; 263 | } else { 264 | return this.siteSettings.min_topic_title_length; 265 | } 266 | }.property('privateMessage'), 267 | 268 | missingReplyCharacters: function() { 269 | const postType = this.get('post.post_type'); 270 | if (postType === this.site.get('post_types.small_action')) { return 0; } 271 | return this.get('minimumPostLength') - this.get('replyLength'); 272 | }.property('minimumPostLength', 'replyLength'), 273 | 274 | /** 275 | Minimum number of characters for a post body to be valid. 276 | 277 | @property minimumPostLength 278 | **/ 279 | minimumPostLength: function() { 280 | if( this.get('privateMessage') ) { 281 | return this.siteSettings.min_private_message_post_length; 282 | } else if (this.get('topicFirstPost')) { 283 | // first post (topic body) 284 | return this.siteSettings.min_first_post_length; 285 | } else { 286 | return this.siteSettings.min_post_length; 287 | } 288 | }.property('privateMessage', 'topicFirstPost'), 289 | 290 | /** 291 | Computes the length of the title minus non-significant whitespaces 292 | 293 | @property titleLength 294 | **/ 295 | titleLength: function() { 296 | const title = this.get('title') || ""; 297 | return title.replace(/\s+/img, " ").trim().length; 298 | }.property('title'), 299 | 300 | /** 301 | Computes the length of the reply minus the quote(s) and non-significant whitespaces 302 | 303 | @property replyLength 304 | **/ 305 | replyLength: function() { 306 | let reply = this.get('reply') || ""; 307 | while (Quote.REGEXP.test(reply)) { reply = reply.replace(Quote.REGEXP, ""); } 308 | return reply.replace(/\s+/img, " ").trim().length; 309 | }.property('reply'), 310 | 311 | _setupComposer: function() { 312 | this.set('archetypeId', this.site.get('default_archetype')); 313 | }.on('init'), 314 | 315 | /** 316 | Append text to the current reply 317 | 318 | @method appendText 319 | @param {String} text the text to append 320 | **/ 321 | appendText(text,position,opts) { 322 | const reply = (this.get('reply') || ''); 323 | position = typeof(position) === "number" ? position : reply.length; 324 | 325 | let before = reply.slice(0, position) || ''; 326 | let after = reply.slice(position) || ''; 327 | 328 | let stripped, i; 329 | if (opts && opts.block){ 330 | if (before.trim() !== ""){ 331 | stripped = before.replace(/\r/g, ""); 332 | for(i=0; i<2; i++){ 333 | if(stripped[stripped.length - 1 - i] !== "\n"){ 334 | before += "\n"; 335 | position++; 336 | } 337 | } 338 | } 339 | if(after.trim() !== ""){ 340 | stripped = after.replace(/\r/g, ""); 341 | for(i=0; i<2; i++){ 342 | if(stripped[i] !== "\n"){ 343 | after = "\n" + after; 344 | } 345 | } 346 | } 347 | } 348 | 349 | if(opts && opts.space){ 350 | if(before.length > 0 && !before[before.length-1].match(/\s/)){ 351 | before = before + " "; 352 | } 353 | if(after.length > 0 && !after[0].match(/\s/)){ 354 | after = " " + after; 355 | } 356 | } 357 | 358 | this.set('reply', before + text + after); 359 | 360 | return before.length + text.length; 361 | }, 362 | 363 | applyTopicTemplate(oldCategoryId, categoryId) { 364 | if (this.get('action') !== CREATE_TOPIC) { return; } 365 | let reply = this.get('reply'); 366 | 367 | // If the user didn't change the template, clear it 368 | if (oldCategoryId) { 369 | const oldCat = this.site.categories.findProperty('id', oldCategoryId); 370 | if (oldCat && (oldCat.get('topic_template') === reply)) { 371 | reply = ""; 372 | } 373 | } 374 | 375 | if (!Ember.isEmpty(reply)) { return; } 376 | const category = this.site.categories.findProperty('id', categoryId); 377 | if (category) { 378 | this.set('reply', category.get('topic_template') || ""); 379 | } 380 | }, 381 | 382 | /* 383 | Open a composer 384 | 385 | opts: 386 | action - The action we're performing: edit, reply or createTopic 387 | post - The post we're replying to, if present 388 | topic - The topic we're replying to, if present 389 | quote - If we're opening a reply from a quote, the quote we're making 390 | */ 391 | open(opts) { 392 | if (!opts) opts = {}; 393 | this.set('loading', false); 394 | 395 | const replyBlank = Em.isEmpty(this.get("reply")); 396 | 397 | const composer = this; 398 | if (!replyBlank && 399 | ((opts.reply || opts.action === EDIT) && this.get('replyDirty'))) { 400 | return; 401 | } 402 | 403 | if (opts.action === REPLY && this.get('action') === EDIT) this.set('reply', ''); 404 | if (!opts.draftKey) throw 'draft key is required'; 405 | if (opts.draftSequence === null) throw 'draft sequence is required'; 406 | 407 | this.setProperties({ 408 | draftKey: opts.draftKey, 409 | draftSequence: opts.draftSequence, 410 | composeState: opts.composerState || OPEN, 411 | action: opts.action, 412 | topic: opts.topic, 413 | targetUsernames: opts.usernames, 414 | composerTotalOpened: opts.composerTime, 415 | typingTime: opts.typingTime 416 | }); 417 | 418 | if (opts.post) { 419 | this.set('post', opts.post); 420 | 421 | this.set('whisper', opts.post.get('post_type') === this.site.get('post_types.whisper')); 422 | if (!this.get('topic')) { 423 | this.set('topic', opts.post.get('topic')); 424 | } 425 | } else { 426 | this.set('post', null); 427 | } 428 | 429 | this.setProperties({ 430 | archetypeId: opts.archetypeId || this.site.get('default_archetype'), 431 | metaData: opts.metaData ? Em.Object.create(opts.metaData) : null, 432 | reply: opts.reply || this.get("reply") || "" 433 | }); 434 | 435 | // We set the category id separately for topic templates on opening of composer 436 | this.set('categoryId', opts.categoryId || this.get('topic.category.id')); 437 | 438 | if (!this.get('categoryId') && this.get('creatingTopic')) { 439 | const categories = Discourse.Category.list(); 440 | if (categories.length === 1) { 441 | this.set('categoryId', categories[0].get('id')); 442 | } 443 | } 444 | 445 | if (opts.postId) { 446 | this.set('loading', true); 447 | this.store.find('post', opts.postId).then(function(post) { 448 | composer.set('post', post); 449 | composer.set('loading', false); 450 | }); 451 | } 452 | 453 | // If we are editing a post, load it. 454 | if (opts.action === EDIT && opts.post) { 455 | 456 | const topicProps = this.serialize(_edit_topic_serializer); 457 | topicProps.loading = true; 458 | 459 | this.setProperties(topicProps); 460 | 461 | this.store.find('post', opts.post.get('id')).then(function(post) { 462 | composer.setProperties({ 463 | reply: post.get('raw'), 464 | originalText: post.get('raw'), 465 | loading: false 466 | }); 467 | }); 468 | } else if (opts.action === REPLY && opts.quote) { 469 | this.setProperties({ 470 | reply: opts.quote, 471 | originalText: opts.quote 472 | }); 473 | } 474 | if (opts.title) { this.set('title', opts.title); } 475 | this.set('originalText', opts.draft ? '' : this.get('reply')); 476 | 477 | return false; 478 | }, 479 | 480 | save(opts) { 481 | if (!this.get('cantSubmitPost')) { 482 | return this.get('editingPost') ? this.editPost(opts) : this.createPost(opts); 483 | } 484 | }, 485 | 486 | /** 487 | Clear any state we have in preparation for a new composition. 488 | 489 | @method clearState 490 | **/ 491 | clearState() { 492 | this.setProperties({ 493 | originalText: null, 494 | reply: null, 495 | post: null, 496 | title: null, 497 | editReason: null, 498 | stagedPost: false, 499 | typingTime: 0, 500 | composerOpened: null, 501 | composerTotalOpened: 0 502 | }); 503 | }, 504 | 505 | // When you edit a post 506 | editPost(opts) { 507 | const post = this.get('post'), 508 | oldCooked = post.get('cooked'), 509 | self = this; 510 | 511 | let promise; 512 | 513 | // Update the title if we've changed it, otherwise consider it a 514 | // successful resolved promise 515 | if (this.get('title') && 516 | post.get('post_number') === 1 && 517 | this.get('topic.details.can_edit')) { 518 | const topicProps = this.getProperties(Object.keys(_edit_topic_serializer)); 519 | 520 | promise = Topic.update(this.get('topic'), topicProps); 521 | } else { 522 | promise = Ember.RSVP.resolve(); 523 | } 524 | 525 | const props = { 526 | raw: this.get('reply'), 527 | edit_reason: opts.editReason, 528 | image_sizes: opts.imageSizes, 529 | cooked: this.getCookedHtml() 530 | }; 531 | 532 | this.set('composeState', CLOSED); 533 | 534 | var rollback = throwAjaxError(function(){ 535 | post.set('cooked', oldCooked); 536 | self.set('composeState', OPEN); 537 | }); 538 | 539 | return promise.then(function() { 540 | return post.save(props).then(function(result) { 541 | self.clearState(); 542 | return result; 543 | }).catch(function(error) { 544 | throw error; 545 | }); 546 | }).catch(rollback); 547 | }, 548 | 549 | serialize(serializer, dest) { 550 | dest = dest || {}; 551 | Object.keys(serializer).forEach(f => { 552 | const val = this.get(serializer[f]); 553 | if (typeof val !== 'undefined') { 554 | Ember.set(dest, f, val); 555 | } 556 | }); 557 | return dest; 558 | }, 559 | 560 | // Create a new Post 561 | createPost(opts) { 562 | const post = this.get('post'), 563 | topic = this.get('topic'), 564 | user = this.user, 565 | postStream = this.get('topic.postStream'); 566 | 567 | let addedToStream = false; 568 | 569 | const postTypes = this.site.get('post_types'); 570 | const postType = this.get('whisper') ? postTypes.whisper : postTypes.regular; 571 | 572 | // Build the post object 573 | const createdPost = this.store.createRecord('post', { 574 | imageSizes: opts.imageSizes, 575 | cooked: this.getCookedHtml(), 576 | reply_count: 0, 577 | name: user.get('name'), 578 | display_username: user.get('name'), 579 | username: user.get('username'), 580 | user_id: user.get('id'), 581 | user_title: user.get('title'), 582 | avatar_template: user.get('avatar_template'), 583 | user_custom_fields: user.get('custom_fields'), 584 | post_type: postType, 585 | actions_summary: [], 586 | moderator: user.get('moderator'), 587 | admin: user.get('admin'), 588 | yours: true, 589 | read: true, 590 | wiki: false, 591 | typingTime: this.get('typingTime'), 592 | composerTime: this.get('composerTime') 593 | }); 594 | 595 | this.serialize(_create_serializer, createdPost); 596 | 597 | if (post) { 598 | createdPost.setProperties({ 599 | reply_to_post_number: post.get('post_number'), 600 | reply_to_user: { 601 | username: post.get('username'), 602 | avatar_template: post.get('avatar_template') 603 | } 604 | }); 605 | } 606 | 607 | let state = null; 608 | 609 | // If we're in a topic, we can append the post instantly. 610 | if (postStream) { 611 | // If it's in reply to another post, increase the reply count 612 | if (post) { 613 | post.set('reply_count', (post.get('reply_count') || 0) + 1); 614 | post.set('replies', []); 615 | } 616 | 617 | // We do not stage posts in mobile view, we do not have the "cooked" 618 | // Furthermore calculating cooked is very complicated, especially since 619 | // we would need to handle oneboxes and other bits that are not even in the 620 | // engine, staging will just cause a blank post to render 621 | if (!_.isEmpty(createdPost.get('cooked'))) { 622 | state = postStream.stagePost(createdPost, user); 623 | if (state === "alreadyStaging") { return; } 624 | } 625 | } 626 | 627 | const composer = this; 628 | composer.set('composeState', SAVING); 629 | composer.set("stagedPost", state === "staged" && createdPost); 630 | 631 | return createdPost.save().then(function(result) { 632 | let saving = true; 633 | 634 | if (result.responseJson.action === "enqueued") { 635 | if (postStream) { postStream.undoPost(createdPost); } 636 | return result; 637 | } 638 | 639 | if (topic) { 640 | // It's no longer a new post 641 | topic.set('draft_sequence', result.target.draft_sequence); 642 | postStream.commitPost(createdPost); 643 | addedToStream = true; 644 | } else { 645 | // We created a new topic, let's show it. 646 | composer.set('composeState', CLOSED); 647 | saving = false; 648 | 649 | // Update topic_count for the category 650 | const category = composer.site.get('categories').find(function(x) { return x.get('id') === (parseInt(createdPost.get('category'),10) || 1); }); 651 | if (category) category.incrementProperty('topic_count'); 652 | Discourse.notifyPropertyChange('globalNotice'); 653 | } 654 | 655 | composer.clearState(); 656 | composer.set('createdPost', createdPost); 657 | 658 | if (addedToStream) { 659 | composer.set('composeState', CLOSED); 660 | } else if (saving) { 661 | composer.set('composeState', SAVING); 662 | } 663 | 664 | return result; 665 | }).catch(throwAjaxError(function() { 666 | if (postStream) { 667 | postStream.undoPost(createdPost); 668 | } 669 | Ember.run.next(() => composer.set('composeState', OPEN)); 670 | })); 671 | }, 672 | 673 | getCookedHtml() { 674 | return $('#reply-control .d-editor-preview').html().replace(/<\/span>/g, ''); 675 | }, 676 | 677 | saveDraft() { 678 | // Do not save when drafts are disabled 679 | if (this.get('disableDrafts')) return; 680 | // Do not save when there is no reply 681 | if (!this.get('reply')) return; 682 | // Do not save when the reply's length is too small 683 | if (this.get('replyLength') < this.siteSettings.min_post_length) return; 684 | 685 | const data = { 686 | reply: this.get('reply'), 687 | action: this.get('action'), 688 | title: this.get('title'), 689 | categoryId: this.get('categoryId'), 690 | postId: this.get('post.id'), 691 | archetypeId: this.get('archetypeId'), 692 | metaData: this.get('metaData'), 693 | usernames: this.get('targetUsernames'), 694 | composerTime: this.get('composerTime'), 695 | typingTime: this.get('typingTime') 696 | }; 697 | 698 | this.set('draftStatus', I18n.t('composer.saving_draft_tip')); 699 | 700 | const composer = this; 701 | 702 | if (this._clearingStatus) { 703 | Em.run.cancel(this._clearingStatus); 704 | this._clearingStatus = null; 705 | } 706 | 707 | // try to save the draft 708 | return Draft.save(this.get('draftKey'), this.get('draftSequence'), data) 709 | .then(function() { 710 | composer.set('draftStatus', I18n.t('composer.saved_draft_tip')); 711 | }).catch(function() { 712 | composer.set('draftStatus', I18n.t('composer.drafts_offline')); 713 | }); 714 | }, 715 | 716 | dataChanged: function(){ 717 | const draftStatus = this.get('draftStatus'); 718 | const self = this; 719 | 720 | if (draftStatus && !this._clearingStatus) { 721 | 722 | this._clearingStatus = Em.run.later(this, function(){ 723 | self.set('draftStatus', null); 724 | self._clearingStatus = null; 725 | }, 1000); 726 | } 727 | }.observes('title','reply') 728 | 729 | }); 730 | 731 | Composer.reopenClass({ 732 | 733 | // TODO: Replace with injection 734 | create(args) { 735 | args = args || {}; 736 | args.user = args.user || Discourse.User.current(); 737 | args.site = args.site || Discourse.Site.current(); 738 | args.siteSettings = args.siteSettings || Discourse.SiteSettings; 739 | return this._super(args); 740 | }, 741 | 742 | serializeToTopic(fieldName, property) { 743 | if (!property) { property = fieldName; } 744 | _edit_topic_serializer[fieldName] = property; 745 | }, 746 | 747 | serializeOnCreate(fieldName, property) { 748 | if (!property) { property = fieldName; } 749 | _create_serializer[fieldName] = property; 750 | }, 751 | 752 | serializedFieldsForCreate() { 753 | return Object.keys(_create_serializer); 754 | }, 755 | 756 | // The status the compose view can have 757 | CLOSED, 758 | SAVING, 759 | OPEN, 760 | DRAFT, 761 | 762 | // The actions the composer can take 763 | CREATE_TOPIC, 764 | PRIVATE_MESSAGE, 765 | REPLY, 766 | EDIT, 767 | 768 | // Draft key 769 | REPLY_AS_NEW_TOPIC_KEY 770 | }); 771 | 772 | export default Composer; 773 | -------------------------------------------------------------------------------- /benchmark/exec_js_uglify/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | if ENV['RR'] 4 | gem 'therubyracer' 5 | else 6 | gem 'mini_racer', path: '../../' 7 | end 8 | 9 | gem 'execjs', github: 'rails/execjs' 10 | gem 'uglifier' 11 | -------------------------------------------------------------------------------- /benchmark/exec_js_uglify/bench.rb: -------------------------------------------------------------------------------- 1 | engines = { 2 | mini_racer: proc { ExecJS::MiniRacerRuntime.new }, 3 | therubyracer: proc { ExecJS::RubyRacerRuntime.new }, 4 | rhino: proc { ExecJS::RubyRhinoRuntime.new }, 5 | duktape: proc { ExecJS::DuktapeRuntime.new}, 6 | node: proc { ExecJS::Runtimes::Node } 7 | } 8 | 9 | engine = ARGV[0] 10 | unless engine && (execjs_engine = engines[engine.to_sym]) 11 | STDERR.puts "Unknown engine try #{engines.keys.join(',')}" 12 | exit 1 13 | end 14 | 15 | unless defined? Bundler 16 | if engine == "therubyracer" 17 | system 'RR=1 bundle' 18 | exec "RR=1 bundle exec ruby bench.rb #{ARGV[0]}" 19 | else 20 | system 'bundle' 21 | exec "bundle exec ruby bench.rb #{ARGV[0]}" 22 | end 23 | end 24 | 25 | unless engine == "node" 26 | require engine 27 | end 28 | 29 | puts "Benching with #{engine}" 30 | require 'uglifier' 31 | 32 | ExecJS.runtime = execjs_engine.call 33 | 34 | start = Time.new 35 | Uglifier.compile(File.read("helper_files/discourse_app.js")) 36 | puts "#{engine} minify discourse_app.js #{(Time.new - start)*1000}ms" 37 | 38 | start = Time.new 39 | Uglifier.compile(File.read("helper_files/discourse_app_minified.js")) 40 | puts "#{engine} minify discourse_app_minified.js #{(Time.new - start)*1000}ms" 41 | 42 | start = Time.new 43 | (0..1).map do 44 | Thread.new do 45 | Uglifier.compile(File.read("helper_files/discourse_app.js")) 46 | end 47 | end.each(&:join) 48 | 49 | puts "#{engine} minify discourse_app.js twice (2 threads) #{(Time.new - start)*1000}ms" 50 | 51 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 4 | require "mini_racer" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/source-map-support/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /examples/source-map-support/error-causing-component.jsx: -------------------------------------------------------------------------------- 1 | function throwSomeError() { 2 | throw new Error( 3 | "^^ Look! These stack traces map to the actual source code :)" 4 | ); 5 | } 6 | 7 | export const ErrorCausingComponent = () => { 8 | throwSomeError(); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/source-map-support/index.jsx: -------------------------------------------------------------------------------- 1 | import { ErrorCausingComponent } from "./error-causing-component.jsx"; 2 | 3 | if (process.env.NODE_ENV === "production") { 4 | require("source-map-support").install({ 5 | // We tell the source-map-support package to retrieve our source maps by 6 | // calling the `readSourceMap` global function, which we attached to the 7 | // miniracer context 8 | retrieveSourceMap: filename => { 9 | return { 10 | url: filename, 11 | map: readSourceMap(filename) 12 | }; 13 | } 14 | }); 15 | } 16 | 17 | // We expose this function so we can call it later 18 | export function renderComponent() { 19 | ErrorCausingComponent(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/source-map-support/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-racer-source-map-support-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@babel/core": "^7.3.4", 8 | "babel-loader": "^8.0.5", 9 | "prettier": "^1.16.4", 10 | "webpack": "^4.29.6", 11 | "webpack-cli": "^3.2.3" 12 | }, 13 | "scripts": { 14 | "build": "webpack" 15 | }, 16 | "dependencies": { 17 | "source-map-support": "^0.5.10" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/source-map-support/readme.md: -------------------------------------------------------------------------------- 1 | # Source Map Support 2 | 3 | This example shows how to map source maps using webpack. Webpack production 4 | builds will compile and minify code aggressively, so source maps are very 5 | important when debugging production issues. This example shows how to give 6 | readable stack traces to make debugging easier. 7 | 8 | ## Running the example 9 | 10 | 1. Install the dependencies: `yarn install` 11 | 2. Build the js bundle: `yarn build` 12 | 3. Run the ruby code which triggers an error: `bundle exec ruby renderer.rb` 13 | 14 | After running that, you will see the correct source code locations where the 15 | error occurred. The result will intentionally throw an error which looks like: 16 | 17 | ``` 18 | Traceback (most recent call last): 19 | 10: from renderer.rb:12:in `
' 20 | 9: from /home/ianks/Development/adhawk/mini_racer/lib/mini_racer.rb:176:in `eval' 21 | 8: from /home/ianks/Development/adhawk/mini_racer/lib/mini_racer.rb:176:in `synchronize' 22 | 7: from /home/ianks/Development/adhawk/mini_racer/lib/mini_racer.rb:178:in `block in eval' 23 | 6: from /home/ianks/Development/adhawk/mini_racer/lib/mini_racer.rb:264:in `timeout' 24 | 5: from /home/ianks/Development/adhawk/mini_racer/lib/mini_racer.rb:179:in `block (2 levels) in eval' 25 | 4: from /home/ianks/Development/adhawk/mini_racer/lib/mini_racer.rb:179:in `eval_unsafe' 26 | 3: from JavaScript at :1:17 27 | 2: from JavaScript at Module.ErrorCausingComponent (/webpack:/webpackLib/index.jsx:19:3) 28 | 1: from JavaScript at throwSomeError (/webpack:/webpackLib/error-causing-component.jsx:8:3) 29 | JavaScript at /webpack:/webpackLib/error-causing-component.jsx:2:9: Error: ^^ Look! These stack traces map to the actual source code :) (MiniRacer::RuntimeError) 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/source-map-support/renderer.rb: -------------------------------------------------------------------------------- 1 | require 'mini_racer' 2 | 3 | ctx = MiniRacer::Context.new 4 | 5 | # Make sure we pass the filename option so source-map-support can map properly 6 | ctx.eval(File.read('./dist/main.js'), filename: 'main.js') 7 | 8 | # Expose a function to retrieve the source map 9 | ctx.attach('readSourceMap', proc { |filename| File.read("./dist/#{filename}.map")} ) 10 | 11 | # This will actually cause the error, and we will have a pretty backtrace! 12 | ctx.eval('this.webpackLib.renderComponent()') 13 | -------------------------------------------------------------------------------- /examples/source-map-support/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "production", 3 | entry: "./index.jsx", 4 | // This will put our source maps in a seperate .map file which we will read 5 | // later 6 | devtool: "source-map", 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.jsx?$/, 11 | exclude: /node_modules/, 12 | use: { 13 | loader: "babel-loader" 14 | } 15 | } 16 | ] 17 | }, 18 | output: { 19 | library: "webpackLib", 20 | libraryTarget: "umd", 21 | // This is necessary to make webpack not define globals on the non-existent 22 | // 'window' object 23 | globalObject: "this" 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /ext/mini_racer_extension/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | $srcs = ["mini_racer_extension.c", "mini_racer_v8.cc"] 4 | 5 | if RUBY_ENGINE == "truffleruby" 6 | File.write("Makefile", dummy_makefile($srcdir).join("")) 7 | return 8 | end 9 | 10 | require_relative '../../lib/mini_racer/version' 11 | gem 'libv8-node', MiniRacer::LIBV8_NODE_VERSION 12 | require 'libv8-node' 13 | 14 | IS_DARWIN = RUBY_PLATFORM =~ /darwin/ 15 | 16 | have_library('pthread') 17 | have_library('objc') if IS_DARWIN 18 | $CXXFLAGS += " -Wall" unless $CXXFLAGS.split.include? "-Wall" 19 | $CXXFLAGS += " -g" unless $CXXFLAGS.split.include? "-g" 20 | $CXXFLAGS += " -rdynamic" unless $CXXFLAGS.split.include? "-rdynamic" 21 | $CXXFLAGS += " -fPIC" unless $CXXFLAGS.split.include? "-rdynamic" or IS_DARWIN 22 | $CXXFLAGS += " -std=c++20" 23 | $CXXFLAGS += " -fpermissive" 24 | $CXXFLAGS += " -fno-rtti" 25 | $CXXFLAGS += " -fno-exceptions" 26 | $CXXFLAGS += " -fno-strict-aliasing" 27 | #$CXXFLAGS += " -DV8_COMPRESS_POINTERS" 28 | $CXXFLAGS += " -fvisibility=hidden " 29 | 30 | # __declspec gets used by clang via ruby 3.x headers... 31 | $CXXFLAGS += " -fms-extensions" 32 | 33 | $CXXFLAGS += " -Wno-reserved-user-defined-literal" if IS_DARWIN 34 | 35 | if IS_DARWIN 36 | $LDFLAGS.insert(0, " -stdlib=libc++ ") 37 | else 38 | $LDFLAGS.insert(0, " -lstdc++ ") 39 | end 40 | 41 | # check for missing symbols at link time 42 | # $LDFLAGS += " -Wl,--no-undefined " unless IS_DARWIN 43 | # $LDFLAGS += " -Wl,-undefined,error " if IS_DARWIN 44 | 45 | if ENV['CXX'] 46 | puts "SETTING CXX" 47 | CONFIG['CXX'] = ENV['CXX'] 48 | end 49 | 50 | CONFIG['LDSHARED'] = '$(CXX) -shared' unless IS_DARWIN 51 | if CONFIG['warnflags'] 52 | CONFIG['warnflags'].gsub!('-Wdeclaration-after-statement', '') 53 | CONFIG['warnflags'].gsub!('-Wimplicit-function-declaration', '') 54 | end 55 | 56 | if enable_config('debug') || enable_config('asan') 57 | CONFIG['debugflags'] << ' -ggdb3 -O0' 58 | end 59 | 60 | Libv8::Node.configure_makefile 61 | 62 | # --exclude-libs is only for i386 PE and ELF targeted ports 63 | append_ldflags("-Wl,--exclude-libs=ALL ") 64 | 65 | if enable_config('asan') 66 | $CXXFLAGS.insert(0, " -fsanitize=address ") 67 | $LDFLAGS.insert(0, " -fsanitize=address ") 68 | end 69 | 70 | # there doesn't seem to be a CPP macro for this in Ruby 2.6: 71 | if RUBY_ENGINE == 'ruby' 72 | $CPPFLAGS += ' -DENGINE_IS_CRUBY ' 73 | end 74 | 75 | create_makefile 'mini_racer_extension' 76 | -------------------------------------------------------------------------------- /ext/mini_racer_extension/mini_racer_v8.cc: -------------------------------------------------------------------------------- 1 | #include "v8.h" 2 | #include "v8-profiler.h" 3 | #include "libplatform/libplatform.h" 4 | #include "mini_racer_v8.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // note: the filter function gets called inside the safe context, 15 | // i.e., the context that has not been tampered with by user JS 16 | // convention: $-prefixed identifiers signify objects from the 17 | // user JS context and should be handled with special care 18 | static const char safe_context_script_source[] = R"js( 19 | ;(function($globalThis) { 20 | const {Map: $Map, Set: $Set} = $globalThis 21 | const sentinel = {} 22 | return function filter(v) { 23 | if (typeof v === "function") 24 | return sentinel 25 | if (typeof v !== "object" || v === null) 26 | return v 27 | if (v instanceof $Map) { 28 | const m = new Map() 29 | for (let [k, t] of Map.prototype.entries.call(v)) { 30 | t = filter(t) 31 | if (t !== sentinel) 32 | m.set(k, t) 33 | } 34 | return m 35 | } else if (v instanceof $Set) { 36 | const s = new Set() 37 | for (let t of Set.prototype.values.call(v)) { 38 | t = filter(t) 39 | if (t !== sentinel) 40 | s.add(t) 41 | } 42 | return s 43 | } else { 44 | const o = Array.isArray(v) ? [] : {} 45 | const pds = Object.getOwnPropertyDescriptors(v) 46 | for (const [k, d] of Object.entries(pds)) { 47 | if (!d.enumerable) 48 | continue 49 | let t = d.value 50 | if (d.get) { 51 | // *not* d.get.call(...), may have been tampered with 52 | t = Function.prototype.call.call(d.get, v, k) 53 | } 54 | t = filter(t) 55 | if (t !== sentinel) 56 | Object.defineProperty(o, k, {value: t, enumerable: true}) 57 | } 58 | return o 59 | } 60 | } 61 | }) 62 | )js"; 63 | 64 | struct Callback 65 | { 66 | struct State *st; 67 | int32_t id; 68 | }; 69 | 70 | // NOTE: do *not* use thread_locals to store state. In single-threaded 71 | // mode, V8 runs on the same thread as Ruby and the Ruby runtime clobbers 72 | // thread-locals when it context-switches threads. Ruby 3.4.0 has a new 73 | // API rb_thread_lock_native_thread() that pins the thread but I don't 74 | // think we're quite ready yet to drop support for older versions, hence 75 | // this inelegant "everything" struct. 76 | struct State 77 | { 78 | v8::Isolate *isolate; 79 | // declaring as Local is safe because we take special care 80 | // to ensure it's rooted in a HandleScope before being used 81 | v8::Local context; 82 | // extra context for when we need access to built-ins like Array 83 | // and want to be sure they haven't been tampered with by JS code 84 | v8::Local safe_context; 85 | v8::Local safe_context_function; 86 | v8::Persistent persistent_context; // single-thread mode only 87 | v8::Persistent persistent_safe_context; // single-thread mode only 88 | v8::Persistent persistent_safe_context_function; // single-thread mode only 89 | Context *ruby_context; 90 | int64_t max_memory; 91 | int err_reason; 92 | bool verbose_exceptions; 93 | std::vector callbacks; 94 | std::unique_ptr allocator; 95 | inline ~State(); 96 | }; 97 | 98 | namespace { 99 | 100 | // deliberately leaked on program exit, 101 | // not safe to destroy after main() returns 102 | v8::Platform *platform; 103 | 104 | struct Serialized 105 | { 106 | uint8_t *data = nullptr; 107 | size_t size = 0; 108 | 109 | Serialized(State& st, v8::Local v) 110 | { 111 | v8::ValueSerializer ser(st.isolate); 112 | ser.WriteHeader(); 113 | if (!ser.WriteValue(st.context, v).FromMaybe(false)) return; // exception pending 114 | auto pair = ser.Release(); 115 | data = pair.first; 116 | size = pair.second; 117 | } 118 | 119 | ~Serialized() 120 | { 121 | free(data); 122 | } 123 | }; 124 | 125 | // throws JS exception on serialization error 126 | bool reply(State& st, v8::Local v) 127 | { 128 | v8::TryCatch try_catch(st.isolate); 129 | { 130 | Serialized serialized(st, v); 131 | if (serialized.data) { 132 | v8_reply(st.ruby_context, serialized.data, serialized.size); 133 | return true; 134 | } 135 | } 136 | if (!try_catch.CanContinue()) { 137 | try_catch.ReThrow(); 138 | return false; 139 | } 140 | auto recv = v8::Undefined(st.isolate); 141 | if (!st.safe_context_function->Call(st.safe_context, recv, 1, &v).ToLocal(&v)) { 142 | try_catch.ReThrow(); 143 | return false; 144 | } 145 | Serialized serialized(st, v); 146 | if (serialized.data) 147 | v8_reply(st.ruby_context, serialized.data, serialized.size); 148 | return serialized.data != nullptr; // exception pending if false 149 | } 150 | 151 | bool reply(State& st, v8::Local result, v8::Local err) 152 | { 153 | v8::TryCatch try_catch(st.isolate); 154 | try_catch.SetVerbose(st.verbose_exceptions); 155 | v8::Local response; 156 | { 157 | v8::Context::Scope context_scope(st.safe_context); 158 | response = v8::Array::New(st.isolate, 2); 159 | } 160 | response->Set(st.context, 0, result).Check(); 161 | response->Set(st.context, 1, err).Check(); 162 | if (reply(st, response)) return true; 163 | if (!try_catch.CanContinue()) { // termination exception? 164 | try_catch.ReThrow(); 165 | return false; 166 | } 167 | v8::String::Utf8Value s(st.isolate, try_catch.Exception()); 168 | const char *message = *s ? *s : "unexpected failure"; 169 | // most serialization errors will be DataCloneErrors but not always 170 | // DataCloneErrors are not directly detectable so use a heuristic 171 | if (!strstr(message, "could not be cloned")) { 172 | try_catch.ReThrow(); 173 | return false; 174 | } 175 | // return an {"error": "foo could not be cloned"} object 176 | v8::Local error; 177 | { 178 | v8::Context::Scope context_scope(st.safe_context); 179 | error = v8::Object::New(st.isolate); 180 | } 181 | auto key = v8::String::NewFromUtf8Literal(st.isolate, "error"); 182 | v8::Local val; 183 | if (!v8::String::NewFromUtf8(st.isolate, message).ToLocal(&val)) { 184 | val = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error"); 185 | } 186 | error->Set(st.context, key, val).Check(); 187 | response->Set(st.context, 0, error).Check(); 188 | if (!reply(st, response)) { 189 | try_catch.ReThrow(); 190 | return false; 191 | } 192 | return true; 193 | } 194 | 195 | // for when a reply is not expected to fail because of serialization 196 | // errors but can still fail when preempted by isolate termination; 197 | // temporarily cancels the termination exception so it can send the reply 198 | void reply_retry(State& st, v8::Local response) 199 | { 200 | v8::TryCatch try_catch(st.isolate); 201 | try_catch.SetVerbose(st.verbose_exceptions); 202 | bool ok = reply(st, response); 203 | while (!ok) { 204 | assert(try_catch.HasCaught()); 205 | assert(try_catch.HasTerminated()); 206 | if (!try_catch.HasTerminated()) abort(); 207 | st.isolate->CancelTerminateExecution(); 208 | ok = reply(st, response); 209 | st.isolate->TerminateExecution(); 210 | } 211 | } 212 | 213 | v8::Local sanitize(State& st, v8::Local v) 214 | { 215 | // punch through proxies 216 | while (v->IsProxy()) v = v8::Proxy::Cast(*v)->GetTarget(); 217 | // V8's serializer doesn't accept symbols 218 | if (v->IsSymbol()) return v8::Symbol::Cast(*v)->Description(st.isolate); 219 | // TODO(bnoordhuis) replace this hack with something more principled 220 | if (v->IsFunction()) { 221 | auto type = v8::NewStringType::kNormal; 222 | const size_t size = sizeof(js_function_marker) / sizeof(*js_function_marker); 223 | return v8::String::NewFromTwoByte(st.isolate, js_function_marker, type, size).ToLocalChecked(); 224 | } 225 | if (v->IsWeakMap() || v->IsWeakSet() || v->IsMapIterator() || v->IsSetIterator()) { 226 | bool is_key_value; 227 | v8::Local array; 228 | if (v8::Object::Cast(*v)->PreviewEntries(&is_key_value).ToLocal(&array)) { 229 | return array; 230 | } 231 | } 232 | return v; 233 | } 234 | 235 | v8::Local to_error(State& st, v8::TryCatch *try_catch, int cause) 236 | { 237 | v8::Local t; 238 | char buf[1024]; 239 | 240 | *buf = '\0'; 241 | if (cause == NO_ERROR) { 242 | // nothing to do 243 | } else if (cause == PARSE_ERROR) { 244 | auto message = try_catch->Message(); 245 | v8::String::Utf8Value s(st.isolate, message->Get()); 246 | v8::String::Utf8Value name(st.isolate, message->GetScriptResourceName()); 247 | if (!*s || !*name) goto fallback; 248 | auto line = message->GetLineNumber(st.context).FromMaybe(0); 249 | auto column = message->GetStartColumn(st.context).FromMaybe(0); 250 | snprintf(buf, sizeof(buf), "%c%s at %s:%d:%d", cause, *s, *name, line, column); 251 | } else if (try_catch->StackTrace(st.context).ToLocal(&t)) { 252 | v8::String::Utf8Value s(st.isolate, t); 253 | if (!*s) goto fallback; 254 | snprintf(buf, sizeof(buf), "%c%s", cause, *s); 255 | } else { 256 | fallback: 257 | v8::String::Utf8Value s(st.isolate, try_catch->Exception()); 258 | const char *message = *s ? *s : "unexpected failure"; 259 | if (cause == MEMORY_ERROR) message = "out of memory"; 260 | if (cause == TERMINATED_ERROR) message = "terminated"; 261 | snprintf(buf, sizeof(buf), "%c%s", cause, message); 262 | } 263 | v8::Local s; 264 | if (v8::String::NewFromUtf8(st.isolate, buf).ToLocal(&s)) return s; 265 | return v8::String::Empty(st.isolate); 266 | } 267 | 268 | extern "C" void v8_global_init(void) 269 | { 270 | char *p; 271 | size_t n; 272 | 273 | v8_get_flags(&p, &n); 274 | if (p) { 275 | for (char *s = p; s < p+n; s += 1 + strlen(s)) { 276 | v8::V8::SetFlagsFromString(s); 277 | } 278 | free(p); 279 | } 280 | v8::V8::InitializeICU(); 281 | if (single_threaded) { 282 | platform = v8::platform::NewSingleThreadedDefaultPlatform().release(); 283 | } else { 284 | platform = v8::platform::NewDefaultPlatform().release(); 285 | } 286 | v8::V8::InitializePlatform(platform); 287 | v8::V8::Initialize(); 288 | } 289 | 290 | void v8_gc_callback(v8::Isolate*, v8::GCType, v8::GCCallbackFlags, void *data) 291 | { 292 | State& st = *static_cast(data); 293 | v8::HeapStatistics s; 294 | st.isolate->GetHeapStatistics(&s); 295 | int64_t used_heap_size = static_cast(s.used_heap_size()); 296 | if (used_heap_size > st.max_memory) { 297 | st.err_reason = MEMORY_ERROR; 298 | st.isolate->TerminateExecution(); 299 | } 300 | } 301 | 302 | extern "C" State *v8_thread_init(Context *c, const uint8_t *snapshot_buf, 303 | size_t snapshot_len, int64_t max_memory, 304 | int verbose_exceptions) 305 | { 306 | State *pst = new State{}; 307 | State& st = *pst; 308 | st.verbose_exceptions = (verbose_exceptions != 0); 309 | st.ruby_context = c; 310 | st.allocator.reset(v8::ArrayBuffer::Allocator::NewDefaultAllocator()); 311 | v8::StartupData blob{nullptr, 0}; 312 | v8::Isolate::CreateParams params; 313 | params.array_buffer_allocator = st.allocator.get(); 314 | if (snapshot_len) { 315 | blob.data = reinterpret_cast(snapshot_buf); 316 | blob.raw_size = snapshot_len; 317 | params.snapshot_blob = &blob; 318 | } 319 | st.isolate = v8::Isolate::New(params); 320 | st.max_memory = max_memory; 321 | if (st.max_memory > 0) 322 | st.isolate->AddGCEpilogueCallback(v8_gc_callback, pst); 323 | { 324 | v8::Locker locker(st.isolate); 325 | v8::Isolate::Scope isolate_scope(st.isolate); 326 | v8::HandleScope handle_scope(st.isolate); 327 | st.safe_context = v8::Context::New(st.isolate); 328 | st.context = v8::Context::New(st.isolate); 329 | v8::Context::Scope context_scope(st.context); 330 | { 331 | v8::Context::Scope context_scope(st.safe_context); 332 | auto source = v8::String::NewFromUtf8Literal(st.isolate, safe_context_script_source); 333 | auto filename = v8::String::NewFromUtf8Literal(st.isolate, "safe_context_script.js"); 334 | v8::ScriptOrigin origin(filename); 335 | auto script = 336 | v8::Script::Compile(st.safe_context, source, &origin) 337 | .ToLocalChecked(); 338 | auto function_v = script->Run(st.safe_context).ToLocalChecked(); 339 | auto function = v8::Function::Cast(*function_v); 340 | auto recv = v8::Undefined(st.isolate); 341 | v8::Local arg = st.context->Global(); 342 | // grant the safe context access to the user context's globalThis 343 | st.safe_context->SetSecurityToken(st.context->GetSecurityToken()); 344 | function_v = 345 | function->Call(st.safe_context, recv, 1, &arg) 346 | .ToLocalChecked(); 347 | // revoke access again now that the script did its one-time setup 348 | st.safe_context->UseDefaultSecurityToken(); 349 | st.safe_context_function = v8::Local::Cast(function_v); 350 | } 351 | if (single_threaded) { 352 | st.persistent_safe_context_function.Reset(st.isolate, st.safe_context_function); 353 | st.persistent_safe_context.Reset(st.isolate, st.safe_context); 354 | st.persistent_context.Reset(st.isolate, st.context); 355 | return pst; // intentionally returning early and keeping alive 356 | } 357 | v8_thread_main(c, pst); 358 | } 359 | delete pst; 360 | return nullptr; 361 | } 362 | 363 | void v8_api_callback(const v8::FunctionCallbackInfo& info) 364 | { 365 | auto ext = v8::External::Cast(*info.Data()); 366 | Callback *cb = static_cast(ext->Value()); 367 | State& st = *cb->st; 368 | v8::Local request; 369 | { 370 | v8::Context::Scope context_scope(st.safe_context); 371 | request = v8::Array::New(st.isolate, 1 + info.Length()); 372 | } 373 | for (int i = 0, n = info.Length(); i < n; i++) { 374 | request->Set(st.context, i, sanitize(st, info[i])).Check(); 375 | } 376 | auto id = v8::Int32::New(st.isolate, cb->id); 377 | request->Set(st.context, info.Length(), id).Check(); // callback id 378 | { 379 | Serialized serialized(st, request); 380 | if (!serialized.data) return; // exception pending 381 | uint8_t marker = 'c'; // callback marker 382 | v8_reply(st.ruby_context, &marker, 1); 383 | v8_reply(st.ruby_context, serialized.data, serialized.size); 384 | } 385 | const uint8_t *p; 386 | size_t n; 387 | for (;;) { 388 | v8_roundtrip(st.ruby_context, &p, &n); 389 | if (*p == 'c') // callback reply 390 | break; 391 | if (*p == 'e') // ruby exception pending 392 | return st.isolate->TerminateExecution(); 393 | v8_dispatch(st.ruby_context); 394 | } 395 | v8::ValueDeserializer des(st.isolate, p+1, n-1); 396 | des.ReadHeader(st.context).Check(); 397 | v8::Local result; 398 | if (!des.ReadValue(st.context).ToLocal(&result)) return; // exception pending 399 | info.GetReturnValue().Set(result); 400 | } 401 | 402 | // response is err or empty string 403 | extern "C" void v8_attach(State *pst, const uint8_t *p, size_t n) 404 | { 405 | State& st = *pst; 406 | v8::TryCatch try_catch(st.isolate); 407 | try_catch.SetVerbose(st.verbose_exceptions); 408 | v8::HandleScope handle_scope(st.isolate); 409 | v8::ValueDeserializer des(st.isolate, p, n); 410 | des.ReadHeader(st.context).Check(); 411 | int cause = INTERNAL_ERROR; 412 | { 413 | v8::Local request_v; 414 | if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail; 415 | v8::Local request; // [name, id] 416 | if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail; 417 | v8::Local name_v; 418 | if (!request->Get(st.context, 0).ToLocal(&name_v)) goto fail; 419 | v8::Local id_v; 420 | if (!request->Get(st.context, 1).ToLocal(&id_v)) goto fail; 421 | v8::Local name; 422 | if (!name_v->ToString(st.context).ToLocal(&name)) goto fail; 423 | int32_t id; 424 | if (!id_v->Int32Value(st.context).To(&id)) goto fail; 425 | Callback *cb = new Callback{pst, id}; 426 | st.callbacks.push_back(cb); 427 | v8::Local ext = v8::External::New(st.isolate, cb); 428 | v8::Local function; 429 | if (!v8::Function::New(st.context, v8_api_callback, ext).ToLocal(&function)) goto fail; 430 | // support foo.bar.baz paths 431 | v8::String::Utf8Value path(st.isolate, name); 432 | if (!*path) goto fail; 433 | v8::Local obj = st.context->Global(); 434 | v8::Local key; 435 | for (const char *p = *path;;) { 436 | size_t n = strcspn(p, "."); 437 | auto type = v8::NewStringType::kNormal; 438 | if (!v8::String::NewFromUtf8(st.isolate, p, type, n).ToLocal(&key)) goto fail; 439 | if (p[n] == '\0') break; 440 | p += n + 1; 441 | v8::Local val; 442 | if (!obj->Get(st.context, key).ToLocal(&val)) goto fail; 443 | if (!val->IsObject() && !val->IsFunction()) { 444 | val = v8::Object::New(st.isolate); 445 | if (!obj->Set(st.context, key, val).FromMaybe(false)) goto fail; 446 | } 447 | obj = val.As(); 448 | } 449 | if (!obj->Set(st.context, key, function).FromMaybe(false)) goto fail; 450 | } 451 | cause = NO_ERROR; 452 | fail: 453 | if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR; 454 | auto err = to_error(st, &try_catch, cause); 455 | reply_retry(st, err); 456 | } 457 | 458 | // response is errback [result, err] array 459 | extern "C" void v8_call(State *pst, const uint8_t *p, size_t n) 460 | { 461 | State& st = *pst; 462 | v8::TryCatch try_catch(st.isolate); 463 | try_catch.SetVerbose(st.verbose_exceptions); 464 | v8::HandleScope handle_scope(st.isolate); 465 | v8::ValueDeserializer des(st.isolate, p, n); 466 | std::vector> args; 467 | des.ReadHeader(st.context).Check(); 468 | v8::Local result; 469 | int cause = INTERNAL_ERROR; 470 | { 471 | v8::Local request_v; 472 | if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail; 473 | v8::Local request; 474 | if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail; 475 | v8::Local name_v; 476 | if (!request->Get(st.context, 0).ToLocal(&name_v)) goto fail; 477 | v8::Local name; 478 | if (!name_v->ToString(st.context).ToLocal(&name)) goto fail; 479 | cause = RUNTIME_ERROR; 480 | // support foo.bar.baz paths 481 | v8::String::Utf8Value path(st.isolate, name); 482 | if (!*path) goto fail; 483 | v8::Local obj = st.context->Global(); 484 | v8::Local key; 485 | for (const char *p = *path;;) { 486 | size_t n = strcspn(p, "."); 487 | auto type = v8::NewStringType::kNormal; 488 | if (!v8::String::NewFromUtf8(st.isolate, p, type, n).ToLocal(&key)) goto fail; 489 | if (p[n] == '\0') break; 490 | p += n + 1; 491 | v8::Local val; 492 | if (!obj->Get(st.context, key).ToLocal(&val)) goto fail; 493 | if (!val->ToObject(st.context).ToLocal(&obj)) goto fail; 494 | } 495 | v8::Local function_v; 496 | if (!obj->Get(st.context, key).ToLocal(&function_v)) goto fail; 497 | if (!function_v->IsFunction()) { 498 | // XXX it's technically possible for |function_v| to be a callable 499 | // object but those are effectively extinct; regexp objects used 500 | // to be callable but not anymore 501 | auto message = v8::String::NewFromUtf8Literal(st.isolate, "not a function"); 502 | auto exception = v8::Exception::TypeError(message); 503 | st.isolate->ThrowException(exception); 504 | goto fail; 505 | } 506 | auto function = v8::Function::Cast(*function_v); 507 | assert(request->IsArray()); 508 | int n = v8::Array::Cast(*request)->Length(); 509 | for (int i = 1; i < n; i++) { 510 | v8::Local val; 511 | if (!request->Get(st.context, i).ToLocal(&val)) goto fail; 512 | args.push_back(val); 513 | } 514 | auto maybe_result_v = function->Call(st.context, obj, args.size(), args.data()); 515 | v8::Local result_v; 516 | if (!maybe_result_v.ToLocal(&result_v)) goto fail; 517 | result = sanitize(st, result_v); 518 | } 519 | cause = NO_ERROR; 520 | fail: 521 | if (st.isolate->IsExecutionTerminating()) { 522 | st.isolate->CancelTerminateExecution(); 523 | cause = st.err_reason ? st.err_reason : TERMINATED_ERROR; 524 | st.err_reason = NO_ERROR; 525 | } 526 | if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR; 527 | if (cause) result = v8::Undefined(st.isolate); 528 | auto err = to_error(st, &try_catch, cause); 529 | if (!reply(st, result, err)) { 530 | assert(try_catch.HasCaught()); 531 | goto fail; // retry; can be termination exception 532 | } 533 | } 534 | 535 | // response is errback [result, err] array 536 | extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n) 537 | { 538 | State& st = *pst; 539 | v8::TryCatch try_catch(st.isolate); 540 | try_catch.SetVerbose(st.verbose_exceptions); 541 | v8::HandleScope handle_scope(st.isolate); 542 | v8::ValueDeserializer des(st.isolate, p, n); 543 | des.ReadHeader(st.context).Check(); 544 | v8::Local result; 545 | int cause = INTERNAL_ERROR; 546 | { 547 | v8::Local request_v; 548 | if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail; 549 | v8::Local request; // [filename, source] 550 | if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail; 551 | v8::Local filename; 552 | if (!request->Get(st.context, 0).ToLocal(&filename)) goto fail; 553 | v8::Local source_v; 554 | if (!request->Get(st.context, 1).ToLocal(&source_v)) goto fail; 555 | v8::Local source; 556 | if (!source_v->ToString(st.context).ToLocal(&source)) goto fail; 557 | v8::ScriptOrigin origin(filename); 558 | v8::Local script; 559 | cause = PARSE_ERROR; 560 | if (!v8::Script::Compile(st.context, source, &origin).ToLocal(&script)) goto fail; 561 | v8::Local result_v; 562 | cause = RUNTIME_ERROR; 563 | auto maybe_result_v = script->Run(st.context); 564 | if (!maybe_result_v.ToLocal(&result_v)) goto fail; 565 | result = sanitize(st, result_v); 566 | } 567 | cause = NO_ERROR; 568 | fail: 569 | if (st.isolate->IsExecutionTerminating()) { 570 | st.isolate->CancelTerminateExecution(); 571 | cause = st.err_reason ? st.err_reason : TERMINATED_ERROR; 572 | st.err_reason = NO_ERROR; 573 | } 574 | if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR; 575 | if (cause) result = v8::Undefined(st.isolate); 576 | auto err = to_error(st, &try_catch, cause); 577 | if (!reply(st, result, err)) { 578 | assert(try_catch.HasCaught()); 579 | goto fail; // retry; can be termination exception 580 | } 581 | } 582 | 583 | extern "C" void v8_heap_stats(State *pst) 584 | { 585 | State& st = *pst; 586 | v8::HandleScope handle_scope(st.isolate); 587 | v8::HeapStatistics s; 588 | st.isolate->GetHeapStatistics(&s); 589 | v8::Local response = v8::Object::New(st.isolate); 590 | #define PROP(name) \ 591 | do { \ 592 | auto key = v8::String::NewFromUtf8Literal(st.isolate, #name); \ 593 | auto val = v8::Number::New(st.isolate, s.name()); \ 594 | response->Set(st.context, key, val).Check(); \ 595 | } while (0) 596 | PROP(total_heap_size); 597 | PROP(total_heap_size); 598 | PROP(total_heap_size_executable); 599 | PROP(total_physical_size); 600 | PROP(total_available_size); 601 | PROP(total_global_handles_size); 602 | PROP(used_global_handles_size); 603 | PROP(used_heap_size); 604 | PROP(heap_size_limit); 605 | PROP(malloced_memory); 606 | PROP(external_memory); 607 | PROP(peak_malloced_memory); 608 | PROP(number_of_native_contexts); 609 | PROP(number_of_detached_contexts); 610 | #undef PROP 611 | reply_retry(st, response); 612 | } 613 | 614 | struct OutputStream : public v8::OutputStream 615 | { 616 | std::vector buf; 617 | 618 | void EndOfStream() final {} 619 | int GetChunkSize() final { return 65536; } 620 | 621 | WriteResult WriteAsciiChunk(char* data, int size) 622 | { 623 | const uint8_t *p = reinterpret_cast(data); 624 | buf.insert(buf.end(), p, p+size); 625 | return WriteResult::kContinue; 626 | } 627 | }; 628 | 629 | extern "C" void v8_heap_snapshot(State *pst) 630 | { 631 | State& st = *pst; 632 | v8::HandleScope handle_scope(st.isolate); 633 | auto snapshot = st.isolate->GetHeapProfiler()->TakeHeapSnapshot(); 634 | OutputStream os; 635 | snapshot->Serialize(&os, v8::HeapSnapshot::kJSON); 636 | v8_reply(st.ruby_context, os.buf.data(), os.buf.size()); // not serialized because big 637 | } 638 | 639 | extern "C" void v8_pump_message_loop(State *pst) 640 | { 641 | State& st = *pst; 642 | v8::TryCatch try_catch(st.isolate); 643 | try_catch.SetVerbose(st.verbose_exceptions); 644 | v8::HandleScope handle_scope(st.isolate); 645 | bool ran_task = v8::platform::PumpMessageLoop(platform, st.isolate); 646 | if (st.isolate->IsExecutionTerminating()) goto fail; 647 | if (try_catch.HasCaught()) goto fail; 648 | if (ran_task) v8::MicrotasksScope::PerformCheckpoint(st.isolate); 649 | if (st.isolate->IsExecutionTerminating()) goto fail; 650 | if (platform->IdleTasksEnabled(st.isolate)) { 651 | double idle_time_in_seconds = 1.0 / 50; 652 | v8::platform::RunIdleTasks(platform, st.isolate, idle_time_in_seconds); 653 | if (st.isolate->IsExecutionTerminating()) goto fail; 654 | if (try_catch.HasCaught()) goto fail; 655 | } 656 | fail: 657 | if (st.isolate->IsExecutionTerminating()) { 658 | st.isolate->CancelTerminateExecution(); 659 | st.err_reason = NO_ERROR; 660 | } 661 | auto result = v8::Boolean::New(st.isolate, ran_task); 662 | reply_retry(st, result); 663 | } 664 | 665 | int snapshot(bool is_warmup, bool verbose_exceptions, 666 | const v8::String::Utf8Value& code, 667 | v8::StartupData blob, v8::StartupData *result, 668 | char (*errbuf)[512]) 669 | { 670 | // SnapshotCreator takes ownership of isolate 671 | v8::Isolate *isolate = v8::Isolate::Allocate(); 672 | v8::StartupData *existing_blob = is_warmup ? &blob : nullptr; 673 | v8::SnapshotCreator snapshot_creator(isolate, nullptr, existing_blob); 674 | v8::Isolate::Scope isolate_scope(isolate); 675 | v8::HandleScope handle_scope(isolate); 676 | v8::TryCatch try_catch(isolate); 677 | try_catch.SetVerbose(verbose_exceptions); 678 | auto filename = is_warmup 679 | ? v8::String::NewFromUtf8Literal(isolate, "") 680 | : v8::String::NewFromUtf8Literal(isolate, ""); 681 | auto mode = is_warmup 682 | ? v8::SnapshotCreator::FunctionCodeHandling::kKeep 683 | : v8::SnapshotCreator::FunctionCodeHandling::kClear; 684 | int cause = INTERNAL_ERROR; 685 | { 686 | auto context = v8::Context::New(isolate); 687 | v8::Context::Scope context_scope(context); 688 | v8::Local source; 689 | auto type = v8::NewStringType::kNormal; 690 | if (!v8::String::NewFromUtf8(isolate, *code, type, code.length()).ToLocal(&source)) { 691 | v8::String::Utf8Value s(isolate, try_catch.Exception()); 692 | if (*s) snprintf(*errbuf, sizeof(*errbuf), "%c%s", cause, *s); 693 | goto fail; 694 | } 695 | v8::ScriptOrigin origin(filename); 696 | v8::Local script; 697 | cause = PARSE_ERROR; 698 | if (!v8::Script::Compile(context, source, &origin).ToLocal(&script)) { 699 | goto err; 700 | } 701 | cause = RUNTIME_ERROR; 702 | if (script->Run(context).IsEmpty()) { 703 | err: 704 | auto m = try_catch.Message(); 705 | v8::String::Utf8Value s(isolate, m->Get()); 706 | v8::String::Utf8Value name(isolate, m->GetScriptResourceName()); 707 | auto line = m->GetLineNumber(context).FromMaybe(0); 708 | auto column = m->GetStartColumn(context).FromMaybe(0); 709 | snprintf(*errbuf, sizeof(*errbuf), "%c%s\n%s:%d:%d", 710 | cause, *s, *name, line, column); 711 | goto fail; 712 | } 713 | cause = INTERNAL_ERROR; 714 | if (!is_warmup) snapshot_creator.SetDefaultContext(context); 715 | } 716 | if (is_warmup) { 717 | isolate->ContextDisposedNotification(false); 718 | auto context = v8::Context::New(isolate); 719 | snapshot_creator.SetDefaultContext(context); 720 | } 721 | *result = snapshot_creator.CreateBlob(mode); 722 | cause = NO_ERROR; 723 | fail: 724 | return cause; 725 | } 726 | 727 | // response is errback [result, err] array 728 | // note: currently needs --stress_snapshot in V8 debug builds 729 | // to work around a buggy check in the snapshot deserializer 730 | extern "C" void v8_snapshot(State *pst, const uint8_t *p, size_t n) 731 | { 732 | State& st = *pst; 733 | v8::TryCatch try_catch(st.isolate); 734 | try_catch.SetVerbose(st.verbose_exceptions); 735 | v8::HandleScope handle_scope(st.isolate); 736 | v8::ValueDeserializer des(st.isolate, p, n); 737 | des.ReadHeader(st.context).Check(); 738 | v8::Local result; 739 | v8::StartupData blob{nullptr, 0}; 740 | int cause = INTERNAL_ERROR; 741 | char errbuf[512] = {0}; 742 | { 743 | v8::Local code_v; 744 | if (!des.ReadValue(st.context).ToLocal(&code_v)) goto fail; 745 | v8::String::Utf8Value code(st.isolate, code_v); 746 | if (!*code) goto fail; 747 | v8::StartupData init{nullptr, 0}; 748 | cause = snapshot(/*is_warmup*/false, st.verbose_exceptions, code, init, &blob, &errbuf); 749 | if (cause) goto fail; 750 | } 751 | if (blob.data) { 752 | auto data = reinterpret_cast(blob.data); 753 | auto type = v8::NewStringType::kNormal; 754 | bool ok = v8::String::NewFromOneByte(st.isolate, data, type, 755 | blob.raw_size).ToLocal(&result); 756 | delete[] blob.data; 757 | blob = v8::StartupData{nullptr, 0}; 758 | if (!ok) goto fail; 759 | } 760 | cause = NO_ERROR; 761 | fail: 762 | if (st.isolate->IsExecutionTerminating()) { 763 | st.isolate->CancelTerminateExecution(); 764 | cause = st.err_reason ? st.err_reason : TERMINATED_ERROR; 765 | st.err_reason = NO_ERROR; 766 | } 767 | if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR; 768 | if (cause) result = v8::Undefined(st.isolate); 769 | v8::Local err; 770 | if (*errbuf) { 771 | if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) { 772 | err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error"); 773 | } 774 | } else { 775 | err = to_error(st, &try_catch, cause); 776 | } 777 | if (!reply(st, result, err)) { 778 | assert(try_catch.HasCaught()); 779 | goto fail; // retry; can be termination exception 780 | } 781 | } 782 | 783 | extern "C" void v8_warmup(State *pst, const uint8_t *p, size_t n) 784 | { 785 | State& st = *pst; 786 | v8::TryCatch try_catch(st.isolate); 787 | try_catch.SetVerbose(st.verbose_exceptions); 788 | v8::HandleScope handle_scope(st.isolate); 789 | std::vector storage; 790 | v8::ValueDeserializer des(st.isolate, p, n); 791 | des.ReadHeader(st.context).Check(); 792 | v8::Local result; 793 | v8::StartupData blob{nullptr, 0}; 794 | int cause = INTERNAL_ERROR; 795 | char errbuf[512] = {0}; 796 | { 797 | v8::Local request_v; 798 | if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail; 799 | v8::Local request; // [snapshot, warmup_code] 800 | if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail; 801 | v8::Local blob_data_v; 802 | if (!request->Get(st.context, 0).ToLocal(&blob_data_v)) goto fail; 803 | v8::Local blob_data; 804 | if (!blob_data_v->ToString(st.context).ToLocal(&blob_data)) goto fail; 805 | assert(blob_data->IsOneByte()); 806 | assert(blob_data->ContainsOnlyOneByte()); 807 | if (const size_t len = blob_data->Length()) { 808 | auto flags = v8::String::NO_NULL_TERMINATION 809 | | v8::String::PRESERVE_ONE_BYTE_NULL; 810 | storage.resize(len); 811 | blob_data->WriteOneByte(st.isolate, storage.data(), 0, len, flags); 812 | } 813 | v8::Local code_v; 814 | if (!request->Get(st.context, 1).ToLocal(&code_v)) goto fail; 815 | v8::String::Utf8Value code(st.isolate, code_v); 816 | if (!*code) goto fail; 817 | auto data = reinterpret_cast(storage.data()); 818 | auto size = static_cast(storage.size()); 819 | v8::StartupData init{data, size}; 820 | cause = snapshot(/*is_warmup*/true, st.verbose_exceptions, code, init, &blob, &errbuf); 821 | if (cause) goto fail; 822 | } 823 | if (blob.data) { 824 | auto data = reinterpret_cast(blob.data); 825 | auto type = v8::NewStringType::kNormal; 826 | bool ok = v8::String::NewFromOneByte(st.isolate, data, type, 827 | blob.raw_size).ToLocal(&result); 828 | delete[] blob.data; 829 | blob = v8::StartupData{nullptr, 0}; 830 | if (!ok) goto fail; 831 | } 832 | cause = NO_ERROR; 833 | fail: 834 | if (st.isolate->IsExecutionTerminating()) { 835 | st.isolate->CancelTerminateExecution(); 836 | cause = st.err_reason ? st.err_reason : TERMINATED_ERROR; 837 | st.err_reason = NO_ERROR; 838 | } 839 | if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR; 840 | if (cause) result = v8::Undefined(st.isolate); 841 | v8::Local err; 842 | if (*errbuf) { 843 | if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) { 844 | err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error"); 845 | } 846 | } else { 847 | err = to_error(st, &try_catch, cause); 848 | } 849 | if (!reply(st, result, err)) { 850 | assert(try_catch.HasCaught()); 851 | goto fail; // retry; can be termination exception 852 | } 853 | } 854 | 855 | extern "C" void v8_low_memory_notification(State *pst) 856 | { 857 | pst->isolate->LowMemoryNotification(); 858 | } 859 | 860 | // called from ruby thread 861 | extern "C" void v8_terminate_execution(State *pst) 862 | { 863 | pst->isolate->TerminateExecution(); 864 | } 865 | 866 | extern "C" void v8_single_threaded_enter(State *pst, Context *c, void (*f)(Context *c)) 867 | { 868 | State& st = *pst; 869 | v8::Locker locker(st.isolate); 870 | v8::Isolate::Scope isolate_scope(st.isolate); 871 | v8::HandleScope handle_scope(st.isolate); 872 | { 873 | st.safe_context_function = v8::Local::New(st.isolate, st.persistent_safe_context_function); 874 | st.safe_context = v8::Local::New(st.isolate, st.persistent_safe_context); 875 | st.context = v8::Local::New(st.isolate, st.persistent_context); 876 | v8::Context::Scope context_scope(st.context); 877 | f(c); 878 | st.context = v8::Local(); 879 | st.safe_context = v8::Local(); 880 | st.safe_context_function = v8::Local(); 881 | } 882 | } 883 | 884 | extern "C" void v8_single_threaded_dispose(struct State *pst) 885 | { 886 | delete pst; // see State::~State() below 887 | } 888 | 889 | } // namespace anonymous 890 | 891 | State::~State() 892 | { 893 | { 894 | v8::Locker locker(isolate); 895 | v8::Isolate::Scope isolate_scope(isolate); 896 | persistent_safe_context.Reset(); 897 | persistent_context.Reset(); 898 | } 899 | isolate->Dispose(); 900 | for (Callback *cb : callbacks) 901 | delete cb; 902 | } 903 | -------------------------------------------------------------------------------- /ext/mini_racer_extension/mini_racer_v8.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #ifdef __cplusplus 6 | extern "C" { 7 | #endif 8 | 9 | enum 10 | { 11 | NO_ERROR = '\0', 12 | INTERNAL_ERROR = 'I', 13 | MEMORY_ERROR = 'M', 14 | PARSE_ERROR = 'P', 15 | RUNTIME_ERROR = 'R', 16 | TERMINATED_ERROR = 'T', 17 | }; 18 | 19 | static const uint16_t js_function_marker[] = {0xBFF,'J','a','v','a','S','c','r','i','p','t','F','u','n','c','t','i','o','n'}; 20 | 21 | // defined in mini_racer_extension.c, opaque to mini_racer_v8.cc 22 | struct Context; 23 | 24 | // defined in mini_racer_v8.cc, opaque to mini_racer_extension.c 25 | struct State; 26 | 27 | // defined in mini_racer_extension.c 28 | extern int single_threaded; 29 | void v8_get_flags(char **p, size_t *n); 30 | void v8_thread_main(struct Context *c, struct State *pst); 31 | void v8_dispatch(struct Context *c); 32 | void v8_reply(struct Context *c, const uint8_t *p, size_t n); 33 | void v8_roundtrip(struct Context *c, const uint8_t **p, size_t *n); 34 | 35 | // defined in mini_racer_v8.cc 36 | void v8_global_init(void); 37 | struct State *v8_thread_init(struct Context *c, const uint8_t *snapshot_buf, 38 | size_t snapshot_len, int64_t max_memory, 39 | int verbose_exceptions); // calls v8_thread_main 40 | void v8_attach(struct State *pst, const uint8_t *p, size_t n); 41 | void v8_call(struct State *pst, const uint8_t *p, size_t n); 42 | void v8_eval(struct State *pst, const uint8_t *p, size_t n); 43 | void v8_heap_stats(struct State *pst); 44 | void v8_heap_snapshot(struct State *pst); 45 | void v8_pump_message_loop(struct State *pst); 46 | void v8_snapshot(struct State *pst, const uint8_t *p, size_t n); 47 | void v8_warmup(struct State *pst, const uint8_t *p, size_t n); 48 | void v8_low_memory_notification(struct State *pst); 49 | void v8_terminate_execution(struct State *pst); // called from ruby or watchdog thread 50 | void v8_single_threaded_enter(struct State *pst, struct Context *c, void (*f)(struct Context *c)); 51 | void v8_single_threaded_dispose(struct State *pst); 52 | 53 | #ifdef __cplusplus 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /ext/mini_racer_extension/serde.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static void des_null(void *arg); 10 | static void des_undefined(void *arg); 11 | static void des_bool(void *arg, int v); 12 | static void des_int(void *arg, int64_t v); 13 | static void des_num(void *arg, double v); 14 | static void des_date(void *arg, double v); 15 | // des_bigint: |p| points to |n|/8 quadwords in little-endian order 16 | // des_bigint: |p| is not quadword aligned 17 | // des_bigint: |n| is in bytes, not quadwords 18 | // des_bigint: |n| is zero when bigint is zero 19 | // des_bigint: |sign| is 1 or -1 20 | static void des_bigint(void *arg, const void *p, size_t n, int sign); 21 | static void des_string(void *arg, const char *s, size_t n); 22 | static void des_string8(void *arg, const uint8_t *s, size_t n); 23 | // des_string16: |s| is not word aligned 24 | // des_string16: |n| is in bytes, not code points 25 | static void des_string16(void *arg, const void *s, size_t n); 26 | static void des_arraybuffer(void *arg, const void *s, size_t n); 27 | static void des_array_begin(void *arg); 28 | static void des_array_end(void *arg); 29 | // called if e.g. an array object has named properties 30 | static void des_named_props_begin(void *arg); 31 | static void des_named_props_end(void *arg); 32 | static void des_object_begin(void *arg); 33 | static void des_object_end(void *arg); 34 | static void des_map_begin(void *arg); 35 | static void des_map_end(void *arg); 36 | static void des_object_ref(void *arg, uint32_t id); 37 | // des_error_begin: followed by des_object_begin + des_object_end calls 38 | static void des_error_begin(void *arg); 39 | static void des_error_end(void *arg); 40 | 41 | // dynamically sized buffer with inline storage so we don't 42 | // have to worry about allocation failures for small payloads 43 | typedef struct Buf { 44 | uint8_t *buf; 45 | uint32_t len, cap; 46 | uint8_t buf_s[48]; 47 | } Buf; 48 | 49 | typedef struct Ser { 50 | Buf b; 51 | char err[64]; 52 | } Ser; 53 | 54 | static const uint8_t the_nan[8] = {0,0,0,0,0,0,0xF8,0x7F}; // canonical nan 55 | 56 | // note: returns |v| if v in [0,1,2] 57 | static inline uint32_t next_power_of_two(uint32_t v) { 58 | v -= 1; 59 | v |= v >> 1; 60 | v |= v >> 2; 61 | v |= v >> 4; 62 | v |= v >> 8; 63 | v |= v >> 16; 64 | v += 1; 65 | return v; 66 | } 67 | 68 | static inline void buf_init(Buf *b) 69 | { 70 | b->len = 0; 71 | b->buf = b->buf_s; 72 | b->cap = sizeof(b->buf_s); 73 | } 74 | 75 | static inline void buf_reset(Buf *b) 76 | { 77 | if (b->buf != b->buf_s) 78 | free(b->buf); 79 | buf_init(b); 80 | } 81 | 82 | static inline void buf_move(Buf *s, Buf *d) 83 | { 84 | if (s == d) 85 | return; 86 | *d = *s; 87 | if (s->buf == s->buf_s) 88 | d->buf = d->buf_s; 89 | buf_init(s); 90 | } 91 | 92 | static inline int buf_grow(Buf *b, size_t n) 93 | { 94 | void *p; 95 | 96 | if ((uint64_t)n + b->len > UINT32_MAX) 97 | return -1; 98 | n += b->len; 99 | if (n < b->cap) 100 | return 0; 101 | n = next_power_of_two(n); 102 | p = NULL; 103 | if (b->buf != b->buf_s) 104 | p = b->buf; 105 | p = realloc(p, n); 106 | if (!p) 107 | return -1; 108 | if (b->buf == b->buf_s) 109 | memcpy(p, b->buf_s, b->len); 110 | b->buf = p; 111 | b->cap = n; 112 | return 0; 113 | } 114 | 115 | static inline int buf_put(Buf *b, const void *p, size_t n) 116 | { 117 | if (n == 0) 118 | return 0; 119 | if (buf_grow(b, n)) 120 | return -1; 121 | memcpy(&b->buf[b->len], p, n); 122 | b->len += n; 123 | return 0; 124 | } 125 | 126 | static inline int buf_putc(Buf *b, uint8_t c) 127 | { 128 | return buf_put(b, &c, 1); 129 | } 130 | 131 | static inline void w(Ser *s, const void *p, size_t n) 132 | { 133 | if (*s->err) 134 | return; 135 | if (buf_put(&s->b, p, n)) 136 | snprintf(s->err, sizeof(s->err), "out of memory"); 137 | } 138 | 139 | static inline void w_byte(Ser *s, uint8_t c) 140 | { 141 | w(s, &c, 1); 142 | } 143 | 144 | static inline void w_varint(Ser *s, uint64_t v) 145 | { 146 | uint8_t b[10]; // 10 == 1 + 64/7 147 | size_t n; 148 | 149 | for (n = 0; v > 127; v >>= 7) 150 | b[n++] = 128 | (v & 127); 151 | b[n++] = v; 152 | w(s, b, n); 153 | } 154 | 155 | static inline void w_zigzag(Ser *s, int64_t v) 156 | { 157 | uint64_t t; 158 | 159 | if (v < 0) { 160 | t = -v; 161 | } else { 162 | t = v; 163 | } 164 | t += t; 165 | t -= (v < 0); 166 | w_varint(s, t); 167 | } 168 | 169 | static inline int r_varint(const uint8_t **p, const uint8_t *pe, uint64_t *r) 170 | { 171 | int i, k; 172 | 173 | for (i = 0; i < 5; i++) { 174 | if (*p+i == pe) 175 | return -1; 176 | if ((*p)[i] < 128) 177 | goto ok; 178 | } 179 | return -1; 180 | ok: 181 | *r = 0; 182 | for (k = 0; k <= i; k++, (*p)++) 183 | *r |= (uint64_t)(**p & 127) << 7*k; 184 | return 0; 185 | } 186 | 187 | static inline int r_zigzag(const uint8_t **p, const uint8_t *pe, int64_t *r) 188 | { 189 | uint64_t v; 190 | 191 | if (r_varint(p, pe, &v)) 192 | return -1; 193 | *r = v&1 ? -(v/2)-1 : v/2; 194 | return 0; 195 | } 196 | 197 | static inline void ser_init(Ser *s) 198 | { 199 | memset(s, 0, sizeof(*s)); 200 | buf_init(&s->b); 201 | w(s, "\xFF\x0F", 2); 202 | } 203 | 204 | static void ser_init1(Ser *s, uint8_t c) 205 | { 206 | memset(s, 0, sizeof(*s)); 207 | buf_init(&s->b); 208 | w_byte(s, c); 209 | w(s, "\xFF\x0F", 2); 210 | } 211 | 212 | static void ser_reset(Ser *s) 213 | { 214 | buf_reset(&s->b); 215 | } 216 | 217 | static void ser_null(Ser *s) 218 | { 219 | w_byte(s, '0'); 220 | } 221 | 222 | static void ser_undefined(Ser *s) 223 | { 224 | w_byte(s, '_'); 225 | } 226 | 227 | static void ser_bool(Ser *s, int v) 228 | { 229 | w_byte(s, "TF"[!v]); 230 | } 231 | 232 | static void ser_num(Ser *s, double v) 233 | { 234 | w_byte(s, 'N'); 235 | if (isnan(v)) { 236 | w(s, the_nan, sizeof(the_nan)); 237 | } else { 238 | w(s, &v, sizeof(v)); 239 | } 240 | } 241 | 242 | // ser_bigint: |n| is in bytes, not quadwords 243 | static void ser_bigint(Ser *s, const uint64_t *p, size_t n, int sign) 244 | { 245 | if (*s->err) 246 | return; 247 | if (n % 8) { 248 | snprintf(s->err, sizeof(s->err), "bad bigint"); 249 | return; 250 | } 251 | w_byte(s, 'Z'); 252 | // chop off high all-zero words 253 | n /= 8; 254 | while (n--) 255 | if (p[n]) 256 | break; 257 | if (n == (size_t)-1) { 258 | w_byte(s, 0); // normalized zero 259 | } else { 260 | n = 8*n + 8; 261 | w_varint(s, 2*n + (sign < 0)); 262 | w(s, p, n); 263 | } 264 | } 265 | 266 | static void ser_int(Ser *s, int64_t v) 267 | { 268 | uint64_t t; 269 | int sign; 270 | 271 | if (*s->err) 272 | return; 273 | if (v < INT32_MIN || v > INT32_MAX) { 274 | if (v > INT64_MIN/1024) 275 | if (v <= INT64_MAX/1024) 276 | return ser_num(s, v); 277 | t = v < 0 ? -v : v; 278 | sign = v < 0 ? -1 : 1; 279 | ser_bigint(s, &t, sizeof(t), sign); 280 | } else { 281 | w_byte(s, 'I'); 282 | w_zigzag(s, v); 283 | } 284 | } 285 | 286 | // |v| is the timestamp in milliseconds since the UNIX epoch 287 | static void ser_date(Ser *s, double v) 288 | { 289 | w_byte(s, 'D'); 290 | if (isfinite(v)) { 291 | w(s, &v, sizeof(v)); 292 | } else { 293 | w(s, the_nan, sizeof(the_nan)); 294 | } 295 | } 296 | 297 | // string must be utf8 298 | static void ser_string(Ser *s, const char *p, size_t n) 299 | { 300 | w_byte(s, 'S'); 301 | w_varint(s, n); 302 | w(s, p, n); 303 | } 304 | 305 | // string must be latin1 306 | static void ser_string8(Ser *s, const uint8_t *p, size_t n) 307 | { 308 | w_byte(s, '"'); 309 | w_varint(s, n); 310 | w(s, p, n); 311 | } 312 | 313 | // string must be utf16le; |n| is in bytes, not code points 314 | static void ser_string16(Ser *s, const void *p, size_t n) 315 | { 316 | w_byte(s, 'c'); 317 | w_varint(s, n); 318 | w(s, p, n); 319 | } 320 | 321 | static void ser_object_begin(Ser *s) 322 | { 323 | w_byte(s, 'o'); 324 | } 325 | 326 | // |count| is the property count 327 | static void ser_object_end(Ser *s, uint32_t count) 328 | { 329 | w_byte(s, '{'); 330 | w_varint(s, count); 331 | } 332 | 333 | static void ser_object_ref(Ser *s, uint32_t id) 334 | { 335 | w_byte(s, '^'); 336 | w_varint(s, id); 337 | } 338 | 339 | static void ser_array_begin(Ser *s, uint32_t count) 340 | { 341 | w_byte(s, 'A'); // 'A'=dense, 'a'=sparse 342 | w_varint(s, count); // element count 343 | } 344 | 345 | // |count| is the element count 346 | static void ser_array_end(Ser *s, uint32_t count) 347 | { 348 | w_byte(s, '$'); 349 | w_varint(s, 0); // property count, always zero 350 | w_varint(s, count); // element count 351 | } 352 | 353 | static int bail(char (*err)[64], const char *str) 354 | { 355 | snprintf(*err, sizeof(*err), "%s", str); 356 | return -1; 357 | } 358 | 359 | static int des1_num(const uint8_t **p, const uint8_t *pe, double *d) 360 | { 361 | if (pe-*p < (int)sizeof(*d)) 362 | return -1; 363 | memcpy(d, *p, sizeof(*d)); 364 | *p += sizeof(*d); 365 | if (isnan(*d)) 366 | memcpy(d, the_nan, sizeof(the_nan)); 367 | return 0; 368 | } 369 | 370 | static int des1(char (*err)[64], const uint8_t **p, const uint8_t *pe, 371 | void *arg, int depth) 372 | { 373 | uint64_t s, t, u; 374 | uint8_t c; 375 | int64_t i; 376 | double d; 377 | 378 | if (depth < 0) 379 | return bail(err, "too much recursion"); 380 | again: 381 | if (*p >= pe) 382 | goto too_short; 383 | switch ((c = *(*p)++)) { 384 | default: 385 | if (c > 32 && c < 127) { 386 | snprintf(*err, sizeof(*err), "bad tag: %c", c); 387 | } else { 388 | snprintf(*err, sizeof(*err), "bad tag: %02x", c); 389 | } 390 | return -1; 391 | case '\0': // skip alignment padding for two-byte strings 392 | if (*p < pe) 393 | goto again; 394 | break; 395 | case '^': 396 | if (r_varint(p, pe, &u)) 397 | goto bad_varint; 398 | des_object_ref(arg, u); 399 | // object refs can (but need not be) followed by a typed array 400 | // that is a view over the arraybufferview 401 | goto typed_array; 402 | case '0': 403 | des_null(arg); 404 | break; 405 | case '_': 406 | des_undefined(arg); 407 | break; 408 | case 'A': // dense array 409 | if (r_varint(p, pe, &u)) 410 | goto bad_varint; 411 | t = u; 412 | des_array_begin(arg); 413 | while (u--) { 414 | if (*p >= pe) 415 | goto too_short; 416 | // '-' is 'the hole', a marker for representing absent 417 | // elements that is inserted when a dense array turns 418 | // sparse during serialization; we replace it with undefined 419 | if (**p == '-') { 420 | (*p)++; 421 | des_undefined(arg); 422 | } else { 423 | if (des1(err, p, pe, arg, depth-1)) 424 | return -1; 425 | } 426 | } 427 | for (s = 0; /*empty*/; s++) { 428 | if (*p >= pe) 429 | goto too_short; 430 | if (**p == '$') 431 | break; 432 | if (s < 1) 433 | des_named_props_begin(arg); 434 | if (des1(err, p, pe, arg, depth-1)) // key 435 | return -1; 436 | if (des1(err, p, pe, arg, depth-1)) // value 437 | return -1; 438 | } 439 | (*p)++; 440 | if (s > 0) 441 | des_named_props_end(arg); 442 | if (r_varint(p, pe, &u)) 443 | goto bad_varint; 444 | if (s != u) 445 | return bail(err, "array property count mismatch"); 446 | if (r_varint(p, pe, &u)) 447 | goto bad_varint; 448 | if (t != u) 449 | return bail(err, "array element count mismatch"); 450 | des_array_end(arg); 451 | break; 452 | case 'B': // arraybuffer 453 | case '~': // resizable arraybuffer (RAB) 454 | if (r_varint(p, pe, &u)) 455 | goto bad_varint; 456 | if (c == '~') 457 | if (r_varint(p, pe, &t)) // maxByteLength, unused 458 | goto bad_varint; 459 | if (pe-*p < (int64_t)u) 460 | goto too_short; 461 | des_arraybuffer(arg, *p, u); 462 | *p += u; 463 | // arraybuffers can (but need not be) followed by a typed array 464 | // that is a view over the arraybufferview 465 | // typed arrays aren't efficiently representable in ruby, and the 466 | // concept of a memory view is wholly unrepresentable, so we 467 | // simply skip over them; callers get just the arraybuffer 468 | typed_array: 469 | if (pe-*p < 2) 470 | break; 471 | if (**p != 'V') 472 | break; 473 | (*p)++; 474 | c = *(*p)++; 475 | // ? DataView 476 | // B Uint8Array 477 | // C Uint8ClampedArray 478 | // D Uint32Array 479 | // F Float64Array 480 | // Q BigUint64Array 481 | // W Uint16Array 482 | // b Int8Array 483 | // d Int32Array 484 | // f Float32Array 485 | // h Float16Array 486 | // q BigInt64Array 487 | // w Int16Array 488 | if (!strchr("?BCDFQWbdfhqw", **p)) 489 | return bail(err, "bad typed array"); 490 | if (r_varint(p, pe, &t)) // byteOffset 491 | goto bad_varint; 492 | if (r_varint(p, pe, &t)) // byteLength 493 | goto bad_varint; 494 | if (r_varint(p, pe, &t)) // flags, only non-zero when backed by RAB 495 | goto bad_varint; 496 | break; 497 | case 'a': // sparse array 498 | // total element count; ignored because we drop sparse entries 499 | if (r_varint(p, pe, &t)) 500 | goto bad_varint; 501 | des_array_begin(arg); 502 | for (u = s = 0;;) { 503 | if (*p >= pe) 504 | goto too_short; 505 | c = **p; 506 | if (c == '@') 507 | break; 508 | if (c == 'I' && !s) { 509 | u++, (*p)++; 510 | if (r_zigzag(p, pe, &i)) // array index, ignored 511 | goto bad_varint; 512 | if (des1(err, p, pe, arg, depth-1)) 513 | return -1; 514 | } else { 515 | if (!s++) 516 | des_named_props_begin(arg); 517 | if (des1(err, p, pe, arg, depth-1)) // key 518 | return -1; 519 | if (des1(err, p, pe, arg, depth-1)) // value 520 | return -1; 521 | } 522 | } 523 | (*p)++; 524 | if (s > 0) 525 | des_named_props_end(arg); 526 | if (r_varint(p, pe, &t)) 527 | goto bad_varint; 528 | if (t != u+s) 529 | return bail(err, "element count mismatch"); 530 | // total element count; ignored because we drop sparse entries 531 | if (r_varint(p, pe, &t)) 532 | goto bad_varint; 533 | des_array_end(arg); 534 | break; 535 | case 'D': 536 | if (des1_num(p, pe, &d)) 537 | goto too_short; 538 | des_date(arg, d); 539 | break; 540 | case 'F': // primitive boolean 541 | case 'x': // new Boolean(...) 542 | des_bool(arg, 0); 543 | break; 544 | case 'T': // primitive boolean 545 | case 'y': // new Boolean(...) 546 | des_bool(arg, 1); 547 | break; 548 | case 'I': 549 | if (r_zigzag(p, pe, &i)) 550 | goto bad_varint; 551 | des_int(arg, i); 552 | break; 553 | case 'N': // primitive number 554 | case 'n': // new Number(...) 555 | if (des1_num(p, pe, &d)) 556 | goto too_short; 557 | des_num(arg, d); 558 | break; 559 | case 'Z': 560 | if (r_varint(p, pe, &u)) 561 | goto bad_varint; 562 | t = u & 1; 563 | u = u >> 1; 564 | if (u & 7) 565 | return bail(err, "bad bigint"); 566 | // V8's serializer never emits -0n; 567 | // its deserializer rejects it with DataCloneError 568 | if (t && !u) 569 | return bail(err, "negative zero bigint"); 570 | if (pe-*p < (int64_t)u) 571 | goto too_short; 572 | des_bigint(arg, *p, u, 1-2*t); 573 | *p += u; 574 | break; 575 | case 'R': // RegExp, deserialized as string 576 | if (*p >= pe) 577 | goto too_short; 578 | switch (**p) { 579 | default: 580 | return bail(err, "bad regexp"); 581 | case '"': 582 | case 'S': 583 | case 'c': 584 | break; 585 | } 586 | if (des1(err, p, pe, arg, depth-1)) // pattern 587 | return -1; 588 | if (r_varint(p, pe, &t)) // flags; ignored 589 | goto bad_varint; 590 | break; 591 | case 's': // string object, decoded as primitive string 592 | if (*p >= pe) 593 | goto too_short; 594 | switch (*(*p)++) { 595 | case '"': 596 | goto s_string8; 597 | case 'S': 598 | goto s_string; 599 | case 'c': 600 | goto s_string16; 601 | } 602 | return bail(err, "bad string object"); 603 | case '"': // ascii/latin1 604 | s_string8: 605 | if (r_varint(p, pe, &u)) 606 | goto bad_varint; 607 | if (pe-*p < (int64_t)u) 608 | goto too_short; 609 | des_string8(arg, *p, u); 610 | *p += u; 611 | break; 612 | case 'S': // utf8 613 | s_string: 614 | if (r_varint(p, pe, &u)) 615 | goto bad_varint; 616 | if (pe-*p < (int64_t)u) 617 | goto too_short; 618 | des_string(arg, (void *)*p, u); 619 | *p += u; 620 | break; 621 | case 'c': // utf16-le 622 | s_string16: 623 | if (r_varint(p, pe, &u)) 624 | goto bad_varint; 625 | if (pe-*p < (int64_t)u) 626 | goto too_short; 627 | if (u & 1) 628 | return bail(err, "bad utf16 string size"); 629 | des_string16(arg, *p, u); 630 | *p += u; 631 | break; 632 | case 'o': 633 | des_object_begin(arg); 634 | for (u = 0;; u++) { 635 | if (pe-*p < 1) 636 | goto too_short; 637 | if (**p == '{') 638 | break; 639 | if (des1(err, p, pe, arg, depth-1)) // key 640 | return -1; 641 | if (des1(err, p, pe, arg, depth-1)) // value 642 | return -1; 643 | } 644 | (*p)++; 645 | if (r_varint(p, pe, &t)) 646 | goto bad_varint; 647 | if (t != u) 648 | return bail(err, "object properties count mismatch"); 649 | des_object_end(arg); 650 | break; 651 | case ';': // Map 652 | des_map_begin(arg); 653 | for (u = 0; /*empty*/; u++) { 654 | if (*p >= pe) 655 | goto too_short; 656 | if (**p == ':') 657 | break; 658 | if (des1(err, p, pe, arg, depth-1)) // key 659 | return -1; 660 | if (des1(err, p, pe, arg, depth-1)) // value 661 | return -1; 662 | } 663 | (*p)++; 664 | if (r_varint(p, pe, &t)) 665 | goto bad_varint; 666 | if (t != 2*u) 667 | return bail(err, "map element count mismatch"); 668 | des_map_end(arg); 669 | break; 670 | case '\'': // Set 671 | des_array_begin(arg); 672 | for (u = 0; /*empty*/; u++) { 673 | if (*p >= pe) 674 | goto too_short; 675 | if (**p == ',') 676 | break; 677 | if (des1(err, p, pe, arg, depth-1)) // value 678 | return -1; 679 | } 680 | (*p)++; 681 | if (r_varint(p, pe, &t)) 682 | goto bad_varint; 683 | if (t != u) 684 | return bail(err, "set element count mismatch"); 685 | des_array_end(arg); 686 | break; 687 | case 'r': 688 | // shortest error is /r[.]/ - Error with no message, cause, or stack 689 | // longest error is /r[EFRSTU]mcs[.]/ where 690 | // EFRSTU is one of {Eval,Reference,Range,Syntax,Type,URI}Error 691 | des_error_begin(arg); 692 | des_object_begin(arg); 693 | if (*p >= pe) 694 | goto too_short; 695 | c = *(*p)++; 696 | if (!strchr("EFRSTU", c)) 697 | goto r_message; 698 | if (*p >= pe) 699 | goto too_short; 700 | c = *(*p)++; 701 | r_message: 702 | if (c != 'm') 703 | goto r_stack; 704 | des_string(arg, "message", sizeof("message")-1); 705 | if (*p >= pe) 706 | goto too_short; 707 | if (!strchr("\"Sc", **p)) 708 | return bail(err, "error .message is not a string"); 709 | if (des1(err, p, pe, arg, depth-1)) 710 | return -1; 711 | if (*p >= pe) 712 | goto too_short; 713 | c = *(*p)++; 714 | r_stack: 715 | if (c != 's') 716 | goto r_cause; 717 | des_string(arg, "stack", sizeof("stack")-1); 718 | if (*p >= pe) 719 | goto too_short; 720 | if (!strchr("\"Sc", **p)) 721 | return bail(err, "error .stack is not a string"); 722 | if (des1(err, p, pe, arg, depth-1)) 723 | return -1; 724 | if (*p >= pe) 725 | goto too_short; 726 | c = *(*p)++; 727 | r_cause: 728 | if (c != 'c') 729 | goto r_end; 730 | des_string(arg, "cause", sizeof("cause")-1); 731 | if (des1(err, p, pe, arg, depth-1)) 732 | return -1; 733 | if (*p >= pe) 734 | goto too_short; 735 | c = *(*p)++; 736 | r_end: 737 | if (c != '.') 738 | return bail(err, "bad error object"); 739 | des_object_end(arg); 740 | des_error_end(arg); 741 | break; 742 | } 743 | return 0; 744 | too_short: 745 | return bail(err, "input too short"); 746 | bad_varint: 747 | return bail(err, "bad varint"); 748 | } 749 | 750 | int des(char (*err)[64], const void *b, size_t n, void *arg) 751 | { 752 | const uint8_t *p, *pe; 753 | 754 | p = b, pe = p + n; 755 | if (n < 2) 756 | return bail(err, "input too short"); 757 | if (*p++ != 255) 758 | return bail(err, "bad header"); 759 | if (*p++ != 15) 760 | return bail(err, "bad version"); 761 | while (p < pe) 762 | if (des1(err, &p, pe, arg, /*depth*/96)) 763 | return -1; 764 | return 0; 765 | } 766 | -------------------------------------------------------------------------------- /ext/mini_racer_loader/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | if RUBY_ENGINE == "truffleruby" 4 | File.write("Makefile", dummy_makefile($srcdir).join("")) 5 | return 6 | end 7 | 8 | extension_name = 'mini_racer_loader' 9 | dir_config extension_name 10 | 11 | $CXXFLAGS += " -fvisibility=hidden " 12 | 13 | create_makefile extension_name 14 | -------------------------------------------------------------------------------- /ext/mini_racer_loader/mini_racer_loader.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // Load a Ruby extension like Ruby does, only with flags that: 8 | // a) hide symbols from other extensions (RTLD_LOCAL) 9 | // b) bind symbols tightly (RTLD_DEEPBIND, when available) 10 | 11 | void Init_mini_racer_loader(void); 12 | 13 | static void *_dln_load(const char *file); 14 | 15 | static VALUE _load_shared_lib(VALUE self, volatile VALUE fname) 16 | { 17 | (void) self; 18 | 19 | // check that path is not tainted 20 | SafeStringValue(fname); 21 | 22 | FilePathValue(fname); 23 | VALUE path = rb_str_encode_ospath(fname); 24 | 25 | char *loc = StringValueCStr(path); 26 | void *handle = _dln_load(loc); 27 | 28 | return handle ? Qtrue : Qfalse; 29 | } 30 | 31 | // adapted from Ruby's dln.c 32 | #define INIT_FUNC_PREFIX ((char[]) {'I', 'n', 'i', 't', '_'}) 33 | #define INIT_FUNCNAME(buf, file) do { \ 34 | const char *base = (file); \ 35 | const size_t flen = _init_funcname(&base); \ 36 | const size_t plen = sizeof(INIT_FUNC_PREFIX); \ 37 | char *const tmp = ALLOCA_N(char, plen + flen + 1); \ 38 | memcpy(tmp, INIT_FUNC_PREFIX, plen); \ 39 | memcpy(tmp+plen, base, flen); \ 40 | tmp[plen+flen] = '\0'; \ 41 | *(buf) = tmp; \ 42 | } while(0) 43 | 44 | // adapted from Ruby's dln.c 45 | static size_t _init_funcname(const char **file) 46 | { 47 | const char *p = *file, 48 | *base, 49 | *dot = NULL; 50 | 51 | for (base = p; *p; p++) { /* Find position of last '/' */ 52 | if (*p == '.' && !dot) { 53 | dot = p; 54 | } 55 | if (*p == '/') { 56 | base = p + 1; 57 | dot = NULL; 58 | } 59 | } 60 | *file = base; 61 | return (uintptr_t) ((dot ? dot : p) - base); 62 | } 63 | 64 | // adapted from Ruby's dln.c 65 | static void *_dln_load(const char *file) 66 | { 67 | char *buf; 68 | const char *error; 69 | #define DLN_ERROR() (error = dlerror(), strcpy(ALLOCA_N(char, strlen(error) + 1), error)) 70 | 71 | void *handle; 72 | void (*init_fct)(void); 73 | 74 | INIT_FUNCNAME(&buf, file); 75 | 76 | #ifndef RTLD_DEEPBIND 77 | # define RTLD_DEEPBIND 0 78 | #endif 79 | /* Load file */ 80 | if ((handle = dlopen(file, RTLD_LAZY|RTLD_LOCAL|RTLD_DEEPBIND)) == NULL) { 81 | DLN_ERROR(); 82 | goto failed; 83 | } 84 | #if defined(RUBY_EXPORT) 85 | { 86 | static const char incompatible[] = "incompatible library version"; 87 | void *ex = dlsym(handle, "ruby_xmalloc"); 88 | if (ex && ex != (void *) &ruby_xmalloc) { 89 | 90 | # if defined __APPLE__ 91 | /* dlclose() segfaults */ 92 | rb_fatal("%s - %s", incompatible, file); 93 | # else 94 | dlclose(handle); 95 | error = incompatible; 96 | goto failed; 97 | #endif 98 | } 99 | } 100 | # endif 101 | 102 | init_fct = (void (*)(void)) dlsym(handle, buf); 103 | if (init_fct == NULL) { 104 | error = DLN_ERROR(); 105 | dlclose(handle); 106 | goto failed; 107 | } 108 | 109 | /* Call the init code */ 110 | (*init_fct)(); 111 | 112 | return handle; 113 | 114 | failed: 115 | rb_raise(rb_eLoadError, "%s", error); 116 | } 117 | 118 | __attribute__((visibility("default"))) void Init_mini_racer_loader(void) 119 | { 120 | VALUE mMiniRacer = rb_define_module("MiniRacer"); 121 | VALUE mLoader = rb_define_module_under(mMiniRacer, "Loader"); 122 | rb_define_singleton_method(mLoader, "load", _load_shared_lib, 1); 123 | } 124 | -------------------------------------------------------------------------------- /lib/mini_racer.rb: -------------------------------------------------------------------------------- 1 | require "mini_racer/version" 2 | require "pathname" 3 | 4 | if RUBY_ENGINE == "truffleruby" 5 | require "mini_racer/truffleruby" 6 | else 7 | if ENV["LD_PRELOAD"].to_s.include?("malloc") 8 | require "mini_racer_extension" 9 | else 10 | require "mini_racer_loader" 11 | ext_filename = "mini_racer_extension.#{RbConfig::CONFIG["DLEXT"]}" 12 | ext_path = 13 | Gem.loaded_specs["mini_racer"].require_paths.map do |p| 14 | (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p 15 | end 16 | ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? } 17 | 18 | unless ext_found 19 | raise LoadError, 20 | "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" 21 | end 22 | MiniRacer::Loader.load(ext_found.to_s) 23 | end 24 | end 25 | 26 | require "thread" 27 | require "json" 28 | require "io/wait" 29 | 30 | module MiniRacer 31 | class Error < ::StandardError; end 32 | 33 | class ContextDisposedError < Error; end 34 | class PlatformAlreadyInitialized < Error; end 35 | 36 | class EvalError < Error; end 37 | class ParseError < EvalError; end 38 | class ScriptTerminatedError < EvalError; end 39 | class V8OutOfMemoryError < EvalError; end 40 | 41 | class RuntimeError < EvalError 42 | def initialize(message) 43 | message, *@frames = message.split("\n") 44 | @frames.map! { "JavaScript #{_1.strip}" } 45 | super(message) 46 | end 47 | 48 | def backtrace 49 | frames = super 50 | @frames + frames unless frames.nil? 51 | end 52 | end 53 | 54 | class SnapshotError < Error 55 | def initialize(message) 56 | message, *@frames = message.split("\n") 57 | @frames.map! { "JavaScript #{_1.strip}" } 58 | super(message) 59 | end 60 | 61 | def backtrace 62 | frames = super 63 | @frames + frames unless frames.nil? 64 | end 65 | end 66 | 67 | class Context 68 | def load(filename) 69 | eval(File.read(filename)) 70 | end 71 | 72 | def write_heap_snapshot(file_or_io) 73 | f = nil 74 | implicit = false 75 | 76 | if String === file_or_io 77 | f = File.open(file_or_io, "w") 78 | implicit = true 79 | else 80 | f = file_or_io 81 | end 82 | 83 | raise ArgumentError, "file_or_io" unless File === f 84 | 85 | f.write(heap_snapshot()) 86 | ensure 87 | f.close if implicit 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/mini_racer/shared.rb: -------------------------------------------------------------------------------- 1 | # This code used to be shared in lib/mini_racer.rb 2 | # but was moved to the extension with https://github.com/rubyjs/mini_racer/pull/325. 3 | # So now this is effectively duplicate logic with C/C++ code. 4 | # Maybe one day it can be actually shared again between both backends. 5 | 6 | module MiniRacer 7 | 8 | MARSHAL_STACKDEPTH_DEFAULT = 2**9-2 9 | MARSHAL_STACKDEPTH_MAX_VALUE = 2**10-2 10 | 11 | class FailedV8Conversion 12 | attr_reader :info 13 | def initialize(info) 14 | @info = info 15 | end 16 | end 17 | 18 | # helper class returned when we have a JavaScript function 19 | class JavaScriptFunction 20 | def to_s 21 | "JavaScript Function" 22 | end 23 | end 24 | 25 | class Isolate 26 | def initialize(snapshot = nil) 27 | unless snapshot.nil? || snapshot.is_a?(Snapshot) 28 | raise ArgumentError, "snapshot must be a Snapshot object, passed a #{snapshot.inspect}" 29 | end 30 | 31 | # defined in the C class 32 | init_with_snapshot(snapshot) 33 | end 34 | end 35 | 36 | class Platform 37 | class << self 38 | def set_flags!(*args, **kwargs) 39 | flags_to_strings([args, kwargs]).each do |flag| 40 | # defined in the C class 41 | set_flag_as_str!(flag) 42 | end 43 | end 44 | 45 | private 46 | 47 | def flags_to_strings(flags) 48 | flags.flatten.map { |flag| flag_to_string(flag) }.flatten 49 | end 50 | 51 | # normalize flags to strings, and adds leading dashes if needed 52 | def flag_to_string(flag) 53 | if flag.is_a?(Hash) 54 | flag.map do |key, value| 55 | "#{flag_to_string(key)} #{value}" 56 | end 57 | else 58 | str = flag.to_s 59 | str = "--#{str}" unless str.start_with?('--') 60 | str 61 | end 62 | end 63 | end 64 | end 65 | 66 | # eval is defined in the C class 67 | class Context 68 | 69 | class ExternalFunction 70 | def initialize(name, callback, parent) 71 | unless String === name 72 | raise ArgumentError, "parent_object must be a String" 73 | end 74 | parent_object, _ , @name = name.rpartition(".") 75 | @callback = callback 76 | @parent = parent 77 | @parent_object_eval = nil 78 | @parent_object = nil 79 | 80 | unless parent_object.empty? 81 | @parent_object = parent_object 82 | 83 | @parent_object_eval = "" 84 | prev = "" 85 | first = true 86 | parent_object.split(".").each do |obj| 87 | prev << obj 88 | if first 89 | @parent_object_eval << "if (typeof #{prev} !== 'object' || typeof #{prev} !== 'function') { #{prev} = {} };\n" 90 | else 91 | @parent_object_eval << "#{prev} = #{prev} || {};\n" 92 | end 93 | prev << "." 94 | first = false 95 | end 96 | @parent_object_eval << "#{parent_object};" 97 | end 98 | notify_v8 99 | end 100 | end 101 | 102 | def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, marshal_stack_depth: nil) 103 | options ||= {} 104 | 105 | check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, marshal_stack_depth: marshal_stack_depth, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout) 106 | 107 | @functions = {} 108 | @timeout = nil 109 | @max_memory = nil 110 | @current_exception = nil 111 | @timeout = timeout 112 | @max_memory = max_memory 113 | @marshal_stack_depth = marshal_stack_depth 114 | 115 | # false signals it should be fetched if requested 116 | @isolate = isolate || false 117 | 118 | @ensure_gc_after_idle = ensure_gc_after_idle 119 | 120 | if @ensure_gc_after_idle 121 | @last_eval = nil 122 | @ensure_gc_thread = nil 123 | @ensure_gc_mutex = Mutex.new 124 | end 125 | 126 | @disposed = false 127 | 128 | @callback_mutex = Mutex.new 129 | @callback_running = false 130 | @thread_raise_called = false 131 | @eval_thread = nil 132 | 133 | # defined in the C class 134 | init_unsafe(isolate, snapshot) 135 | end 136 | 137 | def isolate 138 | return @isolate if @isolate != false 139 | # defined in the C class 140 | @isolate = create_isolate_value 141 | end 142 | 143 | def eval(str, options=nil) 144 | raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed 145 | 146 | filename = options && options[:filename].to_s 147 | 148 | @eval_thread = Thread.current 149 | isolate_mutex.synchronize do 150 | @current_exception = nil 151 | timeout do 152 | eval_unsafe(str, filename) 153 | end 154 | end 155 | ensure 156 | @eval_thread = nil 157 | ensure_gc_thread if @ensure_gc_after_idle 158 | end 159 | 160 | def call(function_name, *arguments) 161 | raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed 162 | 163 | @eval_thread = Thread.current 164 | isolate_mutex.synchronize do 165 | timeout do 166 | call_unsafe(function_name, *arguments) 167 | end 168 | end 169 | ensure 170 | @eval_thread = nil 171 | ensure_gc_thread if @ensure_gc_after_idle 172 | end 173 | 174 | def dispose 175 | return if @disposed 176 | isolate_mutex.synchronize do 177 | return if @disposed 178 | dispose_unsafe 179 | @disposed = true 180 | @isolate = nil # allow it to be garbage collected, if set 181 | end 182 | end 183 | 184 | 185 | def attach(name, callback) 186 | raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed 187 | 188 | wrapped = lambda do |*args| 189 | begin 190 | 191 | r = nil 192 | 193 | begin 194 | @callback_mutex.synchronize{ 195 | @callback_running = true 196 | } 197 | r = callback.call(*args) 198 | ensure 199 | @callback_mutex.synchronize{ 200 | @callback_running = false 201 | } 202 | end 203 | 204 | # wait up to 2 seconds for this to be interrupted 205 | # will very rarely be called cause #raise is called 206 | # in another mutex 207 | @callback_mutex.synchronize { 208 | if @thread_raise_called 209 | sleep 2 210 | end 211 | } 212 | 213 | r 214 | 215 | ensure 216 | @callback_mutex.synchronize { 217 | @thread_raise_called = false 218 | } 219 | end 220 | end 221 | 222 | isolate_mutex.synchronize do 223 | external = ExternalFunction.new(name, wrapped, self) 224 | @functions["#{name}"] = external 225 | end 226 | end 227 | 228 | private 229 | 230 | def ensure_gc_thread 231 | @last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC) 232 | @ensure_gc_mutex.synchronize do 233 | @ensure_gc_thread = nil if !@ensure_gc_thread&.alive? 234 | return if !Thread.main.alive? # avoid "can't alloc thread" exception 235 | @ensure_gc_thread ||= Thread.new do 236 | ensure_gc_after_idle_seconds = @ensure_gc_after_idle / 1000.0 237 | done = false 238 | while !done 239 | now = Process.clock_gettime(Process::CLOCK_MONOTONIC) 240 | 241 | if @disposed 242 | @ensure_gc_thread = nil 243 | break 244 | end 245 | 246 | if !@eval_thread && ensure_gc_after_idle_seconds < now - @last_eval 247 | @ensure_gc_mutex.synchronize do 248 | isolate_mutex.synchronize do 249 | if !@eval_thread 250 | low_memory_notification if !@disposed 251 | @ensure_gc_thread = nil 252 | done = true 253 | end 254 | end 255 | end 256 | end 257 | sleep ensure_gc_after_idle_seconds if !done 258 | end 259 | end 260 | end 261 | end 262 | 263 | def stop_attached 264 | @callback_mutex.synchronize{ 265 | if @callback_running 266 | @eval_thread.raise ScriptTerminatedError, "Terminated during callback" 267 | @thread_raise_called = true 268 | end 269 | } 270 | end 271 | 272 | def timeout(&blk) 273 | return blk.call unless @timeout 274 | 275 | mutex = Mutex.new 276 | done = false 277 | 278 | rp,wp = IO.pipe 279 | 280 | t = Thread.new do 281 | begin 282 | result = rp.wait_readable(@timeout/1000.0) 283 | if !result 284 | mutex.synchronize do 285 | stop unless done 286 | end 287 | end 288 | rescue => e 289 | STDERR.puts e 290 | STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT" 291 | end 292 | end 293 | 294 | rval = blk.call 295 | mutex.synchronize do 296 | done = true 297 | end 298 | 299 | wp.close 300 | 301 | # ensure we do not leak a thread in state 302 | t.join 303 | t = nil 304 | 305 | rval 306 | ensure 307 | # exceptions need to be handled 308 | wp&.close 309 | t&.join 310 | rp&.close 311 | end 312 | 313 | def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:) 314 | assert_option_is_nil_or_a('isolate', isolate, Isolate) 315 | assert_option_is_nil_or_a('snapshot', snapshot, Snapshot) 316 | 317 | assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000, max_value: 2**32-1) 318 | assert_numeric_or_nil('marshal_stack_depth', marshal_stack_depth, min_value: 1, max_value: MARSHAL_STACKDEPTH_MAX_VALUE) 319 | assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1) 320 | assert_numeric_or_nil('timeout', timeout, min_value: 1) 321 | 322 | if isolate && snapshot 323 | raise ArgumentError, 'can only pass one of isolate and snapshot options' 324 | end 325 | end 326 | 327 | def assert_numeric_or_nil(option_name, object, min_value:, max_value: nil) 328 | if max_value && object.is_a?(Numeric) && object > max_value 329 | raise ArgumentError, "#{option_name} must be less than or equal to #{max_value}" 330 | end 331 | 332 | if object.is_a?(Numeric) && object < min_value 333 | raise ArgumentError, "#{option_name} must be larger than or equal to #{min_value}" 334 | end 335 | 336 | if !object.nil? && !object.is_a?(Numeric) 337 | raise ArgumentError, "#{option_name} must be a number, passed a #{object.inspect}" 338 | end 339 | end 340 | 341 | def assert_option_is_nil_or_a(option_name, object, klass) 342 | unless object.nil? || object.is_a?(klass) 343 | raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}" 344 | end 345 | end 346 | end 347 | 348 | # `size` and `warmup!` public methods are defined in the C class 349 | class Snapshot 350 | def initialize(str = '') 351 | # ensure it first can load 352 | begin 353 | ctx = MiniRacer::Context.new 354 | ctx.eval(str) 355 | rescue MiniRacer::RuntimeError => e 356 | raise MiniRacer::SnapshotError, e.message, e.backtrace 357 | end 358 | 359 | @source = str 360 | 361 | # defined in the C class 362 | load(str) 363 | end 364 | 365 | def warmup!(src) 366 | # we have to do something here 367 | # we are bloating memory a bit but it is more correct 368 | # than hitting an exception when attempty to compile invalid source 369 | begin 370 | ctx = MiniRacer::Context.new 371 | ctx.eval(@source) 372 | ctx.eval(src) 373 | rescue MiniRacer::RuntimeError => e 374 | raise MiniRacer::SnapshotError, e.message, e.backtrace 375 | end 376 | 377 | warmup_unsafe!(src) 378 | end 379 | end 380 | end 381 | -------------------------------------------------------------------------------- /lib/mini_racer/truffleruby.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'shared' 4 | 5 | module MiniRacer 6 | 7 | class Context 8 | 9 | class ExternalFunction 10 | private 11 | 12 | def notify_v8 13 | name = @name.encode(::Encoding::UTF_8) 14 | wrapped = lambda do |*args| 15 | converted = @parent.send(:convert_js_to_ruby, args) 16 | begin 17 | result = @callback.call(*converted) 18 | rescue Polyglot::ForeignException => e 19 | e = RuntimeError.new(e.message) 20 | e.set_backtrace(e.backtrace) 21 | @parent.instance_variable_set(:@current_exception, e) 22 | raise e 23 | rescue => e 24 | @parent.instance_variable_set(:@current_exception, e) 25 | raise e 26 | end 27 | @parent.send(:convert_ruby_to_js, result) 28 | end 29 | 30 | if @parent_object.nil? 31 | # set global name to proc 32 | result = @parent.eval_in_context('this') 33 | result[name] = wrapped 34 | else 35 | parent_object_eval = @parent_object_eval.encode(::Encoding::UTF_8) 36 | begin 37 | result = @parent.eval_in_context(parent_object_eval) 38 | rescue Polyglot::ForeignException, StandardError => e 39 | raise ParseError, "Was expecting #{@parent_object} to be an object", e.backtrace 40 | end 41 | result[name] = wrapped 42 | # set evaluated object results name to proc 43 | end 44 | end 45 | end 46 | 47 | def heap_stats 48 | raise ContextDisposedError if @disposed 49 | { 50 | total_physical_size: 0, 51 | total_heap_size_executable: 0, 52 | total_heap_size: 0, 53 | used_heap_size: 0, 54 | heap_size_limit: 0, 55 | } 56 | end 57 | 58 | def stop 59 | if @entered 60 | @context.stop 61 | @stopped = true 62 | stop_attached 63 | end 64 | end 65 | 66 | def low_memory_notification 67 | GC.start 68 | end 69 | 70 | private 71 | 72 | @context_initialized = false 73 | @use_strict = false 74 | 75 | def init_unsafe(isolate, snapshot) 76 | unless defined?(Polyglot::InnerContext) 77 | raise "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version" 78 | end 79 | 80 | unless Polyglot.languages.include? "js" 81 | raise "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`\n" \ 82 | "You also need to install the 'js' component, see https://github.com/oracle/truffleruby/blob/master/doc/user/polyglot.md#installing-other-languages" 83 | end 84 | 85 | @context = Polyglot::InnerContext.new(on_cancelled: -> { 86 | raise ScriptTerminatedError, 'JavaScript was terminated (either by timeout or explicitly)' 87 | }) 88 | Context.instance_variable_set(:@context_initialized, true) 89 | @js_object = @context.eval('js', 'Object') 90 | @isolate_mutex = Mutex.new 91 | @stopped = false 92 | @entered = false 93 | @has_entered = false 94 | @current_exception = nil 95 | if isolate && snapshot 96 | isolate.instance_variable_set(:@snapshot, snapshot) 97 | end 98 | if snapshot 99 | @snapshot = snapshot 100 | elsif isolate 101 | @snapshot = isolate.instance_variable_get(:@snapshot) 102 | else 103 | @snapshot = nil 104 | end 105 | @is_object_or_array_func, @is_map_func, @is_map_iterator_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func = eval_in_context <<-CODE 106 | [ 107 | (x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) }, 108 | (x) => { return x instanceof Map }, 109 | (x) => { return x[Symbol.toStringTag] === 'Map Iterator' }, 110 | (x) => { return x instanceof Date }, 111 | (x) => { return x.getTime(x) }, 112 | (x) => { return typeof x === 'symbol' }, 113 | (x) => { var r = x.description; return r === undefined ? 'undefined' : r }, 114 | (x) => { return new Date(x) }, 115 | (x) => { return new Array(x) }, 116 | ] 117 | CODE 118 | end 119 | 120 | def dispose_unsafe 121 | @context.close 122 | end 123 | 124 | def eval_unsafe(str, filename) 125 | @entered = true 126 | if !@has_entered && @snapshot 127 | snapshot_src = encode(@snapshot.instance_variable_get(:@source)) 128 | begin 129 | eval_in_context(snapshot_src, filename) 130 | rescue Polyglot::ForeignException => e 131 | raise RuntimeError, e.message, e.backtrace 132 | end 133 | end 134 | @has_entered = true 135 | raise RuntimeError, "TruffleRuby does not support eval after stop" if @stopped 136 | raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String) 137 | raise TypeError, "wrong type argument #{filename.class} (should be a string)" unless filename.nil? || filename.is_a?(String) 138 | 139 | str = encode(str) 140 | begin 141 | translate do 142 | eval_in_context(str, filename) 143 | end 144 | rescue Polyglot::ForeignException => e 145 | raise RuntimeError, e.message, e.backtrace 146 | rescue ::RuntimeError => e 147 | if @current_exception 148 | e = @current_exception 149 | @current_exception = nil 150 | raise e 151 | else 152 | raise e, e.message 153 | end 154 | end 155 | ensure 156 | @entered = false 157 | end 158 | 159 | def call_unsafe(function_name, *arguments) 160 | @entered = true 161 | if !@has_entered && @snapshot 162 | src = encode(@snapshot.instance_variable_get(:source)) 163 | begin 164 | eval_in_context(src) 165 | rescue Polyglot::ForeignException => e 166 | raise RuntimeError, e.message, e.backtrace 167 | end 168 | end 169 | @has_entered = true 170 | raise RuntimeError, "TruffleRuby does not support call after stop" if @stopped 171 | begin 172 | translate do 173 | function = eval_in_context(function_name) 174 | function.call(*convert_ruby_to_js(arguments)) 175 | end 176 | rescue Polyglot::ForeignException => e 177 | raise RuntimeError, e.message, e.backtrace 178 | end 179 | ensure 180 | @entered = false 181 | end 182 | 183 | def create_isolate_value 184 | # Returning a dummy object since TruffleRuby does not have a 1-1 concept with isolate. 185 | # However, code and ASTs are shared between contexts. 186 | Isolate.new 187 | end 188 | 189 | def isolate_mutex 190 | @isolate_mutex 191 | end 192 | 193 | def translate 194 | convert_js_to_ruby yield 195 | rescue Object => e 196 | message = e.message 197 | if @current_exception 198 | raise @current_exception 199 | elsif e.message && e.message.start_with?('SyntaxError:') 200 | error_class = MiniRacer::ParseError 201 | elsif e.is_a?(MiniRacer::ScriptTerminatedError) 202 | error_class = MiniRacer::ScriptTerminatedError 203 | else 204 | error_class = MiniRacer::RuntimeError 205 | end 206 | 207 | if error_class == MiniRacer::RuntimeError 208 | bls = e.backtrace_locations&.select { |bl| bl&.source_location&.language == 'js' } 209 | if bls && !bls.empty? 210 | if '(eval)' != bls[0].path 211 | message = "#{e.message}\n at #{bls[0]}\n" + bls[1..].map(&:to_s).join("\n") 212 | else 213 | message = "#{e.message}\n" + bls.map(&:to_s).join("\n") 214 | end 215 | end 216 | raise error_class, message 217 | else 218 | raise error_class, message, e.backtrace 219 | end 220 | end 221 | 222 | def convert_js_to_ruby(value) 223 | case value 224 | when true, false, Integer, Float 225 | value 226 | else 227 | if value.nil? 228 | nil 229 | elsif value.respond_to?(:call) 230 | MiniRacer::JavaScriptFunction.new 231 | elsif value.respond_to?(:to_str) 232 | value.to_str.dup 233 | elsif value.respond_to?(:to_ary) 234 | value.to_ary.map do |e| 235 | if e.respond_to?(:call) 236 | nil 237 | else 238 | convert_js_to_ruby(e) 239 | end 240 | end 241 | elsif time?(value) 242 | js_date_to_time(value) 243 | elsif symbol?(value) 244 | js_symbol_to_symbol(value) 245 | elsif map?(value) 246 | js_map_to_hash(value) 247 | elsif map_iterator?(value) 248 | value.map { |e| convert_js_to_ruby(e) } 249 | else 250 | object = value 251 | h = {} 252 | object.instance_variables.each do |member| 253 | v = object[member] 254 | unless v.respond_to?(:call) 255 | h[member.to_s] = convert_js_to_ruby(v) 256 | end 257 | end 258 | h 259 | end 260 | end 261 | end 262 | 263 | def object_or_array?(val) 264 | @is_object_or_array_func.call(val) 265 | end 266 | 267 | def map?(value) 268 | @is_map_func.call(value) 269 | end 270 | 271 | def map_iterator?(value) 272 | @is_map_iterator_func.call(value) 273 | end 274 | 275 | def time?(value) 276 | @is_time_func.call(value) 277 | end 278 | 279 | def js_date_to_time(value) 280 | millis = @js_date_to_time_func.call(value) 281 | Time.at(Rational(millis, 1000)) 282 | end 283 | 284 | def symbol?(value) 285 | @is_symbol_func.call(value) 286 | end 287 | 288 | def js_symbol_to_symbol(value) 289 | @js_symbol_to_symbol_func.call(value).to_s.to_sym 290 | end 291 | 292 | def js_map_to_hash(map) 293 | map.to_a.to_h do |key, value| 294 | [convert_js_to_ruby(key), convert_js_to_ruby(value)] 295 | end 296 | end 297 | 298 | def js_new_date(value) 299 | @js_new_date_func.call(value) 300 | end 301 | 302 | def js_new_array(size) 303 | @js_new_array_func.call(size) 304 | end 305 | 306 | def convert_ruby_to_js(value) 307 | case value 308 | when nil, true, false, Integer, Float 309 | value 310 | when Array 311 | ary = js_new_array(value.size) 312 | value.each_with_index do |v, i| 313 | ary[i] = convert_ruby_to_js(v) 314 | end 315 | ary 316 | when Hash 317 | h = @js_object.new 318 | value.each_pair do |k, v| 319 | h[convert_ruby_to_js(k.to_s)] = convert_ruby_to_js(v) 320 | end 321 | h 322 | when String, Symbol 323 | Truffle::Interop.as_truffle_string value 324 | when Time 325 | js_new_date(value.to_f * 1000) 326 | when DateTime 327 | js_new_date(value.to_time.to_f * 1000) 328 | else 329 | "Undefined Conversion" 330 | end 331 | end 332 | 333 | def encode(string) 334 | raise ArgumentError unless string 335 | string.encode(::Encoding::UTF_8) 336 | end 337 | 338 | class_eval <<-'RUBY', "(mini_racer)", 1 339 | def eval_in_context(code, file = nil); code = ('"use strict";' + code) if Context.instance_variable_get(:@use_strict); @context.eval('js', code, file || '(mini_racer)'); end 340 | RUBY 341 | 342 | end 343 | 344 | class Isolate 345 | def init_with_snapshot(snapshot) 346 | # TruffleRuby does not have a 1-1 concept with isolate. 347 | # However, isolate can hold a snapshot, and code and ASTs are shared between contexts. 348 | @snapshot = snapshot 349 | end 350 | end 351 | 352 | class Platform 353 | def self.set_flag_as_str!(flag) 354 | raise TypeError, "wrong type argument #{flag.class} (should be a string)" unless flag.is_a?(String) 355 | raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.instance_variable_get(:@context_initialized) 356 | Context.instance_variable_set(:@use_strict, true) if "--use_strict" == flag 357 | end 358 | end 359 | 360 | class Snapshot 361 | def load(str) 362 | raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String) 363 | # Intentionally noop since TruffleRuby mocks the snapshot API 364 | end 365 | 366 | def warmup_unsafe!(src) 367 | raise TypeError, "wrong type argument #{src.class} (should be a string)" unless src.is_a?(String) 368 | # Intentionally noop since TruffleRuby mocks the snapshot API 369 | # by replaying snapshot source before the first eval/call 370 | self 371 | end 372 | end 373 | end 374 | -------------------------------------------------------------------------------- /lib/mini_racer/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MiniRacer 4 | VERSION = "0.18.1" 5 | LIBV8_NODE_VERSION = "~> 23.6.1.0" 6 | end 7 | -------------------------------------------------------------------------------- /mini_racer.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "mini_racer/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "mini_racer" 8 | spec.version = MiniRacer::VERSION 9 | spec.authors = ["Sam Saffron"] 10 | spec.email = ["sam.saffron@gmail.com"] 11 | 12 | spec.summary = "Minimal embedded v8 for Ruby" 13 | spec.description = "Minimal embedded v8 engine for Ruby" 14 | spec.homepage = "https://github.com/discourse/mini_racer" 15 | spec.license = "MIT" 16 | 17 | spec.metadata = { 18 | "bug_tracker_uri" => "https://github.com/discourse/mini_racer/issues", 19 | "changelog_uri" => 20 | "https://github.com/discourse/mini_racer/blob/v#{spec.version}/CHANGELOG", 21 | "documentation_uri" => 22 | "https://www.rubydoc.info/gems/mini_racer/#{spec.version}", 23 | "source_code_uri" => 24 | "https://github.com/discourse/mini_racer/tree/v#{spec.version}" 25 | } 26 | 27 | spec.files = 28 | Dir[ 29 | "lib/**/*.rb", 30 | "ext/**/*", 31 | "README.md", 32 | "LICENSE.txt", 33 | "CHANGELOG", 34 | "CODE_OF_CONDUCT.md" 35 | ] 36 | spec.require_paths = ["lib"] 37 | 38 | spec.add_development_dependency "bundler" 39 | spec.add_development_dependency "rake", ">= 12.3.3" 40 | spec.add_development_dependency "minitest", "~> 5.0" 41 | spec.add_development_dependency "rake-compiler" 42 | spec.add_development_dependency "activesupport", "> 6" 43 | spec.add_development_dependency "m" 44 | 45 | spec.add_dependency "libv8-node", MiniRacer::LIBV8_NODE_VERSION 46 | spec.require_paths = %w[lib ext] 47 | 48 | spec.extensions = %w[ 49 | ext/mini_racer_loader/extconf.rb 50 | ext/mini_racer_extension/extconf.rb 51 | ] 52 | 53 | spec.required_ruby_version = ">= 3.1" 54 | end 55 | -------------------------------------------------------------------------------- /test/file.js: -------------------------------------------------------------------------------- 1 | var hello = "world"; 2 | -------------------------------------------------------------------------------- /test/function_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'timeout' 3 | 4 | class MiniRacerFunctionTest < Minitest::Test 5 | def test_fun 6 | context = MiniRacer::Context.new 7 | context.eval("function f(x) { return 'I need ' + x + ' foos' }") 8 | assert_equal context.eval('f(10)'), 'I need 10 foos' 9 | 10 | assert_raises(ArgumentError) do 11 | context.call 12 | end 13 | 14 | count = 4 15 | res = context.call('f', count) 16 | assert_equal "I need #{count} foos", res 17 | end 18 | 19 | def test_non_existing_function 20 | context = MiniRacer::Context.new 21 | context.eval("function f(x) { return 'I need ' + x + ' galettes' }") 22 | 23 | # f is defined, let's call g 24 | assert_raises(MiniRacer::RuntimeError) do 25 | context.call('g') 26 | end 27 | end 28 | 29 | def test_throwing_function 30 | context = MiniRacer::Context.new 31 | context.eval('function f(x) { throw new Error("foo bar") }') 32 | 33 | # f is defined, let's call g 34 | err = assert_raises(MiniRacer::RuntimeError) do 35 | context.call('f', 1) 36 | end 37 | assert_equal err.message, 'Error: foo bar' 38 | assert_match(/1:23/, err.backtrace[0]) unless RUBY_ENGINE == "truffleruby" 39 | assert_match(/1:/, err.backtrace[0]) if RUBY_ENGINE == "truffleruby" 40 | end 41 | 42 | def test_args_types 43 | context = MiniRacer::Context.new 44 | context.eval("function f(x, y) { return 'I need ' + x + ' ' + y }") 45 | 46 | res = context.call('f', 3, 'bars') 47 | assert_equal 'I need 3 bars', res 48 | 49 | res = context.call('f', { a: 1 }, 'bars') 50 | assert_equal 'I need [object Object] bars', res 51 | 52 | res = context.call('f', [1, 2, 3], 'bars') 53 | assert_equal 'I need 1,2,3 bars', res 54 | end 55 | 56 | def test_complex_return 57 | context = MiniRacer::Context.new 58 | context.eval('function f(x, y) { return { vx: x, vy: y, array: [x, y] } }') 59 | 60 | h = { 'vx' => 3, 'vy' => 'bars', 'array' => [3, 'bars'] } 61 | res = context.call('f', 3, 'bars') 62 | assert_equal h, res 63 | end 64 | 65 | def test_do_not_hang_with_concurrent_calls 66 | context = MiniRacer::Context.new 67 | context.eval("function f(x) { return 'I need ' + x + ' foos' }") 68 | 69 | thread_count = 2 70 | 71 | threads = [] 72 | thread_count.times do 73 | threads << Thread.new do 74 | 10.times do |i| 75 | context.call('f', i) 76 | end 77 | end 78 | end 79 | 80 | joined_thread_count = 0 81 | for t in threads do 82 | joined_thread_count += 1 83 | t.join 84 | end 85 | 86 | # Dummy test, completing should be enough to show we don't hang 87 | assert_equal thread_count, joined_thread_count 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/mini_racer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "securerandom" 4 | require "date" 5 | require "test_helper" 6 | 7 | class MiniRacerTest < Minitest::Test 8 | # see `test_platform_set_flags_works` below 9 | MiniRacer::Platform.set_flags! :use_strict 10 | 11 | # --stress_snapshot works around a bogus debug assert in V8 12 | # that terminates the process with the following error: 13 | # 14 | # Fatal error in ../deps/v8/src/heap/read-only-spaces.cc, line 70 15 | # Check failed: read_only_blob_checksum_ == snapshot_checksum ( vs. 1099685679). 16 | MiniRacer::Platform.set_flags! :stress_snapshot 17 | 18 | def test_locale_mx 19 | if RUBY_ENGINE == "truffleruby" 20 | skip "TruffleRuby does not have all js timezone by default" 21 | end 22 | val = 23 | MiniRacer::Context.new.eval( 24 | "new Date('April 28 2021').toLocaleDateString('es-MX');" 25 | ) 26 | assert_equal "28/4/2021", val 27 | end 28 | 29 | def test_locale_us 30 | if RUBY_ENGINE == "truffleruby" 31 | skip "TruffleRuby does not have all js timezone by default" 32 | end 33 | val = 34 | MiniRacer::Context.new.eval( 35 | "new Date('April 28 2021').toLocaleDateString('en-US');" 36 | ) 37 | assert_equal "4/28/2021", val 38 | end 39 | 40 | def test_locale_fr 41 | # TODO: this causes a segfault on Linux 42 | 43 | if RUBY_ENGINE == "truffleruby" 44 | skip "TruffleRuby does not have all js timezone by default" 45 | end 46 | val = 47 | MiniRacer::Context.new.eval( 48 | "new Date('April 28 2021').toLocaleDateString('fr-FR');" 49 | ) 50 | assert_equal "28/04/2021", val 51 | end 52 | 53 | def test_segfault 54 | skip "running this test is very slow" 55 | # 5000.times do 56 | # GC.start 57 | # context = MiniRacer::Context.new(timeout: 5) 58 | # context.attach("echo", proc{|msg| msg.to_sym.to_s}) 59 | # assert_raises(MiniRacer::EvalError) do 60 | # context.eval("while(true) echo('foo');") 61 | # end 62 | # end 63 | end 64 | 65 | def test_that_it_has_a_version_number 66 | refute_nil ::MiniRacer::VERSION 67 | end 68 | 69 | def test_types 70 | context = MiniRacer::Context.new 71 | assert_equal 2, context.eval("2") 72 | assert_equal "two", context.eval('"two"') 73 | assert_equal 2.1, context.eval("2.1") 74 | assert_equal true, context.eval("true") 75 | assert_equal false, context.eval("false") 76 | assert_nil context.eval("null") 77 | assert_nil context.eval("undefined") 78 | end 79 | 80 | def test_compile_nil_context 81 | context = MiniRacer::Context.new 82 | assert_raises(TypeError) { assert_equal 2, context.eval(nil) } 83 | end 84 | 85 | def test_array 86 | context = MiniRacer::Context.new 87 | assert_equal [1, "two"], context.eval('[1,"two"]') 88 | end 89 | 90 | def test_object 91 | context = MiniRacer::Context.new 92 | # remember JavaScript is quirky {"1" : 1} magically turns to {1: 1} cause magic 93 | assert_equal( 94 | { "1" => 2, "two" => "two" }, 95 | context.eval('var a={"1" : 2, "two" : "two"}; a') 96 | ) 97 | end 98 | 99 | def test_it_returns_runtime_error 100 | context = MiniRacer::Context.new 101 | exp = nil 102 | 103 | begin 104 | context.eval("var foo=function(){boom;}; foo()") 105 | rescue => e 106 | exp = e 107 | end 108 | 109 | assert_equal MiniRacer::RuntimeError, exp.class 110 | 111 | assert_match(/boom/, exp.message) 112 | assert_match(/foo/, exp.backtrace[0]) 113 | assert_match(/mini_racer/, exp.backtrace[2]) 114 | 115 | # context should not be dead 116 | assert_equal 2, context.eval("1+1") 117 | end 118 | 119 | def test_it_can_stop 120 | context = MiniRacer::Context.new 121 | exp = nil 122 | 123 | begin 124 | Thread.new do 125 | sleep 0.01 126 | context.stop 127 | end 128 | context.eval("while(true){}") 129 | rescue => e 130 | exp = e 131 | end 132 | 133 | assert_equal MiniRacer::ScriptTerminatedError, exp.class 134 | assert_match(/terminated/, exp.message) 135 | end 136 | 137 | def test_it_can_timeout_during_serialization 138 | if RUBY_ENGINE == "truffleruby" 139 | skip "TruffleRuby needs a fix for timing out during translation" 140 | end 141 | context = MiniRacer::Context.new(timeout: 500) 142 | 143 | assert_raises(MiniRacer::ScriptTerminatedError) do 144 | context.eval "var a = {get a(){ while(true); }}; a" 145 | end 146 | end 147 | 148 | def test_it_can_automatically_time_out_context 149 | # 2 millisecs is a very short timeout but we don't want test running forever 150 | context = MiniRacer::Context.new(timeout: 2) 151 | assert_raises { context.eval("while(true){}") } 152 | end 153 | 154 | def test_returns_javascript_function 155 | context = MiniRacer::Context.new 156 | assert_same MiniRacer::JavaScriptFunction, 157 | context.eval("var a = function(){}; a").class 158 | end 159 | 160 | def test_it_handles_malformed_js 161 | context = MiniRacer::Context.new 162 | assert_raises MiniRacer::ParseError do 163 | context.eval("I am not JavaScript {") 164 | end 165 | end 166 | 167 | def test_it_handles_malformed_js_with_backtrace 168 | context = MiniRacer::Context.new 169 | assert_raises MiniRacer::ParseError do 170 | begin 171 | context.eval("var i;\ni=2;\nI am not JavaScript {") 172 | rescue => e 173 | # I am not 174 | assert_match(/3:2/, e.message) 175 | raise 176 | end 177 | end 178 | end 179 | 180 | def test_it_remembers_stuff_in_context 181 | context = MiniRacer::Context.new 182 | context.eval("var x = function(){return 22;}") 183 | assert_equal 22, context.eval("x()") 184 | end 185 | 186 | def test_can_attach_functions 187 | context = MiniRacer::Context.new 188 | context.eval "var adder" 189 | context.attach("adder", proc { |a, b| a + b }) 190 | assert_equal 3, context.eval("adder(1,2)") 191 | end 192 | 193 | def test_es6_arrow_functions 194 | context = MiniRacer::Context.new 195 | assert_equal 42, context.eval("var adder=(x,y)=>x+y; adder(21,21);") 196 | end 197 | 198 | def test_concurrent_access 199 | context = MiniRacer::Context.new 200 | context.eval("var counter=0; var plus=()=>counter++;") 201 | 202 | (1..10).map { Thread.new { context.eval("plus()") } }.each(&:join) 203 | 204 | assert_equal 10, context.eval("counter") 205 | end 206 | 207 | class FooError < StandardError 208 | def initialize(message) 209 | super(message) 210 | end 211 | end 212 | 213 | def test_attached_exceptions 214 | context = MiniRacer::Context.new 215 | context.attach("adder", proc { raise FooError, "I like foos" }) 216 | assert_raises do 217 | begin 218 | raise FooError, "I like foos" 219 | context.eval("adder()") 220 | rescue => e 221 | assert_equal FooError, e.class 222 | assert_match(/I like foos/, e.message) 223 | # TODO backtrace splicing so js frames are injected 224 | raise 225 | end 226 | end 227 | end 228 | 229 | def test_attached_on_object 230 | context = MiniRacer::Context.new 231 | context.eval "var minion" 232 | context.attach("minion.speak", proc { "banana" }) 233 | assert_equal "banana", context.eval("minion.speak()") 234 | end 235 | 236 | def test_attached_on_nested_object 237 | context = MiniRacer::Context.new 238 | context.eval "var minion" 239 | context.attach("minion.kevin.speak", proc { "banana" }) 240 | assert_equal "banana", context.eval("minion.kevin.speak()") 241 | end 242 | 243 | def test_return_arrays 244 | context = MiniRacer::Context.new 245 | context.eval "var nose" 246 | context.attach("nose.type", proc { ["banana", ["nose"]] }) 247 | assert_equal ["banana", ["nose"]], context.eval("nose.type()") 248 | end 249 | 250 | def test_return_hash 251 | context = MiniRacer::Context.new 252 | context.attach( 253 | "test", 254 | proc { { :banana => :nose, "inner" => { 42 => 42 } } } 255 | ) 256 | assert_equal( 257 | { "banana" => "nose", "inner" => { "42" => 42 } }, 258 | context.eval("test()") 259 | ) 260 | end 261 | 262 | def test_date_nan 263 | # NoMethodError: undefined method `source_location' for " 264 | # core/float.rb:114:in `to_i'":Thread::Backtrace::Location 265 | skip "TruffleRuby bug" if RUBY_ENGINE == "truffleruby" 266 | context = MiniRacer::Context.new 267 | assert_raises(RangeError) { context.eval("new Date(NaN)") } # should not crash process 268 | end 269 | 270 | def test_return_date 271 | context = MiniRacer::Context.new 272 | test_time = Time.new 273 | test_datetime = test_time.to_datetime 274 | context.attach("test", proc { test_time }) 275 | context.attach("test_datetime", proc { test_datetime }) 276 | 277 | # check that marshalling to JS creates a date object (getTime()) 278 | assert_equal( 279 | (test_time.to_f * 1000).to_i, 280 | context.eval("var result = test(); result.getTime();").to_i 281 | ) 282 | 283 | # check that marshalling to RB creates a Time object 284 | result = context.eval("test()") 285 | assert_equal(test_time.class, result.class) 286 | assert_equal(test_time.tv_sec, result.tv_sec) 287 | 288 | # check that no precision is lost in the marshalling (js only stores milliseconds) 289 | assert_equal( 290 | (test_time.tv_usec / 1000.0).floor, 291 | (result.tv_usec / 1000.0).floor 292 | ) 293 | 294 | # check that DateTime gets marshalled to js date and back out as rb Time 295 | result = context.eval("test_datetime()") 296 | assert_equal(test_time.class, result.class) 297 | assert_equal(test_time.tv_sec, result.tv_sec) 298 | assert_equal( 299 | (test_time.tv_usec / 1000.0).floor, 300 | (result.tv_usec / 1000.0).floor 301 | ) 302 | end 303 | 304 | def test_datetime_missing 305 | # NoMethodError: undefined method `source_location' for 306 | # # 307 | skip "TruffleRuby bug" if RUBY_ENGINE == "truffleruby" 308 | date_time_backup = Object.send(:remove_const, :DateTime) 309 | 310 | begin 311 | # no exceptions should happen here, and non-datetime classes should marshall correctly still. 312 | context = MiniRacer::Context.new 313 | test_time = Time.new 314 | context.attach("test", proc { test_time }) 315 | 316 | assert_equal( 317 | (test_time.to_f * 1000).to_i, 318 | context.eval("var result = test(); result.getTime();").to_i 319 | ) 320 | 321 | result = context.eval("test()") 322 | assert_equal(test_time.class, result.class) 323 | assert_equal(test_time.tv_sec, result.tv_sec) 324 | assert_equal( 325 | (test_time.tv_usec / 1000.0).floor, 326 | (result.tv_usec / 1000.0).floor 327 | ) 328 | ensure 329 | Object.const_set(:DateTime, date_time_backup) 330 | end 331 | end 332 | 333 | def test_return_large_number 334 | context = MiniRacer::Context.new 335 | test_num = 1_000_000_000_000_000 336 | context.attach("test", proc { test_num }) 337 | 338 | assert_equal(true, context.eval("test() === 1000000000000000")) 339 | assert_equal(test_num, context.eval("test()")) 340 | end 341 | 342 | def test_return_int_max 343 | context = MiniRacer::Context.new 344 | test_num = 2**(31) - 1 #last int32 number 345 | context.attach("test", proc { test_num }) 346 | 347 | assert_equal(true, context.eval("test() === 2147483647")) 348 | assert_equal(test_num, context.eval("test()")) 349 | end 350 | 351 | def test_return_unknown 352 | context = MiniRacer::Context.new 353 | test_unknown = Date.new # hits T_DATA in convert_ruby_to_v8 354 | context.attach("test", proc { test_unknown }) 355 | assert_equal("Undefined Conversion", context.eval("test()")) 356 | 357 | # clean up and start up a new context 358 | context = nil 359 | GC.start 360 | 361 | context = MiniRacer::Context.new 362 | test_unknown = Date.new # hits T_DATA in convert_ruby_to_v8 363 | context.attach("test", proc { test_unknown }) 364 | assert_equal("Undefined Conversion", context.eval("test()")) 365 | end 366 | 367 | def test_max_memory 368 | if RUBY_ENGINE == "truffleruby" 369 | skip "TruffleRuby does not yet implement max_memory" 370 | end 371 | context = MiniRacer::Context.new(max_memory: 200_000_000) 372 | 373 | assert_raises(MiniRacer::V8OutOfMemoryError) do 374 | context.eval( 375 | "let s = 1000; var a = new Array(s); a.fill(0); while(true) {s *= 1.1; let n = new Array(Math.floor(s)); n.fill(0); a = a.concat(n); };" 376 | ) 377 | end 378 | end 379 | 380 | def test_max_memory_for_call 381 | if RUBY_ENGINE == "truffleruby" 382 | skip "TruffleRuby does not yet implement max_memory" 383 | end 384 | context = MiniRacer::Context.new(max_memory: 100_000_000) 385 | context.eval(<<~JS) 386 | let s; 387 | function memory_test() { 388 | var a = new Array(s); 389 | a.fill(0); 390 | while(true) { 391 | s *= 1.1; 392 | let n = new Array(Math.floor(s)); 393 | n.fill(0); 394 | a = a.concat(n); 395 | if (s > 1000000) { 396 | return; 397 | } 398 | } 399 | } 400 | function set_s(val) { 401 | s = val; 402 | } 403 | JS 404 | context.call("set_s", 1000) 405 | assert_raises(MiniRacer::V8OutOfMemoryError) { context.call("memory_test") } 406 | s = context.eval("s") 407 | assert_operator(s, :>, 100_000) 408 | end 409 | 410 | def test_max_memory_bounds 411 | assert_raises(ArgumentError) do 412 | MiniRacer::Context.new(max_memory: -200_000_000) 413 | end 414 | 415 | assert_raises(ArgumentError) { MiniRacer::Context.new(max_memory: 2**32) } 416 | end 417 | 418 | module Echo 419 | def self.say(thing) 420 | thing 421 | end 422 | end 423 | 424 | def test_can_attach_method 425 | context = MiniRacer::Context.new 426 | context.eval "var Echo" 427 | context.attach("Echo.say", Echo.method(:say)) 428 | assert_equal "hello", context.eval("Echo.say('hello')") 429 | end 430 | 431 | def test_attach_non_object 432 | context = MiniRacer::Context.new 433 | context.eval("var minion = 2") 434 | context.attach("minion.kevin.speak", proc { "banana" }) 435 | assert_equal "banana", context.call("minion.kevin.speak") 436 | end 437 | 438 | def test_load 439 | context = MiniRacer::Context.new 440 | context.load(File.dirname(__FILE__) + "/file.js") 441 | assert_equal "world", context.eval("hello") 442 | assert_raises { context.load(File.dirname(__FILE__) + "/missing.js") } 443 | end 444 | 445 | def test_contexts_can_be_safely_GCed 446 | context = MiniRacer::Context.new 447 | context.eval 'var hello = "world";' 448 | 449 | context = nil 450 | GC.start 451 | end 452 | 453 | def test_it_can_use_snapshots 454 | snapshot = 455 | MiniRacer::Snapshot.new( 456 | 'function hello() { return "world"; }; var foo = "bar";' 457 | ) 458 | 459 | context = MiniRacer::Context.new(snapshot: snapshot) 460 | 461 | assert_equal "world", context.eval("hello()") 462 | assert_equal "bar", context.eval("foo") 463 | end 464 | 465 | def test_snapshot_size 466 | if RUBY_ENGINE == "truffleruby" 467 | skip "TruffleRuby does not yet implement snapshots" 468 | end 469 | snapshot = MiniRacer::Snapshot.new('var foo = "bar";') 470 | 471 | # for some reason sizes seem to change across runs, so we just 472 | # check it's a positive integer 473 | assert(snapshot.size > 0) 474 | end 475 | 476 | def test_snapshot_dump 477 | if RUBY_ENGINE == "truffleruby" 478 | skip "TruffleRuby does not yet implement snapshots" 479 | end 480 | snapshot = MiniRacer::Snapshot.new('var foo = "bar";') 481 | dump = snapshot.dump 482 | 483 | assert_equal(String, dump.class) 484 | assert_equal(Encoding::ASCII_8BIT, dump.encoding) 485 | assert_equal(snapshot.size, dump.length) 486 | end 487 | 488 | def test_invalid_snapshots_throw_an_exception 489 | begin 490 | MiniRacer::Snapshot.new("var foo = bar;") 491 | rescue MiniRacer::SnapshotError => e 492 | assert(e.backtrace[0].include? "JavaScript") 493 | got_error = true 494 | end 495 | 496 | assert(got_error, "should raise") 497 | end 498 | 499 | def test_an_empty_snapshot_is_valid 500 | MiniRacer::Snapshot.new("") 501 | MiniRacer::Snapshot.new 502 | GC.start 503 | end 504 | 505 | def test_snapshots_can_be_warmed_up_with_no_side_effects 506 | # shamelessly inspired by https://github.com/v8/v8/blob/5.3.254/test/cctest/test-serialize.cc#L792-L854 507 | snapshot_source = <<-JS 508 | function f() { return Math.sin(1); } 509 | var a = 5; 510 | JS 511 | 512 | snapshot = MiniRacer::Snapshot.new(snapshot_source) 513 | 514 | warmup_source = <<-JS 515 | Math.tan(1); 516 | var a = f(); 517 | Math.sin = 1; 518 | JS 519 | 520 | warmed_up_snapshot = snapshot.warmup!(warmup_source) 521 | 522 | context = MiniRacer::Context.new(snapshot: snapshot) 523 | 524 | assert_equal 5, context.eval("a") 525 | assert_equal "function", context.eval("typeof(Math.sin)") 526 | assert_same snapshot, warmed_up_snapshot 527 | end 528 | 529 | def test_invalid_warmup_sources_throw_an_exception 530 | assert_raises(MiniRacer::SnapshotError) do 531 | MiniRacer::Snapshot.new("Math.sin = 1;").warmup!("var a = Math.sin(1);") 532 | end 533 | end 534 | 535 | def test_invalid_warmup_sources_throw_an_exception_2 536 | assert_raises(TypeError) do 537 | MiniRacer::Snapshot.new("function f() { return 1 }").warmup!([]) 538 | end 539 | end 540 | 541 | def test_warming_up_with_invalid_source_does_not_affect_the_snapshot_internal_state 542 | snapshot = MiniRacer::Snapshot.new("Math.sin = 1;") 543 | 544 | begin 545 | snapshot.warmup!("var a = Math.sin(1);") 546 | rescue StandardError 547 | # do nothing 548 | end 549 | 550 | context = MiniRacer::Context.new(snapshot: snapshot) 551 | 552 | assert_equal 1, context.eval("Math.sin") 553 | end 554 | 555 | def test_snapshots_can_be_GCed_without_affecting_contexts_created_from_them 556 | snapshot = MiniRacer::Snapshot.new("Math.sin = 1;") 557 | context = MiniRacer::Context.new(snapshot: snapshot) 558 | 559 | # force the snapshot to be GC'ed 560 | snapshot = nil 561 | GC.start 562 | 563 | # the context should still work fine 564 | assert_equal 1, context.eval("Math.sin") 565 | end 566 | 567 | def test_isolates_from_snapshot_dont_get_corrupted_if_the_snapshot_gets_warmed_up_or_GCed 568 | # basically tests that isolates get their own copy of the snapshot and don't 569 | # get corrupted if the snapshot is subsequently warmed up 570 | snapshot_source = <<-JS 571 | function f() { return Math.sin(1); } 572 | var a = 5; 573 | JS 574 | 575 | snapshot = MiniRacer::Snapshot.new(snapshot_source) 576 | 577 | warmump_source = <<-JS 578 | Math.tan(1); 579 | var a = f(); 580 | Math.sin = 1; 581 | JS 582 | 583 | snapshot.warmup!(warmump_source) 584 | 585 | context1 = MiniRacer::Context.new(snapshot: snapshot) 586 | 587 | assert_equal 5, context1.eval("a") 588 | assert_equal "function", context1.eval("typeof(Math.sin)") 589 | 590 | GC.start 591 | 592 | context2 = MiniRacer::Context.new(snapshot: snapshot) 593 | 594 | assert_equal 5, context2.eval("a") 595 | assert_equal "function", context2.eval("typeof(Math.sin)") 596 | end 597 | 598 | def test_platform_set_flags_raises_an_exception_if_already_initialized 599 | # makes sure it's initialized 600 | MiniRacer::Snapshot.new 601 | 602 | assert_raises(MiniRacer::PlatformAlreadyInitialized) do 603 | MiniRacer::Platform.set_flags! :noconcurrent_recompilation 604 | end 605 | end 606 | 607 | def test_platform_set_flags_works 608 | context = MiniRacer::Context.new 609 | 610 | assert_raises(MiniRacer::RuntimeError) do 611 | # should fail because of strict mode set for all these tests 612 | context.eval "x = 28" 613 | end 614 | end 615 | 616 | def test_error_on_return_val 617 | v8 = MiniRacer::Context.new 618 | assert_raises(MiniRacer::RuntimeError) do 619 | v8.eval( 620 | 'var o = {}; o.__defineGetter__("bar", function() { return null(); }); o' 621 | ) 622 | end 623 | end 624 | 625 | def test_ruby_based_property_in_rval 626 | v8 = MiniRacer::Context.new 627 | v8.attach "echo", proc { |x| x } 628 | assert_equal( 629 | { "bar" => 42 }, 630 | v8.eval("var o = {get bar() { return echo(42); }}; o") 631 | ) 632 | end 633 | 634 | def test_function_rval 635 | context = MiniRacer::Context.new 636 | context.attach("echo", proc { |msg| msg }) 637 | assert_equal("foo", context.eval("echo('foo')")) 638 | end 639 | 640 | def test_timeout_in_ruby_land 641 | skip "TODO(bnoordhuis) need to think on how to interrupt ruby code" 642 | context = MiniRacer::Context.new(timeout: 50) 643 | context.attach("sleep", proc { sleep 10 }) 644 | assert_raises(MiniRacer::ScriptTerminatedError) do 645 | context.eval('sleep(); "hi";') 646 | end 647 | end 648 | 649 | def test_undef_mem 650 | context = MiniRacer::Context.new(timeout: 5) 651 | 652 | context.attach( 653 | "marsh", 654 | proc do |a, b, c| 655 | if a.is_a?(MiniRacer::FailedV8Conversion) || 656 | b.is_a?(MiniRacer::FailedV8Conversion) || 657 | c.is_a?(MiniRacer::FailedV8Conversion) 658 | return a, b, c 659 | end 660 | 661 | a[rand(10_000).to_s] = "a" 662 | b[rand(10_000).to_s] = "b" 663 | c[rand(10_000).to_s] = "c" 664 | [a, b, c] 665 | end 666 | ) 667 | 668 | assert_raises do 669 | # TODO make it raise the correct exception! 670 | context.eval( 671 | "var a = [{},{},{}]; while(true) { a = marsh(a[0],a[1],a[2]); }" 672 | ) 673 | end 674 | end 675 | 676 | def test_can_dispose_context 677 | context = MiniRacer::Context.new(timeout: 5) 678 | context.dispose 679 | assert_raises(MiniRacer::ContextDisposedError) { context.eval("a") } 680 | end 681 | 682 | def test_estimated_size 683 | if RUBY_ENGINE == "truffleruby" 684 | skip "TruffleRuby does not yet implement heap_stats" 685 | end 686 | context = MiniRacer::Context.new(timeout: 500) 687 | context.eval(<<~JS) 688 | let a='testing'; 689 | let f=function(foo) { foo + 42 }; 690 | 691 | // call `f` a lot to have things JIT'd so that total_heap_size_executable becomes > 0 692 | for (let i = 0; i < 1000000; i++) { f(10); } 693 | JS 694 | 695 | stats = context.heap_stats 696 | # eg: {:total_physical_size=>1280640, :total_heap_size_executable=>4194304, :total_heap_size=>3100672, :used_heap_size=>1205376, :heap_size_limit=>1501560832} 697 | assert_equal( 698 | %i[ 699 | external_memory 700 | heap_size_limit 701 | malloced_memory 702 | number_of_detached_contexts 703 | number_of_native_contexts 704 | peak_malloced_memory 705 | total_available_size 706 | total_global_handles_size 707 | total_heap_size 708 | total_heap_size_executable 709 | total_physical_size 710 | used_global_handles_size 711 | used_heap_size 712 | ].sort, 713 | stats.keys.sort 714 | ) 715 | 716 | assert_equal 0, stats[:external_memory] 717 | assert_equal 0, stats[:number_of_detached_contexts] 718 | stats.delete :external_memory 719 | stats.delete :number_of_detached_contexts 720 | 721 | assert( 722 | stats.values.all? { |v| v > 0 }, 723 | "expecting the isolate to have values for all the vals: actual stats #{stats}" 724 | ) 725 | end 726 | 727 | def test_releasing_memory 728 | context = MiniRacer::Context.new 729 | 730 | context.low_memory_notification 731 | 732 | start_heap = context.heap_stats[:used_heap_size] 733 | 734 | context.eval("'#{"x" * 1_000_000}'") 735 | 736 | context.low_memory_notification 737 | 738 | end_heap = context.heap_stats[:used_heap_size] 739 | 740 | assert( 741 | (end_heap - start_heap).abs < 1000, 742 | "expecting most of the 1_000_000 long string to be freed" 743 | ) 744 | end 745 | 746 | def test_bad_params 747 | assert_raises { MiniRacer::Context.new(random: :thing) } 748 | end 749 | 750 | def test_ensure_gc 751 | context = MiniRacer::Context.new(ensure_gc_after_idle: 1) 752 | context.low_memory_notification 753 | 754 | start_heap = context.heap_stats[:used_heap_size] 755 | 756 | context.eval("'#{"x" * 10_000_000}'") 757 | 758 | sleep 0.01 759 | 760 | end_heap = context.heap_stats[:used_heap_size] 761 | 762 | assert( 763 | (end_heap - start_heap).abs < 1000, 764 | "expecting most of the 1_000_000 long string to be freed" 765 | ) 766 | end 767 | 768 | def test_eval_with_filename 769 | context = MiniRacer::Context.new() 770 | context.eval("var foo = function(){baz();}", filename: "b/c/foo1.js") 771 | 772 | got_error = false 773 | begin 774 | context.eval("foo()", filename: "baz1.js") 775 | rescue MiniRacer::RuntimeError => e 776 | assert_match(/foo1.js/, e.backtrace[0]) 777 | assert_match(/baz1.js/, e.backtrace[1]) 778 | got_error = true 779 | end 780 | 781 | assert(got_error, "should raise") 782 | end 783 | 784 | def test_estimated_size_when_disposed 785 | context = MiniRacer::Context.new(timeout: 50) 786 | context.eval("let a='testing';") 787 | context.dispose 788 | 789 | assert_raises(MiniRacer::ContextDisposedError) { context.heap_stats } 790 | end 791 | 792 | def test_can_dispose 793 | skip "takes too long" 794 | # 795 | # junk_it_up 796 | # 3.times do 797 | # GC.start(full_mark: true, immediate_sweep: true) 798 | # end 799 | end 800 | 801 | def junk_it_up 802 | 1000.times do 803 | context = MiniRacer::Context.new(timeout: 5) 804 | context.dispose 805 | end 806 | end 807 | 808 | def test_attached_recursion 809 | context = MiniRacer::Context.new(timeout: 200) 810 | context.attach("a", proc { |a| a }) 811 | context.attach("b", proc { |a| a }) 812 | 813 | context.eval("const obj = {get r(){ b() }}; a(obj);") 814 | end 815 | 816 | def test_heap_dump 817 | if RUBY_ENGINE == "truffleruby" 818 | skip "TruffleRuby does not yet implement heap_dump" 819 | end 820 | f = Tempfile.new("heap") 821 | path = f.path 822 | f.unlink 823 | 824 | context = MiniRacer::Context.new 825 | context.eval("let x = 1000;") 826 | context.write_heap_snapshot(path) 827 | 828 | dump = File.read(path) 829 | 830 | assert dump.length > 0 831 | 832 | FileUtils.rm(path) 833 | end 834 | 835 | def test_pipe_leak 836 | # in Ruby 2.7 pipes will stay open for longer 837 | # make sure that we clean up early so pipe file 838 | # descriptors are not kept around 839 | context = MiniRacer::Context.new(timeout: 1000) 840 | 10_000.times { |i| context.eval("'hello'") } 841 | end 842 | 843 | def test_symbol_support 844 | context = MiniRacer::Context.new() 845 | if RUBY_ENGINE == "truffleruby" 846 | # This seems the correct behavior, but it was changed in https://github.com/rubyjs/mini_racer/pull/325#discussion_r1907113432 847 | assert_equal :foo, context.eval("Symbol('foo')") 848 | assert_equal :undefined, context.eval("Symbol()") # should not crash 849 | else 850 | assert_equal "foo", context.eval("Symbol('foo')") 851 | assert_nil context.eval("Symbol()") # should not crash 852 | end 853 | end 854 | 855 | def test_infinite_object_js 856 | if RUBY_ENGINE == "truffleruby" 857 | skip "TruffleRuby does not yet implement marshal_stack_depth" 858 | end 859 | context = MiniRacer::Context.new 860 | context.attach("a", proc { |a| a }) 861 | 862 | js = <<~JS 863 | var d=0; 864 | function get(z) { 865 | z.depth=d++; // this isn't necessary to make it infinite, just to make it more obvious that it is 866 | Object.defineProperty(z,'foo',{get(){var r={};return get(r);},enumerable:true}) 867 | return z; 868 | } 869 | a(get({})); 870 | JS 871 | 872 | assert_raises(MiniRacer::RuntimeError) { context.eval(js) } 873 | end 874 | 875 | def test_deep_object_js 876 | if RUBY_ENGINE == "truffleruby" 877 | skip "TruffleRuby does not yet implement marshal_stack_depth" 878 | end 879 | context = MiniRacer::Context.new 880 | context.attach("a", proc { |a| a }) 881 | 882 | # stack depth should be enough to marshal the object 883 | assert_equal [[[]]], context.eval("let arr = [[[]]]; a(arr)") 884 | 885 | # too deep 886 | assert_raises(MiniRacer::RuntimeError) do 887 | context.eval("let arr = [[[[[[[[]]]]]]]]; a(arr)") 888 | end 889 | end 890 | 891 | def test_wasm_ref 892 | if RUBY_ENGINE == "truffleruby" 893 | skip "TruffleRuby does not support WebAssembly" 894 | end 895 | context = MiniRacer::Context.new 896 | expected = {} 897 | actual = 898 | context.eval( 899 | " 900 | var b = [0,97,115,109,1,0,0,0,1,26,5,80,0,95,0,80,0,95,1,127,0,96,0,1,110,96,1,100,2,1,111,96,0,1,100,3,3,4,3,3,2,4,7,26,2,12,99,114,101,97,116,101,83,116,114,117,99,116,0,1,7,114,101,102,70,117,110,99,0,2,9,5,1,3,0,1,0,10,23,3,8,0,32,0,20,2,251,27,11,7,0,65,12,251,0,1,11,4,0,210,0,11,0,44,4,110,97,109,101,1,37,3,0,11,101,120,112,111,114,116,101,100,65,110,121,1,12,99,114,101,97,116,101,83,116,114,117,99,116,2,7,114,101,102,70,117,110,99] 901 | var o = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array(b))).exports 902 | o.refFunc()(o.createStruct) // exotic object 903 | " 904 | ) 905 | assert_equal expected, actual 906 | end 907 | 908 | def test_proxy_support 909 | js = <<~JS 910 | function MyProxy(reference) { 911 | return new Proxy(function() {}, { 912 | get: function(obj, prop) { 913 | if (prop === Symbol.toPrimitive) return reference[prop]; 914 | return new MyProxy(reference.concat(prop)); 915 | }, 916 | apply: function(target, thisArg, argumentsList) { 917 | myFunctionLogger(reference); 918 | } 919 | }); 920 | }; 921 | (new MyProxy([])).function_call(new MyProxy([])-1) 922 | JS 923 | context = MiniRacer::Context.new() 924 | context.attach("myFunctionLogger", ->(property) {}) 925 | context.eval(js) 926 | end 927 | 928 | def test_proxy_uncloneable 929 | context = MiniRacer::Context.new() 930 | expected = { "x" => 42 } 931 | assert_equal expected, context.eval(<<~JS) 932 | const o = {x: 42} 933 | const p = new Proxy(o, {}) 934 | Object.seal(p) 935 | JS 936 | end 937 | 938 | def test_promise 939 | context = MiniRacer::Context.new() 940 | context.eval <<~JS 941 | var x = 0; 942 | async function test() { 943 | return 99; 944 | } 945 | 946 | test().then(v => x = v); 947 | JS 948 | 949 | v = context.eval("x") 950 | assert_equal(v, 99) 951 | end 952 | 953 | def test_webassembly 954 | if RUBY_ENGINE == "truffleruby" 955 | skip "TruffleRuby does not enable WebAssembly by default" 956 | end 957 | context = MiniRacer::Context.new() 958 | context.eval("let instance = null;") 959 | filename = File.expand_path("../support/add.wasm", __FILE__) 960 | context.attach("loadwasm", proc { |f| File.read(filename).each_byte.to_a }) 961 | context.attach("print", proc { |f| puts f }) 962 | 963 | context.eval <<~JS 964 | WebAssembly 965 | .instantiate(new Uint8Array(loadwasm()), { 966 | wasi_snapshot_preview1: { 967 | proc_exit: function() { print("exit"); }, 968 | args_get: function() { return 0 }, 969 | args_sizes_get: function() { return 0 } 970 | } 971 | }) 972 | .then(i => { instance = i["instance"];}) 973 | .catch(e => print(e.toString())); 974 | JS 975 | 976 | context.pump_message_loop while !context.eval("instance") 977 | 978 | assert_equal(3, context.eval("instance.exports.add(1,2)")) 979 | end 980 | 981 | class ReproError < StandardError 982 | def initialize(response) 983 | super("response said #{response.code}") 984 | end 985 | end 986 | 987 | Response = Struct.new(:code) 988 | 989 | def test_exception_objects 990 | context = MiniRacer::Context.new 991 | context.attach("repro", lambda { raise ReproError.new(Response.new(404)) }) 992 | assert_raises(ReproError) { context.eval("repro();") } 993 | end 994 | 995 | def test_timeout 996 | context = MiniRacer::Context.new(timeout: 500, max_memory: 20_000_000) 997 | assert_raises(MiniRacer::ScriptTerminatedError) { context.eval <<~JS } 998 | var doit = () => { 999 | while (true) {} 1000 | } 1001 | doit(); 1002 | JS 1003 | end 1004 | 1005 | def test_eval_returns_unfrozen_string 1006 | context = MiniRacer::Context.new 1007 | result = context.eval("'Hello George!'") 1008 | assert_equal("Hello George!", result) 1009 | assert_equal(false, result.frozen?) 1010 | end 1011 | 1012 | def test_call_returns_unfrozen_string 1013 | context = MiniRacer::Context.new 1014 | context.eval('function hello(name) { return "Hello " + name + "!" }') 1015 | result = context.call("hello", "George") 1016 | assert_equal("Hello George!", result) 1017 | assert_equal(false, result.frozen?) 1018 | end 1019 | 1020 | def test_callback_string_arguments_are_not_frozen 1021 | context = MiniRacer::Context.new 1022 | context.attach("test", proc { |text| text.frozen? }) 1023 | 1024 | frozen = context.eval("test('Hello George!')") 1025 | assert_equal(false, frozen) 1026 | end 1027 | 1028 | def test_threading_safety 1029 | Thread.new { MiniRacer::Context.new.eval("100") }.join 1030 | GC.start 1031 | end 1032 | 1033 | def test_forking 1034 | if RUBY_ENGINE == "truffleruby" 1035 | skip "TruffleRuby forking is not supported" 1036 | else 1037 | `bundle exec ruby test/test_forking.rb` 1038 | assert false, "forking test failed" if $?.exitstatus != 0 1039 | end 1040 | end 1041 | 1042 | def test_poison 1043 | if RUBY_ENGINE == "truffleruby" 1044 | skip "TruffleRuby uses some extra JS code when creating/using a Context which seems to trigger the poison" 1045 | end 1046 | context = MiniRacer::Context.new 1047 | context.eval <<~JS 1048 | const f = () => { throw "poison" } 1049 | const d = {get: f, set: f} 1050 | Object.defineProperty(Array.prototype, "0", d) 1051 | Object.defineProperty(Array.prototype, "1", d) 1052 | JS 1053 | assert_equal 42, context.eval("42") 1054 | end 1055 | 1056 | def test_map 1057 | context = MiniRacer::Context.new 1058 | expected = { "x" => 42, "y" => 43 } 1059 | assert_equal expected, context.eval("new Map([['x', 42], ['y', 43]])") 1060 | if RUBY_ENGINE == "truffleruby" 1061 | # See https://github.com/rubyjs/mini_racer/pull/325#discussion_r1907187166 1062 | expected = [["x", 42], ["y", 43]] 1063 | else 1064 | expected = ["x", 42, "y", 43] 1065 | end 1066 | assert_equal expected, 1067 | context.eval("new Map([['x', 42], ['y', 43]]).entries()") 1068 | expected = %w[x y] 1069 | assert_equal expected, 1070 | context.eval("new Map([['x', 42], ['y', 43]]).keys()") 1071 | expected = [[42], [43]] 1072 | assert_equal expected, 1073 | context.eval("new Map([['x', [42]], ['y', [43]]]).values()") 1074 | end 1075 | 1076 | def test_regexp_string_iterator 1077 | if RUBY_ENGINE == "truffleruby" 1078 | skip "TruffleRuby supports passing any object between JS and Ruby" 1079 | end 1080 | context = MiniRacer::Context.new 1081 | # TODO(bnoordhuis) maybe detect the iterator object and serialize 1082 | # it as a string or array of strings; problem is there is no V8 API 1083 | # to detect regexp string iterator objects 1084 | expected = {} 1085 | assert_equal expected, context.eval("'abc'.matchAll(/./g)") 1086 | end 1087 | 1088 | def test_function_property 1089 | context = MiniRacer::Context.new 1090 | if RUBY_ENGINE == "truffleruby" 1091 | expected = { "m" => { 1 => 2, 3 => 4 }, "s" => {}, "x" => 42 } 1092 | else 1093 | expected = { "m" => { 1 => 2, 3 => 4 }, "s" => [5, 7, 11, 13], "x" => 42 } 1094 | end 1095 | script = <<~JS 1096 | ({ 1097 | f: () => {}, 1098 | m: new Map([[1,2],[3,4]]), 1099 | s: new Set([5,7,11,13]), 1100 | x: 42, 1101 | }) 1102 | JS 1103 | assert_equal expected, context.eval(script) 1104 | end 1105 | 1106 | def test_dates_from_active_support 1107 | require "active_support" 1108 | require "active_support/time" 1109 | context = MiniRacer::Context.new 1110 | Time.zone = "UTC" 1111 | time = Time.current 1112 | context.attach("f", proc { time }) 1113 | assert_in_delta time.to_f, context.call("f").to_f, 0.001 1114 | end 1115 | 1116 | def test_string_encoding 1117 | context = MiniRacer::Context.new 1118 | assert_equal "ä", context.eval("'ä'") 1119 | assert_equal "ok", context.eval("'ok'".encode("ISO-8859-1")) 1120 | assert_equal "ok", context.eval("'ok'".encode("ISO8859-1")) 1121 | assert_equal "ok", context.eval("'ok'".encode("UTF-16LE")) 1122 | assert_equal Encoding::UTF_8, context.eval("'ok'").encoding 1123 | assert_equal Encoding::UTF_8, context.eval("'ok\\uD800\\uDC00'").encoding 1124 | if RUBY_ENGINE != "truffleruby" 1125 | # unmatched surrogate pair, cannot be converted by ruby 1126 | assert_equal Encoding::UTF_16LE, context.eval("'ok\\uD800'").encoding 1127 | end 1128 | end 1129 | 1130 | def test_object_ref 1131 | context = MiniRacer::Context.new 1132 | context.eval("function f(o) { return o }") 1133 | expected = {} 1134 | expected["a"] = expected["b"] = { "x" => 42 } 1135 | actual = context.call("f", expected) 1136 | assert_equal actual, expected 1137 | end 1138 | 1139 | def test_termination_exception 1140 | context = MiniRacer::Context.new 1141 | a = Thread.new { context.stop while true } 1142 | b = Thread.new { context.heap_stats while true } # should not crash/abort 1143 | sleep 1.5 1144 | a.kill 1145 | b.kill 1146 | end 1147 | 1148 | def test_large_integer 1149 | [10_000_000_001, -2**63, 2**63-1].each { |big_int| 1150 | context = MiniRacer::Context.new 1151 | context.attach("test", proc { big_int }) 1152 | result = context.eval("test()") 1153 | assert_equal(result.class, big_int.class) 1154 | assert_equal(result, big_int) 1155 | } 1156 | types = [] 1157 | [2**63/1024-1, 2**63/1024, -2**63/1024+1, -2**63/1024].each { |big_int| 1158 | context = MiniRacer::Context.new 1159 | context.attach("test", proc { big_int }) 1160 | context.attach("type", proc { |arg| types.push(arg) }) 1161 | result = context.eval("const t = test(); type(typeof t); t") 1162 | assert_equal(result.class, big_int.class) 1163 | assert_equal(result, big_int) 1164 | } 1165 | if RUBY_ENGINE == "truffleruby" 1166 | assert_equal(types, %w[number number number number]) 1167 | else 1168 | assert_equal(types, %w[number bigint number bigint]) 1169 | end 1170 | end 1171 | end 1172 | -------------------------------------------------------------------------------- /test/smoke/minimal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mini_racer" 4 | require "libv8-node" 5 | require "rbconfig" 6 | 7 | puts "RbConfig::CONFIG['LIBS']: #{RbConfig::CONFIG["LIBS"]}" 8 | puts "RUBY_VERSION: #{RUBY_VERSION}" 9 | puts "RUBY_PLATFORM: #{RUBY_PLATFORM}" 10 | puts "MiniRacer::VERSION: #{MiniRacer::VERSION}" 11 | puts "MiniRacer::LIBV8_NODE_VERSION: #{MiniRacer::LIBV8_NODE_VERSION}" 12 | puts "Libv8::Node::VERSION: #{Libv8::Node::VERSION}" 13 | puts "Libv8::Node::NODE_VERSION: #{Libv8::Node::NODE_VERSION}" 14 | puts "Libv8::Node::LIBV8_VERSION: #{Libv8::Node::LIBV8_VERSION}" 15 | puts "=" * 80 16 | 17 | require "minitest/autorun" 18 | 19 | class MiniRacerFunctionTest < Minitest::Test 20 | def test_minimal 21 | assert_equal MiniRacer::Context.new.eval("41 + 1"), 42 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/support/add.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyjs/mini_racer/adb2cccaa2fed31c915826874b0a8a97c4bd172f/test/support/add.wasm -------------------------------------------------------------------------------- /test/test_crash.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'mini_racer' 3 | 4 | def test 5 | context = MiniRacer::Context.new(timeout: 10) 6 | context.attach("echo", proc{ |msg| 7 | GC.start 8 | msg 9 | }) 10 | 11 | GC.disable 12 | 100.times { 'foo' } # alloc a handful of objects 13 | GC.enable 14 | 15 | 16 | context.eval("while(true) echo('foo');") rescue nil 17 | 18 | # give some time to clean up 19 | puts "we are done" 20 | end 21 | 22 | def test2 23 | 24 | context = MiniRacer::Context.new(timeout: 5) 25 | 26 | context.attach("marsh", proc do |a, b, c| 27 | return [a,b,c] if a.is_a?(MiniRacer::FailedV8Conversion) || b.is_a?(MiniRacer::FailedV8Conversion) || c.is_a?(MiniRacer::FailedV8Conversion) 28 | 29 | a[rand(10000).to_s] = "a" 30 | b[rand(10000).to_s] = "b" 31 | c[rand(10000).to_s] = "c" 32 | [a,b,c] 33 | end) 34 | 35 | begin 36 | context.eval("var a = [{},{},{}]; while(true) { a = marsh(a[0],a[1],a[2]); }") 37 | rescue 38 | p "BOOM" 39 | end 40 | 41 | end 42 | 43 | def test3 44 | snapshot = MiniRacer::Snapshot.new('Math.sin = 1;') 45 | 46 | begin 47 | snapshot.warmup!('var a = Math.sin(1);') 48 | rescue 49 | # do nothing 50 | end 51 | 52 | context = MiniRacer::Context.new(snapshot: snapshot) 53 | 54 | assert_equal 1, context.eval('Math.sin') 55 | end 56 | 57 | 58 | 500_000.times do 59 | test2 60 | end 61 | 62 | exit 63 | 64 | test 65 | GC.start 66 | 67 | test 68 | 69 | 10.times{GC.start} 70 | test 71 | 72 | -------------------------------------------------------------------------------- /test/test_forking.rb: -------------------------------------------------------------------------------- 1 | # use bundle exec to run this script 2 | # 3 | require 'mini_racer' 4 | 5 | MiniRacer::Platform.set_flags! :single_threaded 6 | 7 | @ctx = MiniRacer::Context.new 8 | @ctx.eval("var a = 1+1") 9 | 10 | def trigger_gc 11 | puts "a" 12 | ctx = MiniRacer::Context.new 13 | puts "b" 14 | ctx.eval("var a = #{('x' * 100000).inspect}") 15 | puts "c" 16 | ctx.eval("a = undefined") 17 | puts "d" 18 | ctx.low_memory_notification 19 | puts "f" 20 | puts "done triggering" 21 | end 22 | 23 | trigger_gc 24 | 25 | MiniRacer::Context.new.dispose 26 | 27 | if Process.respond_to?(:fork) 28 | Process.wait fork { puts @ctx.eval("a"); @ctx.dispose; puts Process.pid; trigger_gc; puts "done #{Process.pid}" } 29 | exit $?.exitstatus || 1 30 | end 31 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "mini_racer" 3 | 4 | require "minitest/autorun" 5 | -------------------------------------------------------------------------------- /test/test_leak.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'mini_racer' 3 | require 'objspace' 4 | 5 | def has_contexts 6 | ObjectSpace.each_object(MiniRacer::Context).count > 0 7 | end 8 | 9 | def clear 10 | while has_contexts 11 | GC.start 12 | end 13 | end 14 | 15 | def test 16 | context = MiniRacer::Context.new 17 | context.attach("add", proc{|a,b| a+b}) 18 | context.eval('1+1') 19 | context.eval('"1"') 20 | context.eval('a=function(){}') 21 | context.eval('a={a: 1}') 22 | context.eval('a=[1,2,"3"]') 23 | context.eval('add(1,2)') 24 | end 25 | 26 | 27 | def start 28 | n = 100 29 | 30 | puts "Running #{n} contexts" 31 | 32 | n.times do 33 | test 34 | clear 35 | end 36 | 37 | puts "Ensuring garbage is collected" 38 | 39 | 40 | puts "Done" 41 | end 42 | 43 | start 44 | -------------------------------------------------------------------------------- /test/test_multithread.rb: -------------------------------------------------------------------------------- 1 | # use bundle exec to run this script 2 | # 3 | require 'securerandom' 4 | 5 | 6 | context = nil 7 | 8 | Thread.new do 9 | require "mini_racer" 10 | context = MiniRacer::Context.new 11 | end.join 12 | 13 | Thread.new do 14 | context.low_memory_notification 15 | 16 | start_heap = context.heap_stats[:used_heap_size] 17 | 18 | context.eval("'#{"x" * 1_000_000_0}'") 19 | 20 | 21 | end_heap = context.heap_stats[:used_heap_size] 22 | 23 | p end_heap - start_heap 24 | end.join 25 | 26 | Thread.new do 27 | 10.times do 28 | context.low_memory_notification 29 | end 30 | end_heap = context.heap_stats[:used_heap_size] 31 | p end_heap 32 | end.join 33 | exit 34 | 35 | 36 | ctx = nil 37 | 38 | big_eval = +"" 39 | 40 | (0..100).map do |j| 41 | big_regex = (1..10000).map { |i| SecureRandom.hex }.join("|") 42 | big_eval << "X[#{j}] = /#{big_regex}/;\n" 43 | end 44 | 45 | big_eval = <<~JS 46 | const X = []; 47 | #{big_eval} 48 | 49 | function test(i, str) { 50 | return X[i].test(str); 51 | } 52 | 53 | null; 54 | JS 55 | 56 | Thread 57 | .new do 58 | require "mini_racer" 59 | ctx = MiniRacer::Context.new 60 | ctx.eval("var a = 1+1") 61 | ctx.eval(big_eval) 62 | end 63 | .join 64 | 65 | 3.times { GC.start } 66 | 67 | 68 | Thread 69 | .new do 70 | 10.times do 71 | 10.times { |i| p ctx.eval "test(#{i}, '#{SecureRandom.hex}')" } 72 | end 73 | end 74 | .join 75 | 76 | GC.start 77 | --------------------------------------------------------------------------------