From 68f514b92f7ce6bd8481ba26527a88f17d54af02 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Fri, 3 Nov 2017 16:22:04 -0300 Subject: [PATCH] Implement EventSubscriber class. This class simplifies the task of subscribing/handling javascript events from C++ classes in the main process. --- atom/browser/api/event_subscriber.cc | 121 ++++++++++++++++++++++++ atom/browser/api/event_subscriber.h | 132 +++++++++++++++++++++++++++ filenames.gypi | 2 + 3 files changed, 255 insertions(+) create mode 100644 atom/browser/api/event_subscriber.cc create mode 100644 atom/browser/api/event_subscriber.h diff --git a/atom/browser/api/event_subscriber.cc b/atom/browser/api/event_subscriber.cc new file mode 100644 index 000000000000..7e9bef873f05 --- /dev/null +++ b/atom/browser/api/event_subscriber.cc @@ -0,0 +1,121 @@ +// Copyright (c) 2017 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. +#include + +#include "atom/browser/api/event_subscriber.h" +#include "atom/common/native_mate_converters/callback.h" + +namespace { + +// A FunctionTemplate lifetime is bound to the v8 context, so it can be safely +// stored as a global here since there's only one for the main process. +v8::Global g_cached_template; + +struct JSHandlerData { + JSHandlerData(v8::Isolate* isolate, + mate::internal::EventSubscriberBase* subscriber) + : handle_(isolate, v8::External::New(isolate, this)), + subscriber_(subscriber) { + handle_.SetWeak(this, GC, v8::WeakCallbackType::kFinalizer); + } + + static void GC(const v8::WeakCallbackInfo& data) { + delete data.GetParameter(); + } + + v8::Global handle_; + mate::internal::EventSubscriberBase* subscriber_; +}; + +void InvokeCallback(const v8::FunctionCallbackInfo& info) { + v8::Locker locker(info.GetIsolate()); + v8::HandleScope handle_scope(info.GetIsolate()); + v8::Local context = info.GetIsolate()->GetCurrentContext(); + v8::Context::Scope context_scope(context); + mate::Arguments args(info); + v8::Local handler, event; + args.GetNext(&handler); + args.GetNext(&event); + DCHECK(handler->IsExternal()); + DCHECK(event->IsString()); + JSHandlerData* handler_data = static_cast( + v8::Local::Cast(handler)->Value()); + handler_data->subscriber_->EventEmitted(mate::V8ToString(event), &args); +} + +} // namespace + +namespace mate { + +namespace internal { + +EventSubscriberBase::EventSubscriberBase(v8::Isolate* isolate, + v8::Local emitter) + : isolate_(isolate), emitter_(isolate, emitter) { + if (g_cached_template.IsEmpty()) { + g_cached_template = v8::Global( + isolate_, v8::FunctionTemplate::New(isolate_, InvokeCallback)); + } +} + +EventSubscriberBase::~EventSubscriberBase() { + if (!isolate_) { + return; + } + RemoveAllListeners(); + emitter_.Reset(); + DCHECK_EQ(js_handlers_.size(), 0); +} + +void EventSubscriberBase::On(const std::string& event_name) { + DCHECK(js_handlers_.find(event_name) == js_handlers_.end()); + v8::Locker locker(isolate_); + v8::Isolate::Scope isolate_scope(isolate_); + v8::HandleScope handle_scope(isolate_); + auto fn_template = g_cached_template.Get(isolate_); + auto event = mate::StringToV8(isolate_, event_name); + auto js_handler_data = new JSHandlerData(isolate_, this); + v8::Local fn = internal::BindFunctionWith( + isolate_, isolate_->GetCurrentContext(), fn_template->GetFunction(), + js_handler_data->handle_.Get(isolate_), event); + js_handlers_.insert( + std::make_pair(event_name, v8::Global(isolate_, fn))); + internal::ValueVector converted_args = {event, fn}; + internal::CallMethodWithArgs(isolate_, emitter_.Get(isolate_), "on", + &converted_args); +} + +void EventSubscriberBase::Off(const std::string& event_name) { + v8::Locker locker(isolate_); + v8::Isolate::Scope isolate_scope(isolate_); + v8::HandleScope handle_scope(isolate_); + auto js_handler = js_handlers_.find(event_name); + DCHECK(js_handler != js_handlers_.end()); + RemoveListener(js_handler); +} + +void EventSubscriberBase::RemoveAllListeners() { + v8::Locker locker(isolate_); + v8::Isolate::Scope isolate_scope(isolate_); + v8::HandleScope handle_scope(isolate_); + while (!js_handlers_.empty()) { + RemoveListener(js_handlers_.begin()); + } +} + +std::map>::iterator +EventSubscriberBase::RemoveListener( + std::map>::iterator it) { + internal::ValueVector args = {StringToV8(isolate_, it->first), + it->second.Get(isolate_)}; + internal::CallMethodWithArgs( + isolate_, v8::Local::Cast(emitter_.Get(isolate_)), + "removeListener", &args); + it->second.Reset(); + return js_handlers_.erase(it); +} + +} // namespace internal + +} // namespace mate diff --git a/atom/browser/api/event_subscriber.h b/atom/browser/api/event_subscriber.h new file mode 100644 index 000000000000..2f4f2396be46 --- /dev/null +++ b/atom/browser/api/event_subscriber.h @@ -0,0 +1,132 @@ +// Copyright (c) 2017 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_EVENT_SUBSCRIBER_H_ +#define ATOM_BROWSER_API_EVENT_SUBSCRIBER_H_ + +#include +#include + +#include "atom/common/api/event_emitter_caller.h" +#include "base/synchronization/lock.h" +#include "content/public/browser/browser_thread.h" +#include "native_mate/native_mate/arguments.h" + +namespace mate { + +namespace internal { + +class EventSubscriberBase { + public: + EventSubscriberBase(v8::Isolate* isolate, v8::Local emitter); + virtual ~EventSubscriberBase(); + virtual void EventEmitted(const std::string& event_name, + mate::Arguments* args) = 0; + + protected: + void On(const std::string& event_name); + void Off(const std::string& event_name); + void RemoveAllListeners(); + + private: + std::map>::iterator RemoveListener( + std::map>::iterator it); + + v8::Isolate* isolate_; + v8::Global emitter_; + std::map> js_handlers_; + + DISALLOW_COPY_AND_ASSIGN(EventSubscriberBase); +}; + +} // namespace internal + +template +class EventSubscriber : internal::EventSubscriberBase { + public: + using EventCallback = void (HandlerType::*)(mate::Arguments* args); + // Alias to unique_ptr with deleter. + using unique_ptr = std::unique_ptr, + void (*)(EventSubscriber*)>; + // EventSubscriber should only be created/deleted in the main thread since it + // communicates with the V8 engine. This smart pointer makes it simpler to + // bind the lifetime of EventSubscriber with a class whose lifetime is managed + // by a non-UI thread. + class SafePtr : public unique_ptr { + public: + SafePtr() : SafePtr(nullptr) {} + explicit SafePtr(EventSubscriber* ptr) + : unique_ptr(ptr, Deleter) {} + + private: + // Custom deleter that schedules destructor invocation to the main thread. + static void Deleter(EventSubscriber* ptr) { + DCHECK( + !::content::BrowserThread::CurrentlyOn(::content::BrowserThread::UI)); + DCHECK(ptr); + // Acquire handler lock and reset handler_ to ensure that any new events + // emitted will be ignored after this function returns + base::AutoLock auto_lock(ptr->handler_lock_); + ptr->handler_ = nullptr; + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, + base::Bind( + [](EventSubscriber* subscriber) { + delete subscriber; + }, + ptr)); + } + }; + + EventSubscriber(HandlerType* handler, + v8::Isolate* isolate, + v8::Local emitter) + : EventSubscriberBase(isolate, emitter), handler_(handler) { + DCHECK_CURRENTLY_ON(::content::BrowserThread::UI); + } + + void On(const std::string& event, EventCallback callback) { + DCHECK_CURRENTLY_ON(::content::BrowserThread::UI); + EventSubscriberBase::On(event); + callbacks_.insert(std::make_pair(event, callback)); + } + + void Off(const std::string& event) { + DCHECK_CURRENTLY_ON(::content::BrowserThread::UI); + EventSubscriberBase::Off(event); + DCHECK(callbacks_.find(event) != callbacks_.end()); + callbacks_.erase(callbacks_.find(event)); + } + + void RemoveAllListeners() { + DCHECK_CURRENTLY_ON(::content::BrowserThread::UI); + EventSubscriberBase::RemoveAllListeners(); + callbacks_.clear(); + } + + private: + void EventEmitted(const std::string& event_name, + mate::Arguments* args) override { + DCHECK_CURRENTLY_ON(::content::BrowserThread::UI); + base::AutoLock auto_lock(handler_lock_); + if (!handler_) { + // handler_ was probably destroyed by another thread and we should not + // access it. + return; + } + auto it = callbacks_.find(event_name); + if (it != callbacks_.end()) { + auto method = it->second; + (handler_->*method)(args); + } + } + + HandlerType* handler_; + base::Lock handler_lock_; + std::map callbacks_; +}; + +} // namespace mate + +#endif // ATOM_BROWSER_API_EVENT_SUBSCRIBER_H_ diff --git a/filenames.gypi b/filenames.gypi index 5b2e8c4feb87..2f17ec1086b2 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -163,6 +163,8 @@ 'atom/browser/api/event.h', 'atom/browser/api/event_emitter.cc', 'atom/browser/api/event_emitter.h', + 'atom/browser/api/event_subscriber.cc', + 'atom/browser/api/event_subscriber.h', 'atom/browser/api/trackable_object.cc', 'atom/browser/api/trackable_object.h', 'atom/browser/api/frame_subscriber.cc',