1
0
Fork 0
mirror of https://github.com/rubyjs/mini_racer synced 2023-03-27 23:21:28 -04:00

Compare commits

...

19 commits

Author SHA1 Message Date
Sebastian Cohnen
b05d6914c5
adds note on Windows support to README (#268)
Fixes #252
2023-01-05 12:44:36 +11:00
Benoit Daloze
719abdb1a4
Mention all the requirements for MiniRacer on TruffleRuby (#257)
Raise there instead of waiting for the `@context.eval` below, for a clearer error.
2022-08-17 10:45:08 +10:00
Sam Saffron
9935d5a65a
changelog and version bump 2022-08-16 16:59:55 +10:00
Gerhard Schlager
3d110768ee
Fix GitHub Actions and add tests for mutable strings (#256)
* Fix GitHub Actions

* Disable tests with `musl` for arm64 because there's no `libv8-node` gem for aarch64-linux-musl at the moment
* Fix tests on older Ruby versions by running `gem update --system`
* Removes macOS 10.15 (it will be unsupported by 2022-08-30 anyway)
* Adds macOS 12 to the matrix
* Run TruffleRuby job on macOS as well
* Use `ruby/setup-ruby` action where possible and update `actions/checkout` to v3
* Run tests for pull requests

* Raise sleep duration in tests to fix CI failures on macOS

* Add tests for mutable string arguments and return values
2022-07-22 08:45:20 +10:00
Brandon Fish
e0f5a7ac66
FEATURE: Implement MiniRacer for TruffleRuby (#253) 2022-07-21 15:02:32 +10:00
Jun Jiang
7419fd154e
fix an incorrect syntax (#246) 2022-03-22 15:45:40 +11:00
Sam Saffron
4414ea40f9
hide all libv8 symbols on ELF targets
This gives a nice binary size reduction and may allow us to eliminate
the mini_racer_loader shim:

         text           data    bss
before:  63987419       718108  110676
 after:  56535503       454196  110676

append_ldflags is available in Ruby 2.3+ mkmf.rb, allowing us
to probe for supported LDFLAGS without extra conditionals.
2022-02-24 11:26:01 +11:00
Sam Saffron
9cf4ed6d92
Ruby internals are optimized for common encodings such as UTF-8,
whereas rb_enc_find requires an extra hash lookup.  That said,
I've yet to measure an improvement in current benchmarks, though
binary size is reduced:

             text          data     bss
before: 63988228         718148  110676
 after: 63987419         718108  110676
2022-02-22 15:43:35 +11:00
Sam Saffron
bdb0977d81
simplify timeout implementation
Merely closing the pipe is enough to trigger a wakeup from
IO#wait_readable, there's no need to make a write(2) syscall nor
copy data in/out of the kernel.  We'll use the Ruby 2.3+ 'foo&.method'
calls in a few places to simplify code, as well.
2022-02-22 15:41:23 +11:00
Sam Saffron
5266fec597
Snapshot: remove unused `str' arg for .dump and .size 2022-02-22 15:40:03 +11:00
Sam Saffron
8b4d03eb76
use Check_Type for type-checking
Check_Type is less code and used throughout the Ruby ecosystem and
raises TypeError on incorrect argument types.  This is a minor behavior
change as it replaces some ArgumentError exceptions with TypeError, but
is more consistent with other Ruby libraries.
2022-02-22 15:38:50 +11:00
Yuji Hanamura
493e1bc1fc
Fix typo in CHANGELOG (#241) 2022-02-18 20:50:56 +11:00
Sebastian Cohnen
79f1379ae2
removes travis-ci.org references since it's out of service since 2021-06-15 (#238) 2022-02-14 18:17:28 +11:00
Sebastian Cohnen
15051f2298
Readme: Add note about V8 single threaded platform mode (#239)
* changes link to supported Rubies to "Ruby Maintenance Branches"

This pages lists the support status of different Ruby versions
alongside with a note on the respective EOLs.

* adds note to "Fork safety" about V8 single threaded platform mode
2022-02-14 17:50:02 +11:00
Sebastian Cohnen
38820e54fd
adds Troubleshooting section to the README (#240) 2022-02-14 17:48:59 +11:00
Sam Saffron
b8638e8899
use C++11 atomics to check for RubyVM exit
There's no need to deal with pthread_rwlock for a single
variable since we already rely on C++11 atomics elsewhere in our
code.
2022-02-11 09:28:55 +11:00
Sam Saffron
4487f11202
avoid malloc in single-ref case of free_context
Triggering an allocation inside a `free' callback doesn't help
in low memory situations; so operate directly on the ContextInfo
being freed when possible instead of creating a short-lived copy
of it.

For the `isolate_info->refs() > 1' case, we'll leak slightly
less memory by freeing the short-lived allocation in case
pthread_create(3) fails.
2022-02-11 09:24:42 +11:00
Sam Saffron
9a409cf792
use IO#wait_readable in timeout implementation
IO.select creates more garbage and gets slower on high-numbered
FDs.  IO#wait_readable can use ppoll(2) to achieve consistent
performance regardless of FD value.
2022-02-11 09:22:41 +11:00
Sebastian Cohnen
746bfddfea
adds Ruby 3.1 to CI (#233) 2022-01-21 17:54:40 +11:00
14 changed files with 597 additions and 180 deletions

View file

@ -1,48 +1,92 @@
name: Test
name: Tests
on:
- push
pull_request:
push:
branches:
- master
jobs:
test-truffleruby:
strategy:
fail-fast: false
matrix:
os:
- "macos-11"
- "macos-12"
- "ubuntu-20.04"
ruby:
- "truffleruby+graalvm-head"
name: ${{ matrix.os }} - ${{ matrix.ruby }}
runs-on: ${{ matrix.os }}
env:
TRUFFLERUBYOPT: "--jvm --polyglot"
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Install GraalVM JS component
run: gu install js
- name: Compile
run: bundle exec rake compile
- name: Test
run: bundle exec rake test
test-darwin:
strategy:
fail-fast: false
matrix:
os:
- '10.15'
- '11.0'
platform:
- x86_64
# arm64
name: Test (darwin)
runs-on: macos-${{ matrix.os }}
- "macos-11"
- "macos-12"
ruby:
- "ruby-2.6"
- "ruby-2.7"
- "ruby-3.0"
- "ruby-3.1"
name: ${{ matrix.os }} - ${{ matrix.ruby }}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Bundle
run: bundle install
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Compile
run: bundle exec rake compile
- name: Test
run: bundle exec rake test
test-linux:
strategy:
fail-fast: false
matrix:
ruby:
- '2.6'
- '2.7'
- '3.0'
- "2.6"
- "2.7"
- "3.0"
- "3.1"
platform:
- amd64
- arm64
# arm
# ppc64le
# s390x
- "amd64"
- "arm64"
libc:
- gnu
- musl
name: Test (linux)
- "gnu"
- "musl"
exclude:
# there's no libv8-node (v16) for aarch64-linux-musl at the moment
- platform: "arm64"
libc: "musl"
name: linux-${{ matrix.platform }} - ruby-${{ matrix.ruby }} - ${{ matrix.libc }}
runs-on: ubuntu-20.04
steps:
- name: Enable ${{ matrix.platform }} platform
id: qemu
@ -67,9 +111,11 @@ jobs:
echo "::set-output name=id::$(cat container_id)"
- name: Install Alpine system dependencies
if: ${{ matrix.libc == 'musl' }}
run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apk add --no-cache build-base linux-headers bash python2 python3 git curl tar clang binutils-gold
run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apk add --no-cache build-base bash git
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Update Rubygems
run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} gem update --system
- name: Bundle
run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle install
- name: Compile

View file

@ -1,23 +0,0 @@
language: ruby
os: linux
rvm:
- 2.6
- 2.7
- 3.0
- ruby-head
arch:
- amd64
- arm64
jobs:
include:
- rvm: 2.6
os: osx
osx_image: xcode11.3
- rvm: 2.6
os: osx
osx_image: xcode12.2
- rvm: 2.7
os: osx
osx_image: xcode12.2
dist: xenial
cache: bundler

View file

@ -1,4 +1,14 @@
- 17-01-2021
- 16-08-2022
- 0.6.3
- Truffle ruby support! Thanks to Brandon Fish and the truffle team
- Hide libv8 symbols on ELF targets
- Slightly shrunk binary size
- Simplified timeout implementation
- Some stability fixes
- 17-01-2022
- 0.6.2

View file

@ -1,6 +1,6 @@
# MiniRacer
[![Build Status](https://travis-ci.org/rubyjs/mini_racer.svg?branch=master)](https://travis-ci.org/rubyjs/mini_racer)
[![Test](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml/badge.svg)](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml)
Minimal, modern embedded V8 for Ruby.
@ -10,12 +10,14 @@ It was created as an alternative to the excellent [therubyracer](https://github.
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.
### A note about Ruby version Support
### Supported Ruby Versions
MiniRacer only supports non-EOL versions of Ruby. See [Ruby](https://www.ruby-lang.org/en/downloads) to see the list of non-EOL Rubies.
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.
MiniRacer **does not support** [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).
## Features
### Simple eval for JavaScript
@ -112,16 +114,21 @@ context.eval('bar()', filename: 'a/bar.js')
### Fork safety
Some Ruby web servers employ forking (for example unicorn or puma in clustered mode). V8 is not fork safe.
Sadly Ruby does not have support for fork notifications per [#5446](https://bugs.ruby-lang.org/issues/5446).
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).
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`:
```ruby
MiniRacer::Platform.set_flags!(:single_threaded)
```
If you want to ensure your application does not leak memory after fork either:
1. Ensure no MiniRacer::Context objects are created in the master process
1. Ensure no `MiniRacer::Context` objects are created in the master process
Or
2. Dispose manually of all MiniRacer::Context objects prior to forking
2. Dispose manually of all `MiniRacer::Context` objects prior to forking
```ruby
# before fork
@ -419,18 +426,15 @@ Or install it yourself as:
**Note** using v8.h and compiling MiniRacer requires a C++11 standard compiler, more specifically clang 3.5 (or later) or GCC 6.3 (or later).
## Travis-ci
### Troubleshooting
To install `mini-racer` you will need a version of GCC that supports C++11 (GCC 6.3) this is included by default in ubuntu trusty based images.
If you have a problem installing mini_racer, please consider the following steps:
Travis today ships by default with a precise based image. Precise Pangolin (12.04 LTS) was first released in August 2012. Even though you can install GCC 6.3 on precise the simpler approach is to opt for the trusty based image.
Add this to your .travis.yml file:
```
- sudo: required
- dist: trusty
```
* make sure you try the latest released version of mini_racer
* make sure you have Rubygems >= 3.2.13 and bundler >= 2.2.13 installed via `gem update --system`
* if you are using bundler, make sure to have `PLATFORMS` set correctly in `Gemfile.lock` via `bundle lock --add-platform`
* make sure to recompile/reinstall `mini_racer` and `libv8-node` after system upgrades (for example via `gem uninstall --all mini_racer libv8-node`)
* make sure you are on the latest patch/teeny version of a supported Ruby branch
## Similar Projects

View file

@ -1,6 +1,5 @@
require "bundler/gem_tasks"
require "rake/testtask"
require "rake/extensiontask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
@ -11,8 +10,21 @@ end
task :default => [:compile, :test]
gem = Gem::Specification.load( File.dirname(__FILE__) + '/mini_racer.gemspec' )
Rake::ExtensionTask.new( 'mini_racer_loader', gem )
Rake::ExtensionTask.new( 'mini_racer_extension', gem )
if RUBY_ENGINE == "truffleruby"
task :compile do
# noop
end
task :clean do
# noop
end
else
require 'rake/extensiontask'
Rake::ExtensionTask.new( 'mini_racer_loader', gem )
Rake::ExtensionTask.new( 'mini_racer_extension', gem )
end
# via http://blog.flavorjon.es/2009/06/easily-valgrind-gdb-your-ruby-c.html

View file

@ -1,4 +1,10 @@
require 'mkmf'
if RUBY_ENGINE == "truffleruby"
File.write("Makefile", dummy_makefile($srcdir).join(""))
return
end
require_relative '../../lib/mini_racer/version'
gem 'libv8-node', MiniRacer::LIBV8_NODE_VERSION
require 'libv8-node'
@ -70,6 +76,9 @@ end
Libv8::Node.configure_makefile
# --exclude-libs is only for i386 PE and ELF targeted ports
append_ldflags("-Wl,--exclude-libs=ALL ")
if enable_config('asan')
$CXXFLAGS.insert(0, " -fsanitize=address ")
$LDFLAGS.insert(0, " -fsanitize=address ")

View file

@ -305,8 +305,7 @@ static std::unique_ptr<Platform> current_platform = NULL;
static std::mutex platform_lock;
static pthread_attr_t *thread_attr_p;
static pthread_rwlock_t exit_lock = PTHREAD_RWLOCK_INITIALIZER;
static bool ruby_exiting = false; // guarded by exit_lock
static std::atomic_int ruby_exiting(0);
static bool single_threaded = false;
static void mark_context(void *);
@ -335,10 +334,7 @@ static const rb_data_type_t isolate_type = {
static VALUE rb_platform_set_flag_as_str(VALUE _klass, VALUE flag_as_str) {
bool platform_already_initialized = false;
if(TYPE(flag_as_str) != T_STRING) {
rb_raise(rb_eArgError, "wrong type argument %" PRIsVALUE" (should be a string)",
rb_obj_class(flag_as_str));
}
Check_Type(flag_as_str, T_STRING);
platform_lock.lock();
@ -664,11 +660,7 @@ static VALUE convert_v8_to_ruby(Isolate* isolate, Local<Context> context,
v8::String::Utf8Value symbol_name(isolate,
Local<Symbol>::Cast(value)->Name());
VALUE str_symbol = rb_enc_str_new(
*symbol_name,
symbol_name.length(),
rb_enc_find("utf-8")
);
VALUE str_symbol = rb_utf8_str_new(*symbol_name, symbol_name.length());
return rb_str_intern(str_symbol);
}
@ -679,7 +671,7 @@ static VALUE convert_v8_to_ruby(Isolate* isolate, Local<Context> context,
return Qnil;
} else {
Local<String> rstr = rstr_maybe.ToLocalChecked();
return rb_enc_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(isolate), rb_enc_find("utf-8"));
return rb_utf8_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(isolate));
}
}
@ -861,7 +853,7 @@ StartupData warm_up_snapshot_data_blob(StartupData cold_snapshot_blob,
return result;
}
static VALUE rb_snapshot_size(VALUE self, VALUE str) {
static VALUE rb_snapshot_size(VALUE self) {
SnapshotInfo* snapshot_info;
TypedData_Get_Struct(self, SnapshotInfo, &snapshot_type, snapshot_info);
@ -872,10 +864,7 @@ static VALUE rb_snapshot_load(VALUE self, VALUE str) {
SnapshotInfo* snapshot_info;
TypedData_Get_Struct(self, SnapshotInfo, &snapshot_type, snapshot_info);
if(TYPE(str) != T_STRING) {
rb_raise(rb_eArgError, "wrong type argument %" PRIsVALUE " (should be a string)",
rb_obj_class(str));
}
Check_Type(str, T_STRING);
init_v8();
@ -891,7 +880,7 @@ static VALUE rb_snapshot_load(VALUE self, VALUE str) {
return Qnil;
}
static VALUE rb_snapshot_dump(VALUE self, VALUE str) {
static VALUE rb_snapshot_dump(VALUE self) {
SnapshotInfo* snapshot_info;
TypedData_Get_Struct(self, SnapshotInfo, &snapshot_type, snapshot_info);
@ -902,10 +891,7 @@ static VALUE rb_snapshot_warmup_unsafe(VALUE self, VALUE str) {
SnapshotInfo* snapshot_info;
TypedData_Get_Struct(self, SnapshotInfo, &snapshot_type, snapshot_info);
if(TYPE(str) != T_STRING) {
rb_raise(rb_eArgError, "wrong type argument %" PRIsVALUE " (should be a string)",
rb_obj_class(str));
}
Check_Type(str, T_STRING);
init_v8();
@ -1084,8 +1070,7 @@ static VALUE convert_result_to_ruby(VALUE self /* context */,
// If we were terminated or have the memory softlimit flag set
if (marshal_stack_maxdepth_reached) {
ruby_exception = rb_eScriptRuntimeError;
std::string msg = std::string("Marshal object depth too deep. Script terminated.");
message = rb_enc_str_new(msg.c_str(), msg.length(), rb_enc_find("utf-8"));
message = rb_utf8_str_new_literal("Marshal object depth too deep. Script terminated.");
} else if (result.terminated || mem_softlimit_reached) {
ruby_exception = mem_softlimit_reached ? rb_eV8OutOfMemoryError : rb_eScriptTerminatedError;
} else {
@ -1120,7 +1105,7 @@ static VALUE convert_result_to_ruby(VALUE self /* context */,
if (result.json) {
Local<String> rstr = tmp->ToString(p_ctx->Get(isolate)).ToLocalChecked();
VALUE json_string = rb_enc_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(isolate), rb_enc_find("utf-8"));
VALUE json_string = rb_utf8_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(isolate));
ret = rb_funcall(rb_mJSON, rb_intern("parse"), 1, json_string);
} else {
StackCounter::Reset(isolate);
@ -1149,13 +1134,10 @@ static VALUE rb_context_eval_unsafe(VALUE self, VALUE str, VALUE filename) {
TypedData_Get_Struct(self, ContextInfo, &context_type, context_info);
Isolate* isolate = context_info->isolate_info->isolate;
if(TYPE(str) != T_STRING) {
rb_raise(rb_eArgError, "wrong type argument %" PRIsVALUE " (should be a string)",
rb_obj_class(str));
}
if(filename != Qnil && TYPE(filename) != T_STRING) {
rb_raise(rb_eArgError, "wrong type argument %" PRIsVALUE " (should be nil or a string)",
rb_obj_class(filename));
Check_Type(str, T_STRING);
if (!NIL_P(filename)) {
Check_Type(filename, T_STRING);
}
{
@ -1474,42 +1456,33 @@ static void free_context_raw(void *arg) {
if (isolate_info) {
isolate_info->release();
}
xfree(context_info);
}
static void *free_context_thr(void* arg) {
if (pthread_rwlock_tryrdlock(&exit_lock) != 0) {
return NULL;
if (ruby_exiting.load() == 0) {
free_context_raw(arg);
xfree(arg);
}
if (ruby_exiting) {
return NULL;
}
free_context_raw(arg);
pthread_rwlock_unlock(&exit_lock);
return NULL;
}
// destroys everything except freeing the ContextInfo struct (see deallocate())
static void free_context(ContextInfo* context_info) {
IsolateInfo* isolate_info = context_info->isolate_info;
ContextInfo* context_info_copy = ALLOC(ContextInfo);
context_info_copy->isolate_info = context_info->isolate_info;
context_info_copy->context = context_info->context;
if (isolate_info && isolate_info->refs() > 1) {
pthread_t free_context_thread;
ContextInfo* context_info_copy = ALLOC(ContextInfo);
context_info_copy->isolate_info = context_info->isolate_info;
context_info_copy->context = context_info->context;
if (pthread_create(&free_context_thread, thread_attr_p,
free_context_thr, (void*)context_info_copy)) {
fprintf(stderr, "WARNING failed to release memory in MiniRacer, thread to release could not be created, process will leak memory\n");
xfree(context_info_copy);
}
} else {
free_context_raw(context_info_copy);
free_context_raw(context_info);
}
context_info->context = NULL;
@ -1782,9 +1755,7 @@ static VALUE rb_context_call_unsafe(int argc, VALUE *argv, VALUE self) {
}
VALUE function_name = argv[0];
if (TYPE(function_name) != T_STRING) {
rb_raise(rb_eTypeError, "first argument should be a String");
}
Check_Type(function_name, T_STRING);
char *fname = RSTRING_PTR(function_name);
if (!fname) {
@ -1870,12 +1841,7 @@ static VALUE rb_context_create_isolate_value(VALUE self) {
static void set_ruby_exiting(VALUE value) {
(void)value;
int res = pthread_rwlock_wrlock(&exit_lock);
ruby_exiting = true;
if (res == 0) {
pthread_rwlock_unlock(&exit_lock);
}
ruby_exiting.store(1);
}
extern "C" {
@ -1941,9 +1907,5 @@ extern "C" {
thread_attr_p = &attr;
}
}
auto on_fork_for_child = []() {
exit_lock = PTHREAD_RWLOCK_INITIALIZER;
};
pthread_atfork(nullptr, nullptr, on_fork_for_child);
}
}

View file

@ -1,5 +1,10 @@
require 'mkmf'
if RUBY_ENGINE == "truffleruby"
File.write("Makefile", dummy_makefile($srcdir).join(""))
return
end
extension_name = 'mini_racer_loader'
dir_config extension_name

View file

@ -1,17 +1,22 @@
require "mini_racer/version"
require "mini_racer_loader"
require "pathname"
ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
ext_path = Gem.loaded_specs['mini_racer'].require_paths
.map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
if RUBY_ENGINE == "truffleruby"
require "mini_racer/truffleruby"
else
require "mini_racer_loader"
ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
ext_path = Gem.loaded_specs['mini_racer'].require_paths
.map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
MiniRacer::Loader.load(ext_found.to_s)
raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
MiniRacer::Loader.load(ext_found.to_s)
end
require "thread"
require "json"
require "io/wait"
module MiniRacer
@ -202,7 +207,7 @@ module MiniRacer
end
if !(File === f)
raise ArgumentError("file_or_io")
raise ArgumentError, "file_or_io"
end
write_heap_snapshot_unsafe(f)
@ -349,7 +354,7 @@ module MiniRacer
t = Thread.new do
begin
result = IO.select([rp],[],[],(@timeout/1000.0))
result = rp.wait_readable(@timeout/1000.0)
if !result
mutex.synchronize do
stop unless done
@ -366,7 +371,7 @@ module MiniRacer
done = true
end
wp.write("done")
wp.close
# ensure we do not leak a thread in state
t.join
@ -375,12 +380,9 @@ module MiniRacer
rval
ensure
# exceptions need to be handled
if t && wp
wp.write("done")
t.join
end
wp.close if wp
rp.close if rp
wp&.close
t&.join
rp&.close
end
def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:)

View file

@ -0,0 +1,355 @@
# frozen_string_literal: true
module MiniRacer
class Context
class ExternalFunction
private
def notify_v8
name = @name.encode(::Encoding::UTF_8)
wrapped = lambda do |*args|
converted = @parent.send(:convert_js_to_ruby, args)
begin
result = @callback.call(*converted)
rescue Polyglot::ForeignException => e
e = RuntimeError.new(e.message)
e.set_backtrace(e.backtrace)
@parent.instance_variable_set(:@current_exception, e)
raise e
rescue => e
@parent.instance_variable_set(:@current_exception, e)
raise e
end
@parent.send(:convert_ruby_to_js, result)
end
if @parent_object.nil?
# set global name to proc
result = @parent.eval_in_context('this')
result[name] = wrapped
else
parent_object_eval = @parent_object_eval.encode(::Encoding::UTF_8)
begin
result = @parent.eval_in_context(parent_object_eval)
rescue Polyglot::ForeignException, StandardError => e
raise ParseError, "Was expecting #{@parent_object} to be an object", e.backtrace
end
result[name] = wrapped
# set evaluated object results name to proc
end
end
end
def heap_stats
{
total_physical_size: 0,
total_heap_size_executable: 0,
total_heap_size: 0,
used_heap_size: 0,
heap_size_limit: 0,
}
end
def stop
if @entered
@context.stop
@stopped = true
stop_attached
end
end
private
@context_initialized = false
@use_strict = false
def init_unsafe(isolate, snapshot)
unless defined?(Polyglot::InnerContext)
raise "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version"
end
unless Polyglot.languages.include? "js"
raise "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`\n" \
"You also need to install the 'js' component with 'gu install js' on GraalVM 22.2+\n" \
"Note that you need TruffleRuby+GraalVM and not just the TruffleRuby standalone to use MiniRacer"
end
@context = Polyglot::InnerContext.new(on_cancelled: -> {
raise ScriptTerminatedError, 'JavaScript was terminated (either by timeout or explicitly)'
})
Context.instance_variable_set(:@context_initialized, true)
@js_object = @context.eval('js', 'Object')
@isolate_mutex = Mutex.new
@stopped = false
@entered = false
@has_entered = false
@current_exception = nil
if isolate && snapshot
isolate.instance_variable_set(:@snapshot, snapshot)
end
if snapshot
@snapshot = snapshot
elsif isolate
@snapshot = isolate.instance_variable_get(:@snapshot)
else
@snapshot = nil
end
@is_object_or_array_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
[
(x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
(x) => { return x instanceof Date },
(x) => { return x.getTime(x) },
(x) => { return typeof x === 'symbol' },
(x) => { var r = x.description; return r === undefined ? 'undefined' : r },
(x) => { return new Date(x) },
(x) => { return new Array(x) },
]
CODE
end
def dispose_unsafe
@context.close
end
def eval_unsafe(str, filename)
@entered = true
if !@has_entered && @snapshot
snapshot_src = encode(@snapshot.instance_variable_get(:@source))
begin
eval_in_context(snapshot_src, filename)
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
end
@has_entered = true
raise RuntimeError, "TruffleRuby does not support eval after stop" if @stopped
raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
raise TypeError, "wrong type argument #{filename.class} (should be a string)" unless filename.nil? || filename.is_a?(String)
str = encode(str)
begin
translate do
eval_in_context(str, filename)
end
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
rescue ::RuntimeError => e
if @current_exception
e = @current_exception
@current_exception = nil
raise e
else
raise e, e.message
end
end
ensure
@entered = false
end
def call_unsafe(function_name, *arguments)
@entered = true
if !@has_entered && @snapshot
src = encode(@snapshot.instance_variable_get(:source))
begin
eval_in_context(src)
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
end
@has_entered = true
raise RuntimeError, "TruffleRuby does not support call after stop" if @stopped
begin
translate do
function = eval_in_context(function_name)
function.call(*convert_ruby_to_js(arguments))
end
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
ensure
@entered = false
end
def create_isolate_value
# Returning a dummy object since TruffleRuby does not have a 1-1 concept with isolate.
# However, code and ASTs are shared between contexts.
Isolate.new
end
def isolate_mutex
@isolate_mutex
end
def translate
convert_js_to_ruby yield
rescue Object => e
message = e.message
if @current_exception
raise @current_exception
elsif e.message && e.message.start_with?('SyntaxError:')
error_class = MiniRacer::ParseError
elsif e.is_a?(MiniRacer::ScriptTerminatedError)
error_class = MiniRacer::ScriptTerminatedError
else
error_class = MiniRacer::RuntimeError
end
if error_class == MiniRacer::RuntimeError
bls = e.backtrace_locations&.select { |bl| bl&.source_location&.language == 'js' }
if bls && !bls.empty?
if '(eval)' != bls[0].path
message = "#{e.message}\n at #{bls[0]}\n" + bls[1..].map(&:to_s).join("\n")
else
message = "#{e.message}\n" + bls.map(&:to_s).join("\n")
end
end
raise error_class, message
else
raise error_class, message, e.backtrace
end
end
def convert_js_to_ruby(value)
case value
when true, false, Integer, Float
value
else
if value.nil?
nil
elsif value.respond_to?(:call)
MiniRacer::JavaScriptFunction.new
elsif value.respond_to?(:to_str)
value.to_str.dup
elsif value.respond_to?(:to_ary)
value.to_ary.map do |e|
if e.respond_to?(:call)
nil
else
convert_js_to_ruby(e)
end
end
elsif time?(value)
js_date_to_time(value)
elsif symbol?(value)
js_symbol_to_symbol(value)
else
object = value
h = {}
object.instance_variables.each do |member|
v = object[member]
unless v.respond_to?(:call)
h[member.to_s] = convert_js_to_ruby(v)
end
end
h
end
end
end
def object_or_array?(val)
@is_object_or_array_func.call(val)
end
def time?(value)
@is_time_func.call(value)
end
def js_date_to_time(value)
millis = @js_date_to_time_func.call(value)
Time.at(Rational(millis, 1000))
end
def symbol?(value)
@is_symbol_func.call(value)
end
def js_symbol_to_symbol(value)
@js_symbol_to_symbol_func.call(value).to_s.to_sym
end
def js_new_date(value)
@js_new_date_func.call(value)
end
def js_new_array(size)
@js_new_array_func.call(size)
end
def convert_ruby_to_js(value)
case value
when nil, true, false, Integer, Float
value
when Array
ary = js_new_array(value.size)
value.each_with_index do |v, i|
ary[i] = convert_ruby_to_js(v)
end
ary
when Hash
h = @js_object.new
value.each_pair do |k, v|
h[convert_ruby_to_js(k.to_s)] = convert_ruby_to_js(v)
end
h
when String, Symbol
Truffle::Interop.as_truffle_string value
when Time
js_new_date(value.to_f * 1000)
when DateTime
js_new_date(value.to_time.to_f * 1000)
else
"Undefined Conversion"
end
end
def encode(string)
raise ArgumentError unless string
string.encode(::Encoding::UTF_8)
end
class_eval <<-'RUBY', "(mini_racer)", 1
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
RUBY
end
class Isolate
def init_with_snapshot(snapshot)
# TruffleRuby does not have a 1-1 concept with isolate.
# However, isolate can hold a snapshot, and code and ASTs are shared between contexts.
@snapshot = snapshot
end
def low_memory_notification
GC.start
end
def idle_notification(idle_time)
true
end
end
class Platform
def self.set_flag_as_str!(flag)
raise TypeError, "wrong type argument #{flag.class} (should be a string)" unless flag.is_a?(String)
raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.instance_variable_get(:@context_initialized)
Context.instance_variable_set(:@use_strict, true) if "--use_strict" == flag
end
end
class Snapshot
def load(str)
raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
# Intentionally noop since TruffleRuby mocks the snapshot API
end
def warmup_unsafe!(src)
raise TypeError, "wrong type argument #{src.class} (should be a string)" unless src.is_a?(String)
# Intentionally noop since TruffleRuby mocks the snapshot API
# by replaying snapshot source before the first eval/call
self
end
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
module MiniRacer
VERSION = "0.6.2"
VERSION = "0.6.3"
LIBV8_NODE_VERSION = "~> 16.10.0.0"
end

View file

@ -35,7 +35,8 @@ class MiniRacerFunctionTest < Minitest::Test
context.call('f', 1)
end
assert_equal err.message, 'Error: foo bar'
assert_match(/1:23/, err.backtrace[0])
assert_match(/1:23/, err.backtrace[0]) unless RUBY_ENGINE == "truffleruby"
assert_match(/1:/, err.backtrace[0]) if RUBY_ENGINE == "truffleruby"
end
def test_args_types

View file

@ -8,8 +8,8 @@ class MiniRacerTest < Minitest::Test
# see `test_platform_set_flags_works` below
MiniRacer::Platform.set_flags! :use_strict
def test_locale
skip "TruffleRuby does not have all js timezone by default" if RUBY_ENGINE == "truffleruby"
val = MiniRacer::Context.new.eval("new Date('April 28 2021').toLocaleDateString('es-MX');")
assert_equal '28/4/2021', val
@ -46,7 +46,7 @@ class MiniRacerTest < Minitest::Test
def test_compile_nil_context
context = MiniRacer::Context.new
assert_raises(ArgumentError) do
assert_raises(TypeError) do
assert_equal 2, context.eval(nil)
end
end
@ -88,7 +88,7 @@ class MiniRacerTest < Minitest::Test
begin
Thread.new do
sleep 0.001
sleep 0.01
context.stop
end
context.eval('while(true){}')
@ -102,6 +102,7 @@ class MiniRacerTest < Minitest::Test
end
def test_it_can_timeout_during_serialization
skip "TruffleRuby needs a fix for timing out during translation" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(timeout: 500)
assert_raises(MiniRacer::ScriptTerminatedError) do
@ -302,12 +303,14 @@ raise FooError, "I like foos"
end
def test_max_memory
skip "TruffleRuby does not yet implement max_memory" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(max_memory: 200_000_000)
assert_raises(MiniRacer::V8OutOfMemoryError) { context.eval('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); };') }
end
def test_max_memory_for_call
skip "TruffleRuby does not yet implement max_memory" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(max_memory: 100_000_000)
context.eval(<<~JS)
let s;
@ -399,6 +402,7 @@ raise FooError, "I like foos"
end
def test_snapshot_size
skip "TruffleRuby does not yet implement snapshots" if RUBY_ENGINE == "truffleruby"
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
# for some reason sizes seem to change across runs, so we just
@ -407,6 +411,7 @@ raise FooError, "I like foos"
end
def test_snapshot_dump
skip "TruffleRuby does not yet implement snapshots" if RUBY_ENGINE == "truffleruby"
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
dump = snapshot.dump
@ -433,7 +438,7 @@ raise FooError, "I like foos"
end
def test_snapshots_can_be_warmed_up_with_no_side_effects
# shamelessly insipired by https://github.com/v8/v8/blob/5.3.254/test/cctest/test-serialize.cc#L792-L854
# shamelessly inspired by https://github.com/v8/v8/blob/5.3.254/test/cctest/test-serialize.cc#L792-L854
snapshot_source = <<-JS
function f() { return Math.sin(1); }
var a = 5;
@ -463,7 +468,7 @@ raise FooError, "I like foos"
end
def test_invalid_warmup_sources_throw_an_exception_2
assert_raises(ArgumentError) do
assert_raises(TypeError) do
MiniRacer::Snapshot.new('function f() { return 1 }').warmup!([])
end
end
@ -581,13 +586,11 @@ raise FooError, "I like foos"
def test_concurrent_access_over_the_same_isolate_2
isolate = MiniRacer::Isolate.new
equals_after_sleep = {}
# workaround Rubies prior to commit 475c8701d74ebebe
# (Make SecureRandom support Ractor, 2020-09-04)
SecureRandom.hex
(1..10).map do |i|
equals_after_sleep = (1..10).map do |i|
Thread.new {
random = SecureRandom.hex
context = MiniRacer::Context.new(isolate: isolate)
@ -598,12 +601,12 @@ raise FooError, "I like foos"
# cruby hashes are thread safe as long as you don't mess with the
# same key in different threads
equals_after_sleep[i] = context.eval('a') == random
context.eval('a') == random
}
end.each(&:join)
end.map(&:value)
assert_equal 10, equals_after_sleep.size
assert equals_after_sleep.values.all?
assert equals_after_sleep.all?
end
def test_platform_set_flags_raises_an_exception_if_already_initialized
@ -645,7 +648,7 @@ raise FooError, "I like foos"
def test_timeout_in_ruby_land
context = MiniRacer::Context.new(timeout: 50)
context.attach('sleep', proc{ sleep 0.1 })
context.attach('sleep', proc{ sleep 0.5 })
assert_raises(MiniRacer::ScriptTerminatedError) do
context.eval('sleep(); "hi";')
end
@ -711,6 +714,7 @@ raise FooError, "I like foos"
end
def test_estimated_size
skip "TruffleRuby does not yet implement heap_stats" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(timeout: 5)
context.eval("let a='testing';")
@ -754,7 +758,7 @@ raise FooError, "I like foos"
context.eval("'#{"x" * 10_000_000}'")
sleep 0.005
sleep 0.01
end_heap = context.heap_stats[:used_heap_size]
@ -780,7 +784,7 @@ raise FooError, "I like foos"
def test_estimated_size_when_disposed
context = MiniRacer::Context.new(timeout: 5)
context = MiniRacer::Context.new(timeout: 50)
context.eval("let a='testing';")
context.dispose
@ -805,7 +809,7 @@ raise FooError, "I like foos"
end
def test_attached_recursion
context = MiniRacer::Context.new(timeout: 20)
context = MiniRacer::Context.new(timeout: 200)
context.attach("a", proc{|a| a})
context.attach("b", proc{|a| a})
@ -842,6 +846,7 @@ raise FooError, "I like foos"
end
def test_heap_dump
skip "TruffleRuby does not yet implement heap_dump" if RUBY_ENGINE == "truffleruby"
f = Tempfile.new("heap")
path = f.path
f.unlink
@ -873,6 +878,7 @@ raise FooError, "I like foos"
end
def test_cyclical_object_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -880,6 +886,7 @@ raise FooError, "I like foos"
end
def test_cyclical_array_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -887,6 +894,7 @@ raise FooError, "I like foos"
end
def test_cyclical_elem_in_array_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -894,9 +902,10 @@ raise FooError, "I like foos"
end
def test_infinite_object_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
js = <<~JS
var d=0;
function get(z) {
@ -911,6 +920,7 @@ raise FooError, "I like foos"
end
def test_deep_object_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -966,6 +976,7 @@ raise FooError, "I like foos"
end
def test_webassembly
skip "TruffleRuby does not enable WebAssembly by default" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new()
context.eval("let instance = null;")
filename = File.expand_path("../support/add.wasm", __FILE__)
@ -973,16 +984,16 @@ raise FooError, "I like foos"
context.attach("print", proc {|f| puts f})
context.eval <<~JS
WebAssembly
.instantiate(new Uint8Array(loadwasm()), {
wasi_snapshot_preview1: {
proc_exit: function() { print("exit"); },
args_get: function() { return 0 },
args_sizes_get: function() { return 0 }
}
})
.then(i => { instance = i["instance"];})
.catch(e => print(e.toString()));
WebAssembly
.instantiate(new Uint8Array(loadwasm()), {
wasi_snapshot_preview1: {
proc_exit: function() { print("exit"); },
args_get: function() { return 0 },
args_sizes_get: function() { return 0 }
}
})
.then(i => { instance = i["instance"];})
.catch(e => print(e.toString()));
JS
while !context.eval("instance") do
@ -1022,4 +1033,27 @@ raise FooError, "I like foos"
JS
end
end
def test_eval_returns_unfrozen_string
context = MiniRacer::Context.new
result = context.eval("'Hello George!'")
assert_equal("Hello George!", result)
assert_equal(false, result.frozen?)
end
def test_call_returns_unfrozen_string
context = MiniRacer::Context.new
context.eval('function hello(name) { return "Hello " + name + "!" }')
result = context.call('hello', 'George')
assert_equal("Hello George!", result)
assert_equal(false, result.frozen?)
end
def test_callback_string_arguments_are_not_frozen
context = MiniRacer::Context.new
context.attach("test", proc{ |text| text.frozen? })
frozen = context.eval("test('Hello George!')")
assert_equal(false, frozen)
end
end

View file

@ -23,6 +23,6 @@ trigger_gc
MiniRacer::Context.new.dispose
Process.wait fork { puts @ctx.eval("a"); @ctx.dispose; puts Process.pid; trigger_gc; puts "done #{Process.pid}" }
if Process.respond_to?(:fork)
Process.wait fork { puts @ctx.eval("a"); @ctx.dispose; puts Process.pid; trigger_gc; puts "done #{Process.pid}" }
end