#include #include #include #include #include #include #include #include #include #include #include #include #include using namespace v8; typedef struct { const char* data; int raw_size; } SnapshotInfo; class IsolateInfo { public: Isolate* isolate; ArrayBuffer::Allocator* allocator; StartupData* startup_data; bool interrupted; bool added_gc_cb; pid_t pid; VALUE mutex; class Lock { VALUE &mutex; public: Lock(VALUE &mutex) : mutex(mutex) { rb_mutex_lock(mutex); } ~Lock() { rb_mutex_unlock(mutex); } }; IsolateInfo() : isolate(nullptr), allocator(nullptr), startup_data(nullptr), interrupted(false), added_gc_cb(false), pid(getpid()), refs_count(0) { VALUE cMutex = rb_const_get(rb_cThread, rb_intern("Mutex")); mutex = rb_class_new_instance(0, nullptr, cMutex); } ~IsolateInfo(); void init(SnapshotInfo* snapshot_info = nullptr); void mark() { rb_gc_mark(mutex); } Lock createLock() { Lock lock(mutex); return lock; } void hold() { refs_count++; } void release() { if (--refs_count <= 0) { delete this; } } int refs() { return refs_count; } static void* operator new(size_t size) { return ruby_xmalloc(size); } static void operator delete(void *block) { xfree(block); } private: // how many references to this isolate exist // we can't rely on Ruby's GC for this, because Ruby could destroy the // isolate before destroying the contexts that depend on them. We'd need to // keep a list of linked contexts in the isolate to destroy those first when // isolate destruction was requested. Keeping such a list would require // notification from the context VALUEs when they are constructed and // destroyed. With a ref count, those notifications are still needed, but // we keep a simple int rather than a list of pointers. std::atomic_int refs_count; }; typedef struct { IsolateInfo* isolate_info; Persistent* context; } ContextInfo; typedef struct { bool parsed; bool executed; bool terminated; bool json; Persistent* value; Persistent* message; Persistent* backtrace; } EvalResult; typedef struct { ContextInfo* context_info; Local* eval; Local* filename; useconds_t timeout; EvalResult* result; size_t max_memory; } EvalParams; typedef struct { ContextInfo *context_info; char *function_name; int argc; bool error; Local fun; Local *argv; EvalResult result; size_t max_memory; } FunctionCall; enum IsolateFlags { IN_GVL, DO_TERMINATE, MEM_SOFTLIMIT_VALUE, MEM_SOFTLIMIT_REACHED, }; static VALUE rb_cContext; static VALUE rb_cSnapshot; static VALUE rb_cIsolate; static VALUE rb_eScriptTerminatedError; static VALUE rb_eV8OutOfMemoryError; static VALUE rb_eParseError; static VALUE rb_eScriptRuntimeError; static VALUE rb_cJavaScriptFunction; static VALUE rb_eSnapshotError; static VALUE rb_ePlatformAlreadyInitializedError; static VALUE rb_mJSON; static VALUE rb_cFailedV8Conversion; static VALUE rb_cDateTime = Qnil; static std::unique_ptr 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; // guarded by exit_lock 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)); } platform_lock.lock(); if (current_platform == NULL) { V8::SetFlagsFromString(RSTRING_PTR(flag_as_str), (int)RSTRING_LEN(flag_as_str)); } else { platform_already_initialized = true; } platform_lock.unlock(); // important to raise outside of the lock if (platform_already_initialized) { rb_raise(rb_ePlatformAlreadyInitializedError, "The V8 platform is already initialized"); } return Qnil; } static void init_v8() { // no need to wait for the lock if already initialized if (current_platform != NULL) return; platform_lock.lock(); if (current_platform == NULL) { V8::InitializeICU(); current_platform = platform::NewDefaultPlatform(); V8::InitializePlatform(current_platform.get()); V8::Initialize(); } platform_lock.unlock(); } static void gc_callback(Isolate *isolate, GCType type, GCCallbackFlags flags) { if((bool)isolate->GetData(MEM_SOFTLIMIT_REACHED)) return; size_t softlimit = *(size_t*) isolate->GetData(MEM_SOFTLIMIT_VALUE); HeapStatistics stats; isolate->GetHeapStatistics(&stats); size_t used = stats.used_heap_size(); if(used > softlimit) { isolate->SetData(MEM_SOFTLIMIT_REACHED, (void*)true); isolate->TerminateExecution(); } } // to be called with active lock and scope static void prepare_result(MaybeLocal v8res, TryCatch& trycatch, Isolate* isolate, Local context, EvalResult& evalRes /* out */) { // just don't touch .parsed evalRes.terminated = false; evalRes.json = false; evalRes.value = nullptr; evalRes.message = nullptr; evalRes.backtrace = nullptr; evalRes.executed = !v8res.IsEmpty(); if (evalRes.executed) { // arrays and objects get converted to json Local local_value = v8res.ToLocalChecked(); if ((local_value->IsObject() || local_value->IsArray()) && !local_value->IsDate() && !local_value->IsFunction()) { Local JSON = context->Global()->Get( context, String::NewFromUtf8Literal(isolate, "JSON")) .ToLocalChecked().As(); Local stringify = JSON->Get( context, v8::String::NewFromUtf8Literal(isolate, "stringify")) .ToLocalChecked().As(); Local object = local_value->ToObject(context).ToLocalChecked(); const unsigned argc = 1; Local argv[argc] = { object }; MaybeLocal json = stringify->Call(context, JSON, argc, argv); if (json.IsEmpty()) { evalRes.executed = false; } else { evalRes.json = true; Persistent* persistent = new Persistent(); persistent->Reset(isolate, json.ToLocalChecked()); evalRes.value = persistent; } } else { Persistent* persistent = new Persistent(); persistent->Reset(isolate, local_value); evalRes.value = persistent; } } if (!evalRes.executed || !evalRes.parsed) { if (trycatch.HasCaught()) { if (!trycatch.Exception()->IsNull()) { evalRes.message = new Persistent(); Local message = trycatch.Message(); char buf[1000]; int len, line, column; if (!message->GetLineNumber(context).To(&line)) { line = 0; } if (!message->GetStartColumn(context).To(&column)) { column = 0; } len = snprintf(buf, sizeof(buf), "%s at %s:%i:%i", *String::Utf8Value(isolate, message->Get()), *String::Utf8Value(isolate, message->GetScriptResourceName()->ToString(context).ToLocalChecked()), line, column); if ((size_t) len >= sizeof(buf)) { len = sizeof(buf) - 1; buf[len] = '\0'; } Local v8_message = String::NewFromUtf8(isolate, buf, NewStringType::kNormal, len).ToLocalChecked(); evalRes.message->Reset(isolate, v8_message); } else if(trycatch.HasTerminated()) { evalRes.terminated = true; evalRes.message = new Persistent(); Local tmp = String::NewFromUtf8Literal(isolate, "JavaScript was terminated (either by timeout or explicitly)"); evalRes.message->Reset(isolate, tmp); } if (!trycatch.StackTrace(context).IsEmpty()) { evalRes.backtrace = new Persistent(); evalRes.backtrace->Reset(isolate, trycatch.StackTrace(context).ToLocalChecked()->ToString(context).ToLocalChecked()); } } } } void* nogvl_context_eval(void* arg) { EvalParams* eval_params = (EvalParams*)arg; EvalResult* result = eval_params->result; IsolateInfo* isolate_info = eval_params->context_info->isolate_info; Isolate* isolate = isolate_info->isolate; Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); TryCatch trycatch(isolate); Local context = eval_params->context_info->context->Get(isolate); Context::Scope context_scope(context); v8::ScriptOrigin *origin = NULL; // in gvl flag isolate->SetData(IN_GVL, (void*)false); // terminate ASAP isolate->SetData(DO_TERMINATE, (void*)false); // Memory softlimit isolate->SetData(MEM_SOFTLIMIT_VALUE, (void*)false); // Memory softlimit hit flag isolate->SetData(MEM_SOFTLIMIT_REACHED, (void*)false); MaybeLocal