├── .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 | [](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml) 
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 |
--------------------------------------------------------------------------------