diff --git a/ext/mini_racer_extension/mini_racer_extension.cc b/ext/mini_racer_extension/mini_racer_extension.cc index 9414407a..3f3524ec 100644 --- a/ext/mini_racer_extension/mini_racer_extension.cc +++ b/ext/mini_racer_extension/mini_racer_extension.cc @@ -188,6 +188,14 @@ static VALUE rb_platform_set_flag_as_str(VALUE _klass, VALUE flag_as_str) { return Qnil; } +typedef struct { + Persistent>* v8_func; + // Persistent* v8_func; + Persistent* context; + Isolate* isolate; +} JavascriptFunctionInfo; + + static void init_v8() { // no need to wait for the lock if already initialized if (current_platform != NULL) return; @@ -428,7 +436,24 @@ static VALUE convert_v8_to_ruby(Isolate* isolate, Local context, } if (value->IsFunction()){ - return rb_funcall(rb_cJavaScriptFunction, rb_intern("new"), 0); + Locker lock(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + + JavascriptFunctionInfo* javascript_function_info; + VALUE rb_func = rb_funcall(rb_cJavaScriptFunction, rb_intern("new"), 0); + JavascriptFunctionInfo* fn_info = Data_Get_Struct(rb_func, JavascriptFunctionInfo, javascript_function_info); + + Handle func = Handle::Cast(value); + fn_info->v8_func = new Persistent>(); + // fn_info->v8_func = new Persistent(); + fn_info->v8_func->Reset(isolate, func); + + fn_info->context = new Persistent(); + fn_info->context->Reset(isolate, context); + fn_info->isolate = isolate; + + return rb_func; } if (value->IsDate()){ @@ -800,6 +825,34 @@ static VALUE rb_isolate_pump_message_loop(VALUE self) { return Qfalse; } } +static void deallocate_javascript_function(void* data) { + // JavascriptFunctionInfo* javascript_function_info = (JavascriptFunctionInfo*) data; + + // javascript_function_info->v8_func->Dispose(); +} + +static void rb_javascript_function_call(VALUE self) { + JavascriptFunctionInfo* javascript_function_info; + Data_Get_Struct(self, JavascriptFunctionInfo, javascript_function_info); + Locker lock(javascript_function_info->isolate); + Isolate::Scope isolate_scope(javascript_function_info->isolate); + HandleScope scope(javascript_function_info->isolate); + Local ctx = Local::New(javascript_function_info->isolate, *javascript_function_info->context); + Local cb = Local::New(javascript_function_info->isolate, *javascript_function_info->v8_func); + + cb->Call(ctx, ctx->Global(), 0, NULL); +} + +static VALUE allocate_javascript_function(VALUE klass) { + JavascriptFunctionInfo* javascript_function_info = ALLOC(JavascriptFunctionInfo); + javascript_function_info->v8_func = NULL; + + return Data_Wrap_Struct(klass, NULL, deallocate_javascript_function, javascript_function_info); +} + +static VALUE rb_javascript_function_init_unsafe(VALUE self, VALUE isolate, VALUE snap) { + return Qnil; +} static VALUE rb_context_init_unsafe(VALUE self, VALUE isolate, VALUE snap) { ContextInfo* context_info; @@ -1675,6 +1728,9 @@ extern "C" { rb_eScriptRuntimeError = rb_define_class_under(rb_mMiniRacer, "RuntimeError", rb_eEvalError); rb_cJavaScriptFunction = rb_define_class_under(rb_mMiniRacer, "JavaScriptFunction", rb_cObject); + rb_define_alloc_func(rb_cJavaScriptFunction, allocate_javascript_function); + rb_define_method(rb_cJavaScriptFunction, "call", (VALUE(*)(...))&rb_javascript_function_call, 0); + rb_eSnapshotError = rb_define_class_under(rb_mMiniRacer, "SnapshotError", rb_eError); rb_ePlatformAlreadyInitializedError = rb_define_class_under(rb_mMiniRacer, "PlatformAlreadyInitialized", rb_eError); rb_cFailedV8Conversion = rb_define_class_under(rb_mMiniRacer, "FailedV8Conversion", rb_cObject); diff --git a/lib/mini_racer.rb b/lib/mini_racer.rb index 2e62e469..51292acc 100644 --- a/lib/mini_racer.rb +++ b/lib/mini_racer.rb @@ -130,7 +130,7 @@ def initialize(name, callback, parent) end end - def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil) + def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, set_timeout: false) options ||= {} check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout) @@ -162,6 +162,8 @@ def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle # defined in the C class init_unsafe(isolate, snapshot) + + install_set_timeout if set_timeout end def isolate @@ -397,6 +399,11 @@ def assert_option_is_nil_or_a(option_name, object, klass) raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}" end end + + def install_set_timeout + @timer = MessageBus::TimerThread.new + attach('setTimeout', proc { |cb, ms| @timer.queue(ms / 1000) { cb.call } }) + end end # `size` and `warmup!` public methods are defined in the C class diff --git a/mini_racer.gemspec b/mini_racer.gemspec index 9f1243c0..8516efd2 100644 --- a/mini_racer.gemspec +++ b/mini_racer.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", ">= 12.3.3" spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "rake-compiler" + spec.add_development_dependency "message_bus" spec.add_development_dependency "m" spec.add_dependency 'libv8', MiniRacer::LIBV8_VERSION diff --git a/test/function_test.rb b/test/function_test.rb index 84e1a074..b53a7116 100644 --- a/test/function_test.rb +++ b/test/function_test.rb @@ -1,5 +1,6 @@ require 'test_helper' require 'timeout' +require 'message_bus' class MiniRacerFunctionTest < Minitest::Test def test_fun @@ -61,6 +62,21 @@ def test_complex_return assert_equal h, res end + def test_javascript_function_call + context = MiniRacer::Context.new + timer = MessageBus::TimerThread.new + + handler = proc do |cb, ms| + timer.queue(ms / 1000) { cb.call } + end + + context.attach('setTimeout', handler) + + context.eval('var that = this; setTimeout(function() { that.called = true }, 1000);') + sleep 3 + assert context.eval('this.called') + end + def test_do_not_hang_with_concurrent_calls context = MiniRacer::Context.new context.eval("function f(x) { return 'I need ' + x + ' foos' }") diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index 81af9fea..76912977 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -915,4 +915,17 @@ def test_webassembly assert_equal(3, context.eval("instance.exports.add(1,2)")) end + + def test_set_timeout + context = MiniRacer::Context.new(set_timeout: true) + + context.eval <<~JS + var that = this; + setTimeout(() => that.called = true, 1000); + JS + + sleep 2 + + assert context.eval("this.called") + end end